Deployment10 min read

Deploying MCP Servers to Google Cloud Run: A Complete Guide

Step-by-step guide to deploying MCP servers on Google Cloud Run. Covers containerization, IAM auth, environment secrets via Secret Manager, auto-scaling, and production best practices.

By MyMCPTools Team·

Google Cloud Run is a fully managed container platform that runs your code without you managing servers — and it's an excellent fit for MCP server deployment. Cloud Run scales to zero when idle (no idle cost), scales up instantly under load, and integrates natively with Google Cloud's security, secrets, and observability stack.

If your infrastructure already lives on GCP — BigQuery, Cloud SQL, GCS, Vertex AI — deploying your MCP server to Cloud Run keeps everything in one network perimeter and avoids cross-cloud latency.

Why Cloud Run for MCP Servers

Cloud Run hits a sweet spot for MCP workloads:

  • Scale to zero — Unlike always-on VMs, Cloud Run only costs money when processing requests. Light-use MCP servers (personal, dev team) can run nearly free.
  • No cold start problem for MCP — Unlike AWS Lambda's tight timeout limits, Cloud Run supports minimum instance configuration to keep at least one container warm, eliminating cold starts for production workloads.
  • Native GCP integrations — Secret Manager for credentials, Cloud Logging for structured logs, IAM for access control, and VPC connectors for private Cloud SQL/BigQuery access.
  • HTTP/SSE transport ready — Cloud Run serves HTTP natively, making it perfect for MCP's streamable HTTP and SSE transport modes.

Step 1: Create Your MCP Server

Create a simple MCP server with HTTP transport. Cloud Run expects an HTTP server, so we'll use the SSE or streamable HTTP transport rather than stdio:

// src/server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
import express from 'express'

const app = express()
const PORT = parseInt(process.env.PORT || '8080') // Cloud Run injects PORT

const server = new Server(
  { name: 'my-mcp-server', version: '1.0.0' },
  { capabilities: { tools: {} } }
)

server.setRequestHandler('tools/list', async () => ({
  tools: [
    {
      name: 'hello_world',
      description: 'A simple test tool',
      inputSchema: {
        type: 'object',
        properties: {
          name: { type: 'string', description: 'Name to greet' }
        },
        required: ['name']
      }
    }
  ]
}))

server.setRequestHandler('tools/call', async (request) => {
  if (request.params.name === 'hello_world') {
    const { name } = request.params.arguments as { name: string }
    return {
      content: [{ type: 'text', text: `Hello, ${name}! From Cloud Run.` }]
    }
  }
  throw new Error(`Unknown tool: ${request.params.name}`)
})

// SSE endpoint for MCP clients
app.get('/sse', async (req, res) => {
  const transport = new SSEServerTransport('/messages', res)
  await server.connect(transport)
})

app.post('/messages', express.json(), async (req, res) => {
  // Handle incoming messages from SSE transport
  res.json({ ok: true })
})

// Health check for Cloud Run
app.get('/health', (req, res) => res.json({ status: 'ok' }))

app.listen(PORT, () => {
  console.log(`MCP server running on port ${PORT}`)
})

Step 2: Containerize with Docker

Create a Dockerfile in your project root:

FROM node:20-slim

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY dist/ ./dist/

# Cloud Run runs as non-root by default
USER node

# Cloud Run injects PORT — your app must listen on it
ENV PORT=8080
EXPOSE 8080

CMD ["node", "dist/server.js"]

Build and test locally:

npm run build
docker build -t mcp-server .
docker run -p 8080:8080 mcp-server
# Test: curl http://localhost:8080/health

Step 3: Push to Artifact Registry

