{
  "source_file": "0930",
  "source_platform": "Dev.to",
  "author": "xbill",
  "title_listed": "2B Gemma 4 Deployment with Cloud Run, NVIDIA L4, MCP SDK 2.x, and Claude Code",
  "keywords": "",
  "domain": "大模型算法",
  "content_type": "工程实践",
  "is_paid_flag": "否",
  "url": "https://dev.to/gde/2b-gemma-4-deployment-with-cloud-run-nvidia-l4-mcp-sdk-2x-and-claude-code-4ml3",
  "publish_date": "2026-09-10",
  "corpus_stage": "",
  "notes": "",
  "id": "0c2268fdaf85",
  "collected_at": "2026-10-02T13:51:37",
  "title": "2B Gemma 4 Deployment with Cloud Run, NVIDIA L4, MCP SDK 2.x, and Claude Code - DEV Community",
  "body_markdown": "This article provides a step by step deployment guide for Gemma 4 E2B to a Cloud Run hosted GPU enabled system. A suite of Python MCP tools is built to simplify management of the vLLM hosted deployment with Claude Code.\nhttps://github.com/xbill9/gemma4-dev/tree/main/gpu-2B-cloudrun-devops-agent\nWhat is this project trying to Do?\nThis project is a DevOps/SRE assistant for a Gemma 4 model served by vLLM on Cloud Run with an NVIDIA L4 GPU. A single-file Python MCP server provides tools to stage the weights, deploy the service, check its health, benchmark it, and tear it down.\nCloud Run is the serverless option. There is no VM to provision and no driver to install: the service scales to zero when idle, and one\ngcloud\ncommand attaches the GPU.\nAlong the way the MCP server itself had to move to the MCP Python SDK 2.x, because a fresh\npip install\nstopped it from starting. That migration is covered where it happened — in the MCP server section, before the deploy.\nWhere do I start?\nThe strategy for starting MCP development for model management is a incremental step by step approach.\nFirst, the basic development environment is setup with the required system variables and a working Claude Code configuration.\nThen, the Python MCP server is brought up over stdio and validated with Claude Code in the local environment. That server then stages the model, deploys it to Cloud Run, and drives validation and a benchmark sweep against the live endpoint.\nAt This Point You Should Have…\nPython 3.10 or newer — mcp 2.x declares\nRequires-Python >=3.10\nClaude Code installed and working\nThe Google Cloud SDK, logged in, with application default credentials\nA Google Cloud project with Cloud Run GPU access in your region (this one runs in\nus-east4\n)\nA GCS bucket named\n<project>-bucket\nfor the model weights\nSetup the Basic Environment\nClone the repository and switch to the Cloud Run directory:\ncd\n~\ngit clone https://github.com/xbill9/gemma4-dev\ncd\ngemma4-dev/gpu-2B-cloudrun-devops-agent\nEnter fullscreen mode\nExit fullscreen mode\nThen run\ninit.sh\nonce. It checks your\ngcloud\nlogin and application default credentials, asks for a project ID, installs the Python requirements, enables the Cloud Run, Secret Manager and related APIs, and grants the default compute service account its roles. It pauses on errors and waits for input, so run it in a terminal:\nsource\ninit.sh\nEnter fullscreen mode\nExit fullscreen mode\nIf your session times out or you need to reset your variables, run\nset_env.sh\n:\nsource\nset_env.sh\nEnter fullscreen mode\nExit fullscreen mode\nCurrent Environment:\n  GOOGLE_CLOUD_PROJECT=aisprint-491218\n  GOOGLE_CLOUD_LOCATION=us-east4\n  SERVICE_NAME=gpu-2b-l4-devops-agent\n  MODEL_NAME=/mnt/models/gemma-4-E2B-it\n  VLLM_BASE_URL=<unset — discovered via gcloud>\n\nCloud Run here is --no-allow-unauthenticated. If calls fail, run: source ./set_adc.sh\nEnter fullscreen mode\nExit fullscreen mode\nVLLM_BASE_URL\ncan stay unset: the MCP server finds the service URL through\ngcloud\nwhen a tool first needs it.\nModel Management Tool with MCP Stdio Transport\nOne of the key features that the MCP libraries provide is abstracting various transport methods. The tool implementation is the same no matter which transport the MCP client uses to connect.\nThe simplest transport is stdio — the client launches the server as a local process and talks to it over stdin and stdout. Both must run in the same environment. In this project Claude Code is the MCP client.\nThe server is created in one line:\n# Initialize MCP server (mcp 2.x; FastMCP was renamed MCPServer)\nmcp\n=\nMCPServer\n(\n\"\nSelf-Hosted vLLM DevOps Agent\n\"\n)\nEnter fullscreen mode\nExit fullscreen mode\nThat line used to say\nFastMCP\n. The next section is why it changed.\nWait — Why MCPServer and not FastMCP?\nNothing in the repository changed. A fresh install did.\nrequirements.txt\nlisted\nmcp\nwith no version bound, so the next\npip install\nresolved the 2.x line, and the server stopped importing:\npython3\n-c\n\"from mcp.server.fastmcp import FastMCP\"\nEnter fullscreen mode\nExit fullscreen mode\nraise ModuleNotFoundError(_MESSAGE, name=__name__)\nModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer (from mcp.server.mcpserver import MCPServer) and other APIs changed; see the migration guide at https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver or pin 'mcp<2' to keep running v1 code.\nEnter fullscreen mode\nExit fullscreen mode\nIn Claude Code it showed up less helpfully, as a server listed with\nConnection closed\n: it died on the import before the handshake.\nThe error names two fixes. Pinning\nmcp<2\nis legitimate — the v1 line still gets critical fixes — but these projects install into one system Python with no virtualenvs, so a pin here is a downgrade for every other project on the machine. Migrating keeps the change inside the repository.\nThe whole code change is the import and the constructor:\n-from mcp.server.fastmcp import FastMCP\n+from mcp.server.mcpserver import MCPServer\n...\n-mcp = FastMCP(\"Self-Hosted vLLM DevOps Agent\")\n+mcp = MCPServer(\"Self-Hosted vLLM DevOps Agent\")\nEnter fullscreen mode\nExit fullscreen mode\n@mcp.tool()\n,\n@mcp.resource()\n,\nmcp.run()\nand every tool body stay as they are. Before editing, check the rest of the migration guide's list against your own server:\ngrep\n-c\n\"^@mcp\n\\.\\(\ntool\n\\|\nresource\n\\)\n\"\nserver.py\ngrep\n-A1\n\"^@mcp\n\\.\n\"\nserver.py |\ngrep\n-c\n\"^def\"\ngrep\n-n\n\"get_running_loop\n\\|\nasyncio.run(\"\nserver.py\n||\necho\n\"(no matches)\"\ngrep\n-n\n\"^import httpx\"\nserver.py\n;\ngrep\n-n\n\"^httpx\"\nrequirements.txt\nEnter fullscreen mode\nExit fullscreen mode\n28\n12\n(no matches)\n13:import httpx\n14:httpx\nEnter fullscreen mode\nExit fullscreen mode\nThree things to know from that output:\nmcp\n2.x no longer installs\nhttpx\n.\nIt depends on\nhttpx2\ninstead. This server imports\nhttpx\nand was safe only because\nrequirements.txt\nalready declared it.\nThe 12 sync handlers now run on a worker thread.\nOnly code that expects the event loop's thread breaks, and there is none here.\nThe server's version is now blank\nunless you pass\nversion=\nto\nMCPServer(...)\n. Nothing breaks; it shows in the handshake below.\nThe requirement then says what the code needs —\nmcp>=2\nin place of the bare\nmcp\nline.\n🔎 Tip: Change the Usage Before the Import\nThis project has a Claude Code hook that runs\nruff check --fix\nafter every edit. Change the import line first and, for a moment,\nMCPServer\nis imported but unused — so the hook deletes it:\n--- server.py\n+++ server.py\n@@ -1,3 +1,2 @@\n-from mcp.server.mcpserver import MCPServer\n\n mcp = FastMCP(\"demo\")\n\nWould fix 1 error.\nEnter fullscreen mode\nExit fullscreen mode\nMake both changes in one edit, or change the usage first. Any editor that runs\nruff check --fix\non save does the same thing.\nRunning the Python Code\nThe project can be linted:\nmake lint\nEnter fullscreen mode\nExit fullscreen mode\nruff check .\nAll checks passed!\nruff format --check .\n14 files already formatted\nmypy .\nSuccess: no issues found in 6 source files\nEnter fullscreen mode\nExit fullscreen mode\nand tested:\nmake\ntest\nEnter fullscreen mode\nExit fullscreen mode\n----------------------------------------------------------------------\nRan 28 tests in 1.151s\n\nOK\nEnter fullscreen mode\nExit fullscreen mode\nThe suite compares the registered tool set against a hard-coded list, so a tool that silently failed to register after the rename would fail here. ✅\nTest the Protocol by Hand\nUnit tests call Python. A client speaks JSON-RPC over stdio, so test that too.\nHold stdin open with\nsleep\n— with a bare\nprintf\npipe the server sees end-of-input and exits after answering only\ninitialize\n:\n{\nprintf\n'%s\\n'\n\\\n'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"probe\",\"version\":\"0\"}}}'\n\\\n'{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}'\n\\\n'{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}'\n\\\n'{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"resources/list\",\"params\":{}}'\n;\nsleep\n5\n;\n}\n\\\n| python3 server.py 2>/dev/null\nEnter fullscreen mode\nExit fullscreen mode\nSummarised:\ninitialize OK: name='Self-Hosted vLLM DevOps Agent' version='' proto 2025-06-18\ntools/list OK: 27 tools -> cloudrun_analyze_cloud_logging, cloudrun_analyze_gpu_logs, cloudrun_check_gpu_quotas, cloudrun_deploy, ...\nresources/list OK: ['config://vllm-deployment-template']\nEnter fullscreen mode\nExit fullscreen mode\n🟢 27 tools and the resource. Keep this snippet — it is the fastest way to tell \"my server is broken\" from \"my client config is broken.\"\nClaude Code .mcp.json\nClaude Code reads\n.mcp.json\nin the project directory. It launches\nserver.py\nwith the system\npython3\n:\n{\n\"mcpServers\"\n:\n{\n\"cloudrun-devops\"\n:\n{\n\"command\"\n:\n\"python3\"\n,\n\"args\"\n:\n[\n\"/home/xbill/gemma4-dev/gpu-2B-cloudrun-devops-agent/server.py\"\n],\n\"env\"\n:\n{\n\"GOOGLE_CLOUD_PROJECT\"\n:\n\"aisprint-491218\"\n,\n\"GOOGLE_CLOUD_LOCATION\"\n:\n\"us-east4\"\n,\n\"VLLM_BASE_URL\"\n:\n\"https://gpu-2b-l4-devops-agent-289270257791.us-east4.run.app\"\n,\n\"MODEL_NAME\"\n:\n\"/mnt/models/gemma-4-E2B-it\"\n}\n}\n}\n}\nEnter fullscreen mode\nExit fullscreen mode\nValidation with Claude Code\nCheck the connection from Claude Code to the local server:\nclaude mcp get cloudrun-devops\nEnter fullscreen mode\nExit fullscreen mode\ncloudrun-devops:\n  Scope: Project config (shared via .mcp.json)\n  Status: ✔ Connected\nEnter fullscreen mode\nExit fullscreen mode\nIf the server failed at startup earlier in the session, reconnect it from\n/mcp\nor start a new session to pick up the fixed code.\nModel Lifecycle Management via MCP\nThe MCP tools cover the whole lifecycle of the Cloud Run deployment. Every tool is prefixed\ncloudrun_\n. Abridged output of\ncloudrun_get_help\n:\nThe server is running in CLOUD RUN mode targeting NVIDIA L4 GPU in region us-east4.\n\n🐳 Infrastructure & Deployment\n  cloudrun_deploy, cloudrun_destroy, cloudrun_status, cloudrun_update_scaling,\n  cloudrun_get_deployment_config, cloudrun_get_gpu_deployment_config, cloudrun_check_gpu_quotas\n📊 Model Management\n  cloudrun_list_vertex_models, cloudrun_list_bucket_models, cloudrun_save_hf_token,\n  cloudrun_get_vertex_ai_model_copy_instructions, cloudrun_get_huggingface_model_copy_instructions,\n  cloudrun_get_huggingfacehub_download_path\n📊 Monitoring & Status\n  cloudrun_get_metrics, cloudrun_get_system_status, cloudrun_get_endpoint,\n  cloudrun_get_endpoint_url, cloudrun_get_model_details, cloudrun_verify_model_health\n📈 Performance & Benchmarking\n  cloudrun_run_benchmark\n💬 Interaction & Diagnostics\n  cloudrun_query_gemma4, cloudrun_query_gemma4_with_stats, cloudrun_query,\n  cloudrun_analyze_cloud_logging, cloudrun_analyze_gpu_logs, cloudrun_suggest_sre_remediation\nEnter fullscreen mode\nExit fullscreen mode\nStage the Model Weights\nCloud Run mounts the bucket read-only at\n/mnt/models\nthrough GCS FUSE, so the weights go to GCS once. Download to a real disk rather than\n/tmp\n, which on this host is a RAM-backed tmpfs smaller than the model:\nhf download google/gemma-4-E2B-it\n--local-dir\n~/hf-downloads/gemma-4-E2B-it\ngcloud storage rsync ~/hf-downloads/gemma-4-E2B-it gs://aisprint-491218-bucket/gemma-4-E2B-it\n\\\n--recursive\n--exclude\n=\n'^\\.cache/'\ngcloud storage\nls\n-l\ngs://aisprint-491218-bucket/gemma-4-E2B-it/\nEnter fullscreen mode\nExit fullscreen mode\nAverage throughput: 41.4MiB/s\n      4954  2026-09-10T14:51:14Z  gs://aisprint-491218-bucket/gemma-4-E2B-it/config.json\n10246621918  2026-09-10T14:55:13Z  gs://aisprint-491218-bucket/gemma-4-E2B-it/model.safetensors\n  32169626  2026-09-10T14:51:34Z  gs://aisprint-491218-bucket/gemma-4-E2B-it/tokenizer.json\nTOTAL: 9 objects, 10278849571 bytes (9.57GiB)\nEnter fullscreen mode\nExit fullscreen mode\nThe bucket is shared with other models, and\ncloudrun_list_bucket_models\nshows the trap:\n> cloudrun_list_bucket_models\n\n### Contents of GCS Bucket: aisprint-491218-bucket\n- gemma-2b-it/config.json (0.00 MB)\n- gemma-2b-it/model-00001-of-00002.safetensors (4716.15 MB)\n...\n- gemma-4-12B-it-qat-w4a16-ct/model.safetensors (9788.73 MB)\n...\nEnter fullscreen mode\nExit fullscreen mode\nCheck the architecture, not the folder name.\ngemma-2b-it/\nlooks like the answer and is the original Gemma, which the\ngemma4\nparsers cannot serve.\nconfig.json\nsettles it:\ngcloud storage\ncat\ngs://aisprint-491218-bucket/gemma-4-E2B-it/config.json\n\\\n| python3\n-c\n'import json,sys; c=json.load(sys.stdin); t=c[\"text_config\"]; print(c[\"model_type\"], c[\"architectures\"], \"hidden\", t[\"hidden_size\"], \"layers\", t[\"num_hidden_layers\"])'\nEnter fullscreen mode\nExit fullscreen mode\ngemma4 ['Gemma4ForConditionalGeneration'] hidden 1536 layers 35\nEnter fullscreen mode\nExit fullscreen mode\nDeploy to Cloud Run\nThe\ndeploy-vllm\ntarget in the\nMakefile\nis the single source of truth for the vLLM and Cloud Run flags. The ones that matter most:\nFlag\nValue\nWhy\n--gpu-type\nnvidia-l4\none L4 per instance\n--no-gpu-zonal-redundancy\nthe cheaper L4 SKU\n--concurrency\n4\nrequests Cloud Run sends one instance\n--max-num-seqs\n8\nvLLM's batch ceiling\n--tool-call-parser\n,\n--reasoning-parser\ngemma4\nGemma 4 tool calling breaks without either\nThe service is also\n--no-allow-unauthenticated\n, so every caller needs an identity token.\nmake deploy\nEnter fullscreen mode\nExit fullscreen mode\nDeploying container to Cloud Run service [gpu-2b-l4-devops-agent] in project [aisprint-491218] region [us-east4]\nDeploying new service...\nRouting traffic.....done\nDone.\nService [gpu-2b-l4-devops-agent] revision [gpu-2b-l4-devops-agent-00001-ssq] has been deployed and is serving 100 percent of traffic.\nService URL: https://gpu-2b-l4-devops-agent-289270257791.us-east4.run.app\nEnter fullscreen mode\nExit fullscreen mode\ngcloud run deploy\nreturns only once the startup probe passes, and the probe waits\ninitialDelaySeconds=180\nbefore its first check. Expect several minutes.\nDemo Mode: Fixed Instances\nThe default autoscales between zero and one instance, which means a cold GPU start after the service goes idle. A demo cannot wait for that. The\nMakefile\ntakes a\nSCALING\nvariable:\nSCALING\n?=\nauto\nifeq\n($(SCALING),auto)\nSCALING_FLAGS\n=\n--scaling\n=\nauto\n--max-instances\n=\n1\n--min-instances\n=\n0\nelse\nSCALING_FLAGS\n=\n--scaling\n=\n$(\nSCALING\n)\nendif\nEnter fullscreen mode\nExit fullscreen mode\nmake deploy\nSCALING\n=\n1\ngcloud run services describe gpu-2b-l4-devops-agent\n--region\nus-east4\n--format\n=\n'yaml(metadata.annotations)'\nEnter fullscreen mode\nExit fullscreen mode\nrun.googleapis.com/manualInstanceCount: '1'\n    run.googleapis.com/scalingMode: manual\nEnter fullscreen mode\nExit fullscreen mode\nOne L4 now runs until you change it. Plain\nmake deploy\n, and the\ncloudrun_deploy\nand\ncloudrun_update_scaling\ntools, all pass\n--scaling=auto\non purpose — a service stuck in manual scaling at zero returns 503 to every request. So any of them quietly takes a demo back to scale-to-zero.\nChecking System Status\nThe status can be checked with an MCP tool:\n> cloudrun_get_system_status\n\n### 🌀 GPU Cloud Run System Status (gpu-2b-l4-devops-agent)\n- vLLM Health: 🟢 Online (https://gpu-2b-l4-devops-agent-289270257791.us-east4.run.app)\n- Cloud Run Service Status: 🟢 Ready\n👉 Next Step: Use cloudrun_query_gemma4 to interact with the model.\nEnter fullscreen mode\nExit fullscreen mode\nCross Check The Deployed Model\nAsk vLLM what it loaded:\ncurl\n-s\n-H\n\"Authorization: Bearer\n$(\ngcloud auth print-identity-token\n)\n\"\n\\\nhttps://gpu-2b-l4-devops-agent-289270257791.us-east4.run.app/v1/models | python3\n-m\njson.tool\nEnter fullscreen mode\nExit fullscreen mode\n{\n\"object\"\n:\n\"list\"\n,\n\"data\"\n:\n[\n{\n\"id\"\n:\n\"/mnt/models/gemma-4-E2B-it\"\n,\n\"object\"\n:\n\"model\"\n,\n\"owned_by\"\n:\n\"vllm\"\n,\n\"max_model_len\"\n:\n16384\n}\n]\n}\nEnter fullscreen mode\nExit fullscreen mode\nThe model id is the mount path, not the Hugging Face repo id. The container is started with\n--model=/mnt/models/<path>\n, so that path is the name the OpenAI API expects.\nThen the MCP health check:\n> cloudrun_verify_model_health\n\n✅ Model health check PASSED.\nModel: /mnt/models/gemma-4-E2B-it\nResponse: 'Hello! Yes, I am working. I am Gemma 4, a Large La...'\nLatency: 0.87 seconds.\nEnter fullscreen mode\nExit fullscreen mode\nRight after the first deploy the same check took 2.48 seconds. That answer came through the whole stack: Claude Code called the tool over MCP, and the tool called vLLM on Cloud Run. 🟢\nBenchmark the Model\nAsk the agent for\ncloudrun_run_benchmark\nwith its defaults: one warmup request, then 20 requests at each concurrency level of 1, 2, 4 and 8, up to 128 output tokens each, one fixed prompt at temperature 0.\nConcurrency\nReq/s\nTokens/s\nAvg latency\nP95 latency\n1\n0.39\n49.63\n2.58 s\n2.59 s\n2\n0.75\n95.36\n2.68 s\n2.74 s\n4\n1.47\n188.46\n2.71 s\n2.78 s\n8\n1.48\n189.53\n4.86 s\n5.47 s\nEvery request at every level succeeded. Three readings:\nFrom 1 to 4, throughput scales almost linearly.\n188.46 tokens/s is 3.8x the single-stream 49.63 (arithmetic), while average latency moves from 2.58 s to 2.71 s. The L4 is nowhere near full at 4.\nFrom 4 to 8, it stops.\nThroughput rises 0.6% (arithmetic) while average latency goes from 2.71 s to 4.86 s. Half the requests are waiting.\nThe ceiling is a Cloud Run setting, not the GPU.\nOne instance accepts\n--concurrency=4\nrequests at a time, and vLLM would batch up to\n--max-num-seqs=8\n. The next section checks that against the same chip with nothing in front of it.\nCompare to Other Deployments\nThe same checkpoint was served on the same chip — one NVIDIA L4 in an AWS EC2\ng6.2xlarge\n— with vLLM and\n--max-num-seqs 8\n, and swept with\nvllm bench serve\nat the same concurrency levels and 128 output tokens:\nConcurrency\nCloud Run L4 tok/s\nEC2 g6 L4 tok/s\n1\n49.63\n46.09\n🥇 Cloud Run\n2\n95.36\n92.6\n🥇 Cloud Run\n4\n188.46\n175.83\n🥇 Cloud Run\n8\n189.53\n360.17\n🥇 EC2 g6\nUp to 4, the two are the same chip doing the same work\n— within 8% of each other at every level, with Cloud Run slightly ahead.\nAt 8, the g6 keeps going.\nIt reaches 360.17 tokens/s, 1.9x Cloud Run's 189.53 (arithmetic), because nothing in front of vLLM caps admissions at 4. That is the direct evidence that Cloud Run's plateau is\n--concurrency=4\n, not the L4.\nRead the rows as a shape, not a leaderboard. The runs differ in engine version, KV-cache dtype, prompt and client location; the scope paragraph at the end lists each.\nAnd Price/Performance?\nCloud Run bills a GPU service by the second for the whole instance while it runs — GPU, CPU and memory — because the L4 requires\n--no-cpu-throttling\n. List prices in\nus-east4\nfrom the Cloud Billing catalog:\nComponent\nPrice\nPer hour\nNVIDIA L4, no zonal redundancy\n$0.0001867 / s\n$0.6721\n8 vCPU, instance-based\n$0.000018 / vCPU-s\n$0.5184\n32 GiB memory, instance-based\n$0.000002 / GiB-s\n$0.2304\nOne instance\n$1.4209\nThat is $34.10 a day in demo mode with one fixed instance (arithmetic). In the default mode an idle service scales to zero instances.\nPer million output tokens, from the benchmark (arithmetic):\nDeployment\nTokens/s\n$ / hour\n$ / M tokens\nCloud Run, concurrency 1\n49.63\n1.4209\n7.95\nCloud Run, concurrency 4\n188.46\n1.4209\n2.09\nCloud Run, concurrency 8\n189.53\n1.4209\n2.08\n🥇 EC2 g6 spot, concurrency 8\n360.17\n0.9412\n0.73\nThe winner is the VM, on two counts: a lower hourly rate, and twice the throughput with no admission cap. Cloud Run's price is list and on-demand; the g6 price is a spot price and can be reclaimed. What Cloud Run buys is no VM to manage and zero cost while idle — for intermittent SRE work that is the number that matters.\nTeardown\nmake destroy\nEnter fullscreen mode\nExit fullscreen mode\nNot run for this article — the demo service is still up. It deletes the Cloud Run service; the weights stay in the bucket for the next deploy.\nSummary\nThe goal of this article was to deploy Gemma 4 E2B to a Cloud Run NVIDIA L4 GPU with vLLM, and to manage the whole lifecycle from Claude Code through a Python MCP server. The key to the solution was bringing the MCP server up and validating it locally before the deploy — which is where the move to the MCP SDK 2.x surfaced, and where it cost one import and one class name. The deployment results were:\n🟢 The migrated MCP server registered all 27 tools and the resource unchanged, and drove staging, deploy, validation and benchmarking\n🟢 Gemma 4 E2B deployed to Cloud Run with one\nmake deploy\nand passed the MCP health check in 0.87 seconds\n🟢 Throughput scaled 3.8x from concurrency 1 to 4 with flat latency, peaking at 189.53 tokens/s\n❌ Past 4, Cloud Run's\n--concurrency=4\nis the ceiling — the same L4 on EC2 reached 360.17 tokens/s at 8\n⚠️ One fixed Cloud Run L4 instance lists at $1.4209 an hour, $2.08 per million output tokens at peak\nScope: one Cloud Run instance with one NVIDIA L4 in\nus-east4\n, vLLM\nv0.26.0-cu129\n, Gemma 4 E2B with bf16 weights and fp8 KV cache, manual scaling at one fixed instance during the sweep. One sweep of 20 requests per level, a single fixed prompt, 128 max output tokens, driven over the internet from a laptop. The EC2 comparison is a separate run: vLLM 0.28.0, bf16 KV cache, 1,024-token random prompts, 8 to 32 requests per level, driven on the instance itself, spot-priced in\nus-east-1\n. Costs are arithmetic on list prices, not a bill. MCP server on mcp 2.2.0 and Python 3.14.7.\nThe strategy for using MCP for Gemma 4 GPU deployment to Cloud Run with Claude Code was validated with an incremental step by step approach.\nReferences\ngpu-2B-cloudrun-devops-agent | GitHub\nMigration Guide: v1 to v2 | MCP Python SDK\nGitHub - modelcontextprotocol/python-sdk",
  "metadata": {
    "title": "2B Gemma 4 Deployment with Cloud Run, NVIDIA L4, MCP SDK 2.x, and Claude Code - DEV Community",
    "description": "Step by step deployment of Gemma 4 E2B to a Cloud Run NVIDIA L4 GPU with vLLM, managed by a Python MCP server migrated to the MCP SDK 2.x. Tagged with mcp, gemma, googlecloud, claudecode.",
    "keywords": "mcp, gemma, googlecloud, claudecode, software, coding, development, engineering, inclusive, community",
    "author": null,
    "og:type": "article",
    "og:url": "https://dev.to/gde/2b-gemma-4-deployment-with-cloud-run-nvidia-l4-mcp-sdk-2x-and-claude-code-4ml3",
    "og:title": "2B Gemma 4 Deployment with Cloud Run, NVIDIA L4, MCP SDK 2.x, and Claude Code",
    "og:description": "Step by step deployment of Gemma 4 E2B to a Cloud Run NVIDIA L4 GPU with vLLM, managed by a Python MCP server migrated to the MCP SDK 2.x.",
    "og:site_name": "DEV Community",
    "og:image": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fxbill9%2Fgemma4-dev%2Fmain%2Fgpu-2B-cloudrun-devops-agent%2Fdocs%2Fdeploy%2Fdevto-cover.24212670.jpg",
    "twitter:site": "@thepracticaldev",
    "twitter:creator": "@",
    "twitter:title": "2B Gemma 4 Deployment with Cloud Run, NVIDIA L4, MCP SDK 2.x, and Claude Code",
    "twitter:description": "Step by step deployment of Gemma 4 E2B to a Cloud Run NVIDIA L4 GPU with vLLM, managed by a Python MCP server migrated to the MCP SDK 2.x.",
    "twitter:card": "summary_large_image",
    "twitter:widgets:new-embed-design": "on",
    "twitter:image:src": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fxbill9%2Fgemma4-dev%2Fmain%2Fgpu-2B-cloudrun-devops-agent%2Fdocs%2Fdeploy%2Fdevto-cover.24212670.jpg"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/0c2268fdaf85/code/body-000.txt",
      "chars": 95,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-001.txt",
      "chars": 15,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-002.txt",
      "chars": 18,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-003.txt",
      "chars": 306,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-004.txt",
      "chars": 114,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-005.txt",
      "chars": 52,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-006.txt",
      "chars": 388,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-007.txt",
      "chars": 187,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-008.txt",
      "chars": 235,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-009.txt",
      "chars": 44,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-010.txt",
      "chars": 132,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-011.txt",
      "chars": 10,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-012.txt",
      "chars": 131,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-013.txt",
      "chars": 10,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-014.txt",
      "chars": 98,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-015.txt",
      "chars": 416,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-016.txt",
      "chars": 271,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-017.txt",
      "chars": 425,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-018.txt",
      "chars": 31,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-019.txt",
      "chars": 86,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-020.txt",
      "chars": 971,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-021.txt",
      "chars": 273,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-022.txt",
      "chars": 353,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-023.txt",
      "chars": 247,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-024.txt",
      "chars": 259,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-025.txt",
      "chars": 64,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-026.txt",
      "chars": 12,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-027.txt",
      "chars": 377,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-028.txt",
      "chars": 154,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-029.txt",
      "chars": 130,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-030.txt",
      "chars": 91,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-031.txt",
      "chars": 279,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-032.txt",
      "chars": 169,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-033.txt",
      "chars": 212,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-034.txt",
      "chars": 184,
      "language": "text"
    },
    {
      "file": "attachments/0c2268fdaf85/code/body-035.txt",
      "chars": 13,
      "language": "text"
    }
  ],
  "images": [],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://github.com/xbill9/gemma4-dev/tree/main/gpu-2B-cloudrun-devops-agent",
      "label": "https://github.com/xbill9/gemma4-dev/tree/main/gpu-2B-cloudrun-devops-agent",
      "type": "a"
    },
    {
      "href": "https://py.sdk.modelcontextprotocol.io/v2/migration/",
      "label": "Migration Guide: v1 to v2 | MCP Python SDK",
      "type": "a"
    },
    {
      "href": "https://github.com/modelcontextprotocol/python-sdk",
      "label": "GitHub - modelcontextprotocol/python-sdk",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:48:35Z",
  "source_url": "https://dev.to/gde/2b-gemma-4-deployment-with-cloud-run-nvidia-l4-mcp-sdk-2x-and-claude-code-4ml3",
  "papers": [],
  "actual_video_files": [],
  "body_chars": 21340,
  "expected_body_images": 0,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/0c2268fdaf85/pages/source.html",
  "local_html": "attachments/0c2268fdaf85/pages/article.html",
  "body_base_path": "attachments/0c2268fdaf85/pages/",
  "body_markdown_path": "attachments/0c2268fdaf85/pages/article.md"
}