Back to blog
Deployment • 9 min read

Deploy an MCP Server to Google Cloud Run

Package a generated MCP server as a container or buildpack app, bind it to Cloud Run's port and host, wire secrets from Secret Manager, and scale to zero.

Google Cloud Run runs containers on demand and scales to zero when nobody is calling. That makes it a good fit for an MCP server that is used in bursts: you get an HTTPS endpoint, standard container logs, and no cluster to manage.

The important detail is that Cloud Run injects a PORT environment variable and expects the process to listen on 0.0.0.0. Generated bundles default to localhost and a fixed port, so each language needs a small adjustment before deployment.

Choose a build path

  • Buildpacks: TypeScript and Python deploy from source with no Dockerfile.
  • Dockerfile: PHP and Java use a small container definition. This path also works for TypeScript and Python if you need full control.

Install the Google Cloud CLI, authenticate, and select a project before starting.

terminal
gcloud auth login
gcloud config set project YOUR_PROJECT_ID

The first source deploy may prompt you to enable Cloud Build and Artifact Registry. Accept the prompts and let the build finish.

TypeScript with buildpacks

Open the generated bundle and make two changes. First, let the entry point read PORT and bind to all interfaces in src/server.ts.

src/server.ts
// Before
port: { type: 'string', default: '3000' },
serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => {

// After
port: { type: 'string', default: process.env.PORT ?? '3000' },
serve({ fetch: app.fetch, port, hostname: '0.0.0.0' }, () => {

Second, make npm start launch the HTTP transport, because server.ts defaults to stdio. Keep the build script unchanged; the Node buildpack runs it automatically.

package.json
{
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js --http",
    "dev": "tsx watch src/server.ts"
  }
}

Deploy from the bundle root. Cloud Run builds the app, runs the build script, and starts it with npm start.

terminal
gcloud run deploy my-mcp-server \
  --source . \
  --region us-central1 \
  --allow-unauthenticated

Python with buildpacks

The generated Python server accepts a host and port, so a Procfile is enough. Add it at the bundle root next to requirements.txt.

Procfile
web: python server.py --transport streamable-http --host 0.0.0.0 --port $PORT

Cloud Run supplies $PORT when it starts the process. Deploy the same way:

terminal
gcloud run deploy my-mcp-server \
  --source . \
  --region us-central1 \
  --allow-unauthenticated

PHP with a Dockerfile

The generated PHP bundle is served by PHP's built-in web server, which accepts a bind address and port. Build the dependencies in a Composer stage, then copy the application into a CLI image.

Dockerfile
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json ./
RUN composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader

FROM php:8.3-cli
WORKDIR /app
COPY . .
COPY --from=vendor /app/vendor ./vendor
RUN mkdir -p sessions
CMD ["sh", "-c", "php -S 0.0.0.0:${PORT:-8080} server.php"]

Add a .dockerignore so local artifacts do not end up in the image, then deploy from the bundle root.

.dockerignore
.git
vendor
sessions
dev.log
terminal
gcloud run deploy my-mcp-server \
  --source . \
  --region us-central1 \
  --allow-unauthenticated

PHP's built-in server is single-threaded. It is fine for development and low traffic, but use PHP-FPM behind a production web server if you expect concurrent load.

Java with a Dockerfile

The generated Java server embeds Tomcat and binds to all interfaces. Build the classes and copy the dependency set, then run Server with the HTTP transport and the injected port.

Dockerfile
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml ./
RUN mvn -q -DskipTests dependency:go-offline
COPY src ./src
RUN mvn -q -DskipTests package dependency:copy-dependencies

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/classes ./classes
COPY --from=build /app/target/dependency ./dependency
CMD ["sh", "-c", "java -cp 'classes:dependency/*' generated.mcp.Server --http --port ${PORT:-8080}"]
terminal
gcloud run deploy my-mcp-server \
  --source . \
  --region us-central1 \
  --allow-unauthenticated

Add credentials

Every generated bundle reads upstream credentials from environment variables. Put tokens in Secret Manager instead of passing them on the command line.

terminal
printf '%s' "$API_TOKEN" | gcloud secrets create mcp-api-token --data-file=-

gcloud run deploy my-mcp-server \
  --source . \
  --region us-central1 \
  --allow-unauthenticated \
  --set-secrets MCP_AUTH_API_TOKEN=mcp-api-token:latest

Use the exact variable names from the bundle README. They are prefixed with MCP_AUTH_ and derived from your security schemes. The Cloud Run service account needs permission to read the secret.

Verify the deployment

After the deploy finishes, Cloud Run prints the service URL. The MCP endpoint is that URL plus /mcp.

https://my-mcp-server-abc123-uc.a.run.app/mcp

Connect an MCP client that supports Streamable HTTP to that address, then call a read-only tool. If the connection fails, inspect the container logs; startup errors and missing environment variables appear there.

terminal
gcloud run services logs read my-mcp-server --region us-central1 --limit 50

Operational notes

  • Scale to zero. The default minimum instance count is zero, so the first request after idle pays a cold start. Use --min-instances 1 when latency matters more than cost.
  • Streaming and timeouts. Streamable HTTP can hold a response open. Cloud Run has a maximum request timeout and a concurrency limit per instance; tune --timeout and --concurrency for your client behavior.
  • Session affinity. The PHP bundle stores sessions on the local filesystem, and any in-memory state disappears when an instance stops. Enable session affinity for sticky routing, or move state to an external store. For a quick test, cap the service at one instance.
  • Public access. --allow-unauthenticated makes the endpoint reachable by any MCP client, but it also makes it public. Upstream API credentials are protected; the MCP endpoint itself is not. Add authentication or put it behind an identity-aware proxy before sharing it.
  • Cost control. Set --max-instances to cap scale-out, especially for long-lived streams.

Next steps

Cloud Run gives you the most language coverage of the options covered so far, at the cost of a container or buildpack adjustment per runtime. Compare it with Cloudflare Workers, Vercel, AWS App Runner, Render, and Railway, or start from From OpenAPI to a Running MCP Server if you still need a bundle. A side-by-side comparison of all deployment options is available in Where to Deploy Your MCP Server.

Need a bundle to deploy?

Import a spec and generate an MCP server in TypeScript, Python, Java, or PHP.

Upload Spec