Google Cloud Run pulls images from Artifact Registry (Google's container registry):

# Authenticate Docker with GCP
gcloud auth configure-docker us-central1-docker.pkg.dev

# Create a repository (one-time)
gcloud artifacts repositories create mcp-servers   --repository-format=docker   --location=us-central1   --description="MCP server images"

# Tag and push
docker tag mcp-server   us-central1-docker.pkg.dev/YOUR_PROJECT_ID/mcp-servers/my-mcp-server:latest

docker push   us-central1-docker.pkg.dev/YOUR_PROJECT_ID/mcp-servers/my-mcp-server:latest

Step 4: Store Secrets in Secret Manager

Never bake credentials into your container image. Use Google Cloud Secret Manager:

# Create a secret
echo -n "your-api-key-value" | gcloud secrets create MY_API_KEY   --data-file=-

# Grant Cloud Run's service account access
gcloud secrets add-iam-policy-binding MY_API_KEY   --member="serviceAccount:YOUR_PROJECT_NUMBER-compute@developer.gserviceaccount.com"   --role="roles/secretmanager.secretAccessor"

Your MCP server reads the secret at runtime via the Secret Manager API or by mounting it as an environment variable in the Cloud Run configuration.

Step 5: Deploy to Cloud Run

gcloud run deploy my-mcp-server   --image us-central1-docker.pkg.dev/YOUR_PROJECT_ID/mcp-servers/my-mcp-server:latest   --region us-central1   --platform managed   --port 8080   --memory 512Mi   --cpu 1   --min-instances 1   --max-instances 10   --set-secrets "MY_API_KEY=MY_API_KEY:latest"   --no-allow-unauthenticated

Key flags explained:

  • --min-instances 1 — Keep one instance warm to eliminate cold starts. Set to 0 for dev/staging to save cost.
  • --no-allow-unauthenticated — Require Google IAM authentication. Remove this only if you implement your own auth in the MCP server.
  • --set-secrets — Mounts the secret as an environment variable in the container.

Step 6: Configure IAM Authentication

With --no-allow-unauthenticated, callers need a valid Google identity token. Generate one for testing:

# Get the service URL
gcloud run services describe my-mcp-server   --region us-central1   --format 'value(status.url)'

# Get an identity token for testing
TOKEN=$(gcloud auth print-identity-token)

# Test the health endpoint
curl -H "Authorization: Bearer $TOKEN"   https://MY-SERVICE-URL.run.app/health

For MCP clients connecting to your server, you'll need to configure them to include the identity token as a bearer token in the Authorization header. Alternatively, grant specific service accounts the roles/run.invoker role for server-to-server authentication.

Step 7: Connect Private GCP Services

If your MCP server needs to access Cloud SQL, Cloud Memorystore, or other private GCP resources, connect to your VPC:

gcloud run services update my-mcp-server   --region us-central1   --vpc-connector my-vpc-connector   --vpc-egress all-traffic

This routes all egress from your MCP server through your VPC, giving it access to private Cloud SQL instances via their internal IP addresses without exposing them to the public internet.

Monitoring and Logging

Cloud Run automatically forwards stdout/stderr to Cloud Logging. Use structured JSON logging to make queries easier:

// Structured logging for Cloud Run
function log(severity: 'INFO' | 'WARNING' | 'ERROR', message: string, data?: object) {
  console.log(JSON.stringify({
    severity,
    message,
    timestamp: new Date().toISOString(),
    ...data
  }))
}

View logs in the Google Cloud Console under Cloud Run → your service → Logs, or query with:

gcloud logging read   'resource.type="cloud_run_revision" AND resource.labels.service_name="my-mcp-server"'   --limit 50   --format json

CI/CD with Cloud Build

Automate deployments from GitHub with Cloud Build:

# cloudbuild.yaml
steps:
  - name: 'node:20'
    entrypoint: 'npm'
    args: ['ci']
  - name: 'node:20'
    entrypoint: 'npm'
    args: ['run', 'build']
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '-t', 'us-central1-docker.pkg.dev/$PROJECT_ID/mcp-servers/my-mcp-server:$COMMIT_SHA', '.']
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', 'us-central1-docker.pkg.dev/$PROJECT_ID/mcp-servers/my-mcp-server:$COMMIT_SHA']
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
    args:
      - 'run'
      - 'deploy'
      - 'my-mcp-server'
      - '--image'
      - 'us-central1-docker.pkg.dev/$PROJECT_ID/mcp-servers/my-mcp-server:$COMMIT_SHA'
      - '--region'
      - 'us-central1'

Cost Optimization

Cloud Run's per-request pricing makes it economical for MCP workloads:

  • Dev/personal servers: Set --min-instances 0 and accept cold starts. Cost is near-zero for low traffic.
  • Team servers: Use --min-instances 1 to eliminate cold starts. The cost of one always-warm instance is ~$5-15/month at 1 vCPU/512MB.
  • Production: Set min instances based on your p95 concurrency. Cloud Run's automatic scaling handles burst traffic.

Browse the MCP server directory for servers to deploy, and check our guides for other platforms: AWS Lambda, Cloudflare Workers, and Railway.

Recommended Tools

Better Stack

Free Plan

Get alerted when your APIs, browser tests, payment pipelines, or MCP server dependencies go down. Used by 100K+ developers.

Start monitoring free →

1Password

14-day Free Trial

Store and inject API keys, payment credentials, tokens, and file access secrets into your MCP server configs. Trusted by 150K+ developers.

Try 1Password free →

🔧 MCP Servers Mentioned in This Article

☁️

Google Cloud MCP Server

Google Cloud's official MCP server enables AI agents (like Claude Desktop, Cursor, Gemini CLI, and Windsurf) to securely interact with GCP services and deploy applications directly to Cloud Run. It exposes essential deployment and observability tools: `deploy-file-contents` (deploys files directly), `list-services`, `get-service`, and `get-service-log`. When running locally, it additionally provides `deploy-local-folder`, `list-projects`, and `create-project` (which creates a new GCP project and attaches it to the first available billing account). The server supports natural language prompts like 'deploy' and 'logs' for faster workflows. It respects standard Google Cloud SDK authentication (`gcloud auth login`) and can be configured via environment variables like `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_REGION`, and `DEFAULT_SERVICE_NAME`. It also includes an anti-DNS-rebinding security feature (`ENABLE_HOST_VALIDATION`).

Local
🔧

Docker MCP Server

The Docker MCP server (ckreiling/mcp-server-docker) gives an AI assistant direct control of a Docker daemon over the Model Context Protocol: containers, images, networks and volumes, as tools rather than shell commands. It is the community server most people mean by "Docker MCP" — distinct from Docker’s own Docker MCP Gateway, which does not manage your containers at all but runs *other* MCP servers inside containers. If you want to ask Claude why the postgres container keeps restarting, you want this one; if you want a single secure endpoint in front of twenty catalog servers, you want the gateway. The tool surface is explicit and small enough to reason about: list_containers, create_container, run_container, recreate_container, start_container, fetch_container_logs, stop_container and remove_container for containers; list_images, pull_image, push_image, build_image and remove_image for images; list_networks / create_network / remove_network and list_volumes / create_volume / remove_volume for the rest. Two resource templates, docker://containers/{id}/logs and docker://containers/{id}/stats, let a client read logs and live stats by container ID or name without a tool call. It also ships a docker_compose prompt that puts the model into a plan-then-apply loop — you describe the containers you want under a project name, the model proposes a concise plan, and nothing runs until you approve it; reopening the prompt with the same project name re-reads the state of everything created under it, which is how you clean up after a lost chat. It runs on the Python Docker SDK’s from_env, so DOCKER_HOST applies: set ssh://user@host and the same server administers a remote engine. Two limits are deliberate and stated by the project — privileged options like --privileged and --cap-add/--cap-drop are not supported, and container configuration passes through the model, so no secrets belong in it.

Local📘
🔧

Kubernetes MCP Server

The Kubernetes MCP server (mcp-server-kubernetes, built by Flux159) brings cluster management capabilities into AI assistant workflows, letting developers and platform engineers query and manage Kubernetes resources through natural-language interactions with Claude, Cursor, and other MCP-compatible clients. It loads your existing kubeconfig automatically, so it works with any cluster — local minikube and kind setups, Amazon EKS, Google GKE, Azure AKS, or on-premises deployments — with no separate credential setup required. Core tools exposed by the server include: listing pods, deployments, services, and namespaces; describing individual resources and their status; fetching pod logs for debugging; applying and updating manifests; scaling deployments; checking rollout status and history; and querying resource utilization and cluster events. A built-in non-destructive mode can disable delete/scale-down operations entirely, making it safe to point at production clusters for read-only diagnostics. DevOps engineers use it to debug failing deployments by asking Claude to inspect pod logs and recent events, identify resource constraints causing OOMKilled pods, or summarize the current state of a namespace before a production release. For SREs responding to incidents, it enables rapid triage through conversational commands — no memorizing kubectl flags or switching terminal windows mid-incident — and optional OpenTelemetry integration adds observability into what the AI agent actually did against the cluster. Install with: `npx mcp-server-kubernetes`. Pairs well with the GitHub MCP server for full GitOps review workflows.

Local
☁️

Google Cloud Storage

Access and manage Google Cloud Storage buckets and objects. Transfer files, configure IAM permissions, set retention policies, and analyze storage usage.

Local
💻

GitHub MCP Server

authenticated access to the whole GitHub platform — repositories, files, branches, issues, pull requests, Actions runs, security alerts, discussions and notifications — from Claude, Cursor, VS Code, Copilot CLI and any other MCP host. There is no npm package for this server, and that trips up most people who try to install it: `@github/mcp-server` is not published to the npm registry, so any `npx` line you find for it will fail. GitHub ships it three other ways. The easiest is the hosted remote server at https://api.githubcopilot.com/mcp/, which needs no install at all — point an HTTP-transport MCP client at that URL and log in with OAuth (VS Code 1.101+, Claude Desktop, Claude Code, Cursor and Windsurf all support this). The second is the official Docker image ghcr.io/github/github-mcp-server, which is what the copy-paste command on this page runs; on github.com it now performs a browser-based OAuth login on first use and keeps the token in memory only, which is why the published Docker configs map a fixed loopback callback port (-p 127.0.0.1:8085:8085 with GITHUB_OAUTH_CALLBACK_PORT=8085) so the container can receive the callback. Prefer a token? Set GITHUB_PERSONAL_ACCESS_TOKEN instead — it takes precedence over OAuth, and the minimum useful scopes are repo, read:org and read:packages. The third is the native Go binary from the repository's releases, which needs no fixed port for the OAuth flow. GitHub Enterprise Server has no hosted option: use the local server with --gh-host or GITHUB_HOST set to your instance (include the https:// scheme — it defaults to http://, which GHES rejects). Toolsets can be narrowed with GITHUB_TOOLSETS, and an insiders channel is available at /mcp/insiders or via the X-MCP-Insiders header.

Auth required📘

📚 More from the Blog