Deploy Your MCP Server to Cloud Run
Deploy a Node.js MCP server to Cloud Run and connect it to Gemini Enterprise.
Introduction
30 Second Summary
AI assistants are most powerful when they can reach your custom tools and data. But a server running on your laptop is only useful to you.
In this project, you will deploy your Node.js MCP server to Google Cloud Run and register it with Gemini Enterprise so that Gemini can discover and invoke your custom tools over the internet.
What You'll Build
A production-ready MCP server deployment on Cloud Run that Gemini Enterprise can securely access and invoke tools from.
By the end of this project, you'll have:
- A StreamableHTTP-compatible MCP server entry point ready for remote clients.
- A Dockerfile and deployed Cloud Run service with IAM-based authentication.
- A registered Custom MCP Server data store in Gemini Enterprise with Google Workspace OAuth.
- Secret Mission: Connect your deployed MCP server to Gemini CLI for local testing and development workflows.
Are there any prerequisites?
You need an existing Node.js MCP server built with @modelcontextprotocol/sdk, a GCP account with billing enabled, and the gcloud CLI installed and authenticated.
Before We Start
Before We Start...
Verify Your Environment
Before deploying your MCP server to the cloud, you need to confirm that your local environment is properly configured.
This means checking that your gcloud CLI is authenticated, the right GCP APIs are enabled, and your existing MCP server can start locally with a compatible SDK version.
In this step, get ready to:
- Confirm your gcloud CLI authentication and GCP project configuration.
- Enable the Cloud Run, Artifact Registry, and Cloud Build APIs.
- Verify your Node.js version and MCP SDK compatibility.
Confirm gcloud authentication and project
Your gcloud CLI needs an active authenticated account and a project set before you can deploy anything to Cloud Run.
- Check which account is active by running this command:
gcloud auth list
You should see your email address listed with (active) next to it.
- Confirm your GCP project is set by running:
gcloud config get-value project
You should see your project ID printed in the terminal.
No active account or no project set?
If gcloud auth list shows no accounts, run gcloud auth login to authenticate. This opens a browser window where you sign in with your Google account.
If gcloud config get-value project returns empty or the wrong project, set it with gcloud config set project YOUR_PROJECT_ID.
Help me fix my gcloud authentication
Enable the required GCP APIs
Cloud Run deployment requires three APIs to be enabled on your project. These handle container storage, the deployment service itself, and the build pipeline.
- Enable all three APIs by running this command:
gcloud services enable run.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com
What do these APIs do?
- run.googleapis.com is the Cloud Run Admin API. It lets you deploy and manage serverless containers.
- artifactregistry.googleapis.com is Artifact Registry. It stores the container image that Cloud Run will run.
- cloudbuild.googleapis.com is Cloud Build. It builds your container image from source code in the cloud.
The command may take a minute to complete. You should see a message ending with Operation finished successfully. for each API.
Seeing a permission denied error?
You need the Owner or Editor role on your GCP project to enable APIs. If you're using an organization-managed project, ask your admin to grant you the Service Usage Admin role.
Help me fix my API enablement permissions
Verify Node.js and your MCP SDK version
Your MCP server needs Node.js 18+ to run on Cloud Run, and @modelcontextprotocol/sdk version 1.6.0 or later for StreamableHTTP transport support.
- Check your Node.js version by running:
node --version
You should see v18 or higher (e.g. v20.x.x).
- Navigate to your MCP server project directory and check the installed SDK version by running:
npm list @modelcontextprotocol/sdk
Confirm the version shown is 1.6.0 or later. StreamableHTTP transport support was added in v1.6.0.
- Start your existing MCP server locally to confirm it still runs:
npm start
Your server should start without errors. Stop it with Ctrl+C once you've confirmed it works.
SDK version too old?
If your @modelcontextprotocol/sdk version is below 1.6.0, upgrade it by running npm install @modelcontextprotocol/sdk@latest in your project directory. The latest stable version is 1.29.0.
Help me upgrade my MCP SDK
Your environment is verified and ready. Next up, you'll adapt your MCP server to use StreamableHTTP transport so it can communicate with remote clients over HTTPS.
Adapt Your Server for StreamableHTTP Transport
Your MCP server currently communicates over stdio, which works for local tools like VS Code or Claude Desktop. Cloud Run needs an HTTP endpoint that clients can reach over the network.
The MCP protocol defines a transport called StreamableHTTP that wraps your server's tools behind a single /mcp HTTP endpoint. In this step, you will install Express, create a new entry point file that exposes your server over StreamableHTTP, and test it locally.
In this step, get ready to:
- Install Express as an HTTP server dependency.
- Create a StreamableHTTP entry point file for Cloud Run.
- Test the new HTTP endpoint locally with a JSON-RPC initialize request.
Install Express
The @modelcontextprotocol/sdk v1.x provides a helper called createMcpExpressApp that sets up an Express app with sensible defaults for MCP servers. You need Express installed as a dependency for this helper to work.
- Install Express by running this command:
npm install express
- Confirm the install succeeded by checking your package.json.
- You should see express listed under dependencies.
Seeing permission errors?
Make sure you are running the command inside your project folder where package.json lives. If you see EACCES errors on macOS, avoid using sudo and instead fix npm permissions with a version manager like nvm.
Still stuck? Help me fix npm install permission errors.
Create the StreamableHTTP entry point
Gemini Enterprise only supports StreamableHTTP transport for Custom MCP Server data stores. SSE transport is deprecated in favor of StreamableHTTP in the SDK.
Why stateless mode?
Cloud Run instances are ephemeral and scale horizontally. Setting sessionIdGenerator: undefined disables session tracking so each request is independent.
This means any Cloud Run instance can handle any request without needing shared session state. It is the simplest deployment model with minimal infrastructure requirements.
- Create a new file called server-http.ts in your project root.
- Add the imports and server instance by pasting this code into server-http.ts:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
import { Request, Response } from 'express';
// Create your MCP server instance
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
// ============================================================
// Register your tools here (adapt from your existing server)
// ============================================================
// Example:
// server.registerTool('my-tool', {
// description: 'Description of my tool',
// inputSchema: { param: z.string() }
// }, async ({ param }) => {
// return { content: [{ type: 'text', text: `Result: ${param}` }] };
// });
// Create Express app with MCP defaults
const app = createMcpExpressApp({ host: '0.0.0.0' });
const PORT = parseInt(process.env.PORT || '3000', 10);
What does this code do?
McpServer is your MCP server instance that holds tool registrations. You will adapt your existing tool registrations into this instance where the comment block indicates.
createMcpExpressApp creates an Express app with DNS rebinding protection disabled (host: '0.0.0.0'), which is required for Cloud Run deployment. The PORT reads from the environment variable that Cloud Run sets automatically.
- Add the POST handler below the code you just pasted in server-http.ts:
// Handle POST requests to /mcp (stateless mode for Cloud Run)
app.post('/mcp', async (req: Request, res: Response) => {
try {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: { code: -32603, message: 'Internal server error' },
id: null,
});
}
}
});
What does this code do?
Each incoming POST to /mcp creates a fresh StreamableHTTPServerTransport with sessionIdGenerator: undefined for stateless operation.
The transport connects to your server instance and processes the JSON-RPC request body. If anything goes wrong, the catch block returns a standard JSON-RPC error response.
- Add the GET and DELETE handlers, plus the listen call, below the POST handler in server-http.ts:
// Handle GET requests (for SSE stream establishment)
app.get('/mcp', async (req: Request, res: Response) => {
res.status(405).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Method not allowed in stateless mode' },
id: null,
});
});
// Handle DELETE requests (session termination)
app.delete('/mcp', async (req: Request, res: Response) => {
res.status(405).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Method not allowed in stateless mode' },
id: null,
});
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`MCP StreamableHTTP Server listening on port ${PORT}`);
});
What does this code do?
GET and DELETE handlers return 405 (Method Not Allowed) because stateless mode does not support SSE streaming or session termination. These endpoints exist so clients get a clear error rather than a generic 404.
app.listen starts the server on all network interfaces (0.0.0.0) at the configured port. Cloud Run requires binding to 0.0.0.0 so it can route traffic to your container.
- Save server-http.ts.
✔️ Awesome, I've got everything!
Great. Double check you have saved server-http.ts with all three sections (imports/setup, POST handler, GET/DELETE/listen).
ⓧ I'd like to double check the full code
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
import { Request, Response } from 'express';
// Create your MCP server instance
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
// ============================================================
// Register your tools here (adapt from your existing server)
// ============================================================
// Example:
// server.registerTool('my-tool', {
// description: 'Description of my tool',
// inputSchema: { param: z.string() }
// }, async ({ param }) => {
// return { content: [{ type: 'text', text: `Result: ${param}` }] };
// });
// Create Express app with MCP defaults
const app = createMcpExpressApp({ host: '0.0.0.0' });
const PORT = parseInt(process.env.PORT || '3000', 10);
// Handle POST requests to /mcp (stateless mode for Cloud Run)
app.post('/mcp', async (req: Request, res: Response) => {
try {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: { code: -32603, message: 'Internal server error' },
id: null,
});
}
}
});
// Handle GET requests (for SSE stream establishment)
app.get('/mcp', async (req: Request, res: Response) => {
res.status(405).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Method not allowed in stateless mode' },
id: null,
});
});
// Handle DELETE requests (session termination)
app.delete('/mcp', async (req: Request, res: Response) => {
res.status(405).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Method not allowed in stateless mode' },
id: null,
});
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`MCP StreamableHTTP Server listening on port ${PORT}`);
});
Test your StreamableHTTP server locally
Before deploying to Cloud Run, confirm the new entry point responds to MCP protocol messages locally.
- Start your StreamableHTTP server by running:
npx ts-node --esm server-http.ts
- You should see MCP StreamableHTTP Server listening on port 3000 in your terminal.
Server won't start?
If you see a module resolution error, make sure your tsconfig.json has "module": "ESNext" and "moduleResolution": "node" (or "bundler"). The .js extensions in the import paths are intentional for ESM resolution.
If port 3000 is already in use, stop the other process or set a different port with PORT=3001 npx ts-node --esm server-http.ts.
Still stuck? Help me debug why my MCP StreamableHTTP server won't start.
- Open a second terminal window and send a test initialize request by running:
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
}
}
}'
What does this request do?
This is the MCP protocol's handshake message. The client announces its protocol version and capabilities, and the server responds with its own capabilities and server info.
The Accept header includes both application/json and text/event-stream as required by the MCP StreamableHTTP specification.
- You should see a JSON response containing protocolVersion, serverInfo, and capabilities fields. This confirms your server correctly handles MCP initialization over HTTP.
Getting connection refused or an empty response?
Make sure your server is still running in the first terminal window. Check that you are posting to port 3000 (or whichever port the server printed on startup).
If you get a 500 error, check the server terminal for the logged error message. A common cause is a missing express install or an incompatible @modelcontextprotocol/sdk version below 1.6.0.
Need help? Help me debug my MCP server's initialize response.
Your MCP server now speaks StreamableHTTP over a single /mcp endpoint. Next up, you will containerize this server and deploy it to Cloud Run.
Containerize and Deploy to Cloud Run
Your MCP server works locally with StreamableHTTP transport. The next step is getting it live on the internet so that Gemini Enterprise can reach it.
In this step, you will package your server into a Docker container and deploy it to Cloud Run. Cloud Run handles HTTPS, autoscaling, and scales to zero when idle, so you pay nothing between requests.
In this step, get ready to:
- Create a Dockerfile and .dockerignore for your MCP server.
- Deploy the containerized server to Cloud Run with authentication enforced.
- Grant your account the Cloud Run Invoker role to access the service.
Create a Dockerfile for Cloud Run
Cloud Run needs a container image to run your server. A Dockerfile describes how to build that image from your project files.
- Create a file called Dockerfile in the root of your project folder (the same directory as server-http.ts) and paste in the following:
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ENV PORT=3000
EXPOSE 3000
CMD ["node", "--loader", "ts-node/esm", "server-http.ts"]
What does this Dockerfile do?
- FROM node:20-slim starts from a lightweight Node.js 20 image.
- COPY package*.json and RUN npm ci --only=production install only production dependencies, keeping the image small.
- ENV PORT=3000 sets the port that Cloud Run will inject as the PORT environment variable.
- CMD runs your server with the ts-node/esm loader so TypeScript executes directly without a separate compile step.
- Save Dockerfile.
Create a .dockerignore file
A .dockerignore file tells Docker which files to exclude from the build context. This keeps the image lean and avoids leaking secrets.
- Create a file called .dockerignore in the same directory as your Dockerfile and paste in the following:
node_modules
npm-debug.log
.git
.gitignore
README.md
*.test.ts
*.spec.ts
.env
Why exclude these files?
The node_modules directory is rebuilt inside the container by npm ci. Test files, git history, and .env secrets should never ship in a production image.
- Save .dockerignore.
- Confirm both files exist by running:
ls Dockerfile .dockerignore
You should see both filenames listed in the output.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files.
ⓧ I'd like to double check the full code
Here is the complete Dockerfile:
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ENV PORT=3000
EXPOSE 3000
CMD ["node", "--loader", "ts-node/esm", "server-http.ts"]
And the complete .dockerignore:
node_modules
npm-debug.log
.git
.gitignore
README.md
*.test.ts
*.spec.ts
.env
Deploy to Cloud Run from source
With your Dockerfile ready, you can deploy directly from your project directory. Cloud Build will build the container image remotely and push it to Artifact Registry, then Cloud Run will start serving it.
- Deploy your MCP server to Cloud Run by running this command:
gcloud run deploy mcp-server --no-allow-unauthenticated --region=us-central1 --source .
What does this command do?
- mcp-server is the name of your Cloud Run service.
- --no-allow-unauthenticated ensures only authenticated callers can invoke your server. Public access is blocked.
- --region=us-central1 deploys to the us-central1 region.
- --source . tells Cloud Build to upload your local directory, build the container using your Dockerfile, and deploy the resulting image.
Cloud Build may ask to create a repository
If this is your first source-based deploy in us-central1, you will be prompted to create an Artifact Registry repository named cloud-run-source-deploy. Type y and press Enter to confirm.
The deploy takes 2-4 minutes. On success, the terminal displays a Service URL line with your HTTPS endpoint.
- Copy the Service URL from the terminal output and save it somewhere. It will look like https://mcp-server-XXXXXXXXXX-uc.a.run.app.
Deploy failing with a build error?
- Make sure ts-node is listed in your package.json dependencies (not just devDependencies). The Dockerfile installs production deps only.
- Check that your Dockerfile is named exactly Dockerfile (capital D, no extension). A misspelled filename makes Cloud Build fall back to buildpacks.
- Verify the APIs are enabled by re-running gcloud services enable run.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com.
Still stuck? Help me debug my Cloud Run deploy
Grant the Cloud Run Invoker role to your account
Your service is deployed with authentication enforced. To call it yourself, your Google account needs the roles/run.invoker IAM role on this service.
- Grant your account the Invoker role by running this command:
gcloud run services add-iam-policy-binding mcp-server --region=us-central1 --member="user:[[YOUR_EMAIL="your-email@example.com"]]" --role="roles/run.invoker"
What does this command do?
This grants the Cloud Run Invoker role to your specific Google account on the mcp-server service. Without this role, even authenticated requests from your account would be rejected with a 403 Forbidden response.
- You should see the updated IAM policy printed in the terminal, confirming the binding was added.
Permission denied when running the IAM command?
- Make sure the email you pass matches the account listed in gcloud auth list exactly.
- You need the roles/run.admin or project Owner role to modify IAM bindings on a Cloud Run service.
Need help? Help me fix a permission denied error when adding IAM policy bindings
Your MCP server is now live on Cloud Run with a secure HTTPS endpoint. Next up, you will test the deployed server by tunneling requests through the Cloud Run proxy.
Test Your Deployed Server
Your MCP server is live on Cloud Run, but it's locked down with IAM authentication. You can't just hit the HTTPS URL directly from your terminal without attaching the right identity token.
The Cloud Run proxy solves this by running a local server that automatically injects your credentials into every request it forwards. In this step, you'll start the proxy, then send MCP protocol requests through it to prove your deployed server responds correctly.
In this step, get ready to:
- Start the Cloud Run proxy for authenticated local access.
- Send a tools/list request to verify your tools are discoverable.
- Invoke one of your tools to confirm end-to-end functionality.
Start the Cloud Run proxy
Your Cloud Run service requires authentication for every request. The gcloud proxy creates a tunnel from your local machine to the remote service, automatically attaching your identity token to each forwarded request.
- Start the authenticated proxy by running this command:
gcloud run services proxy mcp-server --region=us-central1
What does this command do?
This starts a local server on port 8080 that forwards every request to your remote mcp-server Cloud Run service.
The proxy injects the identity token of your active gcloud account into each request. This means you can use curl against http://127.0.0.1:8080 without manually managing tokens.
- You should see output indicating the proxy is listening and forwarding to your service.
- Keep this terminal window running. Open a new terminal tab or window for the next substep.
Seeing a permission denied error?
- Confirm you granted yourself roles/run.invoker in the previous step. IAM changes can take up to 60 seconds to propagate.
- Run gcloud auth list to confirm the active account matches the one you granted the invoker role to.
Why can't I connect to my Cloud Run proxy?
Test with a tools/list request
The MCP protocol uses JSON-RPC 2.0 messages over HTTP. A tools/list request asks the server to return all registered tools, proving the deployment is alive and your tool registrations survived containerization.
- In your new terminal tab, send a tools/list request through the proxy by running:
curl -X POST http://127.0.0.1:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
What does this request do?
- Content-Type: application/json tells the server the body is JSON.
- Accept: application/json, text/event-stream is required by the MCP StreamableHTTP specification. It tells the server the client can handle both a direct JSON response and a streaming SSE response.
- The body is a standard JSON-RPC 2.0 request with method tools/list which asks the MCP server to enumerate its available tools.
- You should see a JSON response containing a result object with a tools array listing all the tools you registered on your MCP server.
Getting an empty tools array or an error?
- If tools is an empty array, check that your server-http.ts actually registers tools. The example code in the Complete Code has the tool registration commented out. Make sure you adapted it with your own tool registrations.
- If you see Connection refused, confirm the proxy is still running in your other terminal tab.
- If you get an HTTP 403, the proxy may not be attaching credentials. Stop and restart it with the same command.
Why is my tools/list returning unexpected results?
Test a tool invocation
Listing tools proves the server is reachable. Invoking a tool proves the full request-processing pipeline works end-to-end, from JSON-RPC parsing through to your tool's logic and response.
- Call one of your tools through the proxy by running this command (replace the tool name and arguments with one of the tools from your tools/list response):
curl -X POST http://127.0.0.1:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "[[TOOL_NAME="your-tool-name from tools/list response"]]", "arguments": {}}}'
What does this request do?
- The tools/call method tells the MCP server to execute a specific tool.
- The params.name field must match one of the tool names returned by your tools/list response.
- The params.arguments object passes input to your tool. Adjust it to match the input schema your tool expects.
- You should see a JSON response with a result object containing a content array with your tool's output.
Tool call returning an error?
- Double-check that the name field matches exactly what tools/list returned. Tool names are case-sensitive.
- If your tool requires arguments, add them to the arguments object matching your tool's input schema.
- Check the Cloud Run logs for server-side errors by running gcloud run services logs read mcp-server --region=us-central1 in another terminal.
Why is my MCP tool call failing?
Your MCP server is deployed, authenticated, and responding to real protocol requests. Next up, you'll register it in Gemini Enterprise so that Gemini's AI assistant can discover and invoke your tools on behalf of users.
Register in Gemini Enterprise as a Custom MCP Server
Your MCP server is deployed, authenticated, and verified on Cloud Run. Now it's time to give Gemini Enterprise access to discover and invoke your tools.
Gemini Enterprise connects to remote MCP servers through a Custom MCP Server data store. You'll set up OAuth 2.0 credentials for authentication, adjust an organization policy, and register the data store pointing to your Cloud Run URL.
In this step, get ready to:
- Create OAuth 2.0 credentials for Gemini Enterprise authentication.
- Override the organization policy blocking Custom MCP Server connectors.
- Register your Cloud Run service as a Custom MCP Server data store in Gemini Enterprise.
Set up OAuth credentials for Gemini Enterprise
Gemini Enterprise authenticates requests to your MCP server using OAuth 2.0. You need to create an OAuth Client ID in Google Cloud Console so Gemini can identify itself when calling your server.
- In Google Cloud Console, click the navigation menu (hamburger icon in the top left).
- Navigate to APIs & Services, then click Credentials.
- Click Create Credentials at the top of the page.
- Select OAuth client ID from the dropdown menu.
OAuth consent screen required
If you haven't configured an OAuth consent screen for this project before, Google will prompt you to set one up first. Follow the prompts to configure a basic consent screen (Internal is fine for testing), then return to the Credentials page to continue.
- In the Application type dropdown, select Web application.
- Enter a name for your OAuth client (e.g. MCP Server Client).
- Under Authorized redirect URIs, click Add URI.
- Enter the following redirect URI:
https://vertexaisearch.cloud.google.com/oauth-redirect
- Click Create.
- A dialog appears showing your Client ID and Client Secret. Copy both values and save them somewhere safe. You'll need them shortly.
Why this specific redirect URI?
The redirect URI https://vertexaisearch.cloud.google.com/oauth-redirect is the fixed endpoint that Gemini Enterprise uses to receive OAuth authorization responses. This is required by Google's Custom MCP Server connector.
Don't see the Web application option?
Make sure you've configured an OAuth consent screen first. Google Cloud requires this before you can create OAuth client IDs.
help me set up the OAuth consent screen in Google Cloud Console
Override the organization policy for Custom MCP Server
By default, GCP organizations block the Custom MCP Server connector. You need to override this policy at the project level before you can create a Custom MCP data store.
What role do I need?
Overriding organization policies requires the Organization Policy Administrator role (roles/orgpolicy.policyAdmin). If you don't have this role, ask your organization admin to grant it or to override the policy for you.
- In Google Cloud Console, navigate to the Organization policies page. You can find this by searching for "Organization policies" in the console search bar.
- From the project picker at the top, make sure your project is selected.
- In the filter field, type Disable custom mcp server connector for gemini enterprise to locate the constraint.
- Click the constraint name to open its Policy details page.
- Click Manage Policy.
- On the Edit policy page, select Override parent's policy.
- Click Add a rule.
- Under Enforcement, select Off.
- Click Set policy to save your changes.
You should see a notification confirming the policy update. The change can take up to 15 minutes to fully propagate.
Can't find the policy or don't have permission?
If the constraint doesn't appear in the list, your organization may not enforce it. In that case, you can skip this substep. If you see a permission error, you need the roles/orgpolicy.policyAdmin role on your project or organization.
help me get Organization Policy Administrator access to override the Custom MCP Server policy
Create the Custom MCP Server data store in Gemini Enterprise
With OAuth credentials ready and the policy override in place, you can now register your MCP server in Gemini Enterprise. This creates the connection between Gemini's AI assistant and your Cloud Run service.
What role do I need for this?
Creating a data store requires the Discovery Engine Editor role (roles/discoveryengine.editor). If you're the project owner, you already have this.
- In Google Cloud Console, navigate to Gemini Enterprise. You can search for it in the console search bar.
- Click Data stores in the left sidebar.
- Click Create data store.
- Select Custom MCP Server (marked as Preview).
Custom MCP Server is in Preview
This feature is currently in Preview. The UI and behavior may change before General Availability. If you don't see the Custom MCP Server option, confirm that the organization policy override has propagated (it can take up to 15 minutes).
- In the MCP Server URL field, enter your Cloud Run HTTPS URL with /mcp appended. It looks like https://mcp-server-PROJECT_NUMBER.us-central1.run.app/mcp (use your actual URL from Step 3).
- In the Authorization URL field, enter:
https://accounts.google.com/o/oauth2/v2/auth
- In the Token URL field, enter:
https://oauth2.googleapis.com/token
- In the Client ID field, paste the Client ID you saved from the OAuth credentials step.
- In the Client Secret field, paste the Client Secret you saved from the OAuth credentials step.
- In the Scopes field, enter:
openid email profile
- Click Login.
- A Google sign-in window appears. Sign in with your Google account and authorize the application.
- After successful authorization, click Create to finish creating the data store.
You should see your new Custom MCP Server data store appear in the data stores list. Gemini Enterprise can now discover and invoke the tools on your deployed MCP server.
What just happened?
Gemini Enterprise now has a registered connection to your Cloud Run MCP server. When users interact with Gemini in this project, Gemini can use the OAuth credentials to authenticate with your server and call your tools via the StreamableHTTP protocol.
The Authorization URL and Token URL are Google's standard OAuth 2.0 endpoints. They handle the authentication handshake so Gemini can prove it has permission to call your server.
Authorization flow failing?
Double-check that your authorized redirect URI is exactly https://vertexaisearch.cloud.google.com/oauth-redirect in your OAuth client settings. Also verify that the Client ID and Client Secret match what's shown in the APIs & Services > Credentials page.
help me debug the OAuth authorization flow for my Custom MCP Server data store in Gemini Enterprise
Secret mission
Connect Your MCP Server to Gemini CLI
Your MCP server is live on Cloud Run and connected to Gemini Enterprise. But what about your local development workflow? In this secret mission, you'll connect your deployed server to Gemini CLI so you can invoke your tools directly from the terminal.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Cloud Run scales to zero automatically when idle, so there are no ongoing costs while your service is not receiving requests.
Resources you used:
- Cloud Run service mcp-server in us-central1.
- Artifact Registry repository cloud-run-source-deploy in us-central1.
- OAuth 2.0 Client ID credential (in APIs & Services > Credentials).
- Gemini Enterprise Custom MCP Server data store.
- Organization policy override (enforcement OFF for Custom MCP Server connector).
- Gemini CLI settings file ~/.gemini/settings.json (Secret Mission).
Keep everything running
No action needed. Choose this if you plan to keep using your MCP server with Gemini Enterprise or Gemini CLI.
- Cloud Run scales to zero when idle. You pay nothing while no requests are coming in.
- The free tier covers 2 million requests per month, 240,000 vCPU-seconds, and 450,000 GiB-seconds. Development and testing usage fits comfortably within these limits.
- Your OAuth Client ID credential and Gemini Enterprise data store remain configured and ready to use.
- The identity token in ~/.gemini/settings.json expires hourly. You will need to refresh it with gcloud auth print-identity-token before using Gemini CLI again.
Pause - I'll come back to this later
Cloud Run already scales to zero when idle, so there is nothing to pause on the compute side. You can optionally remove the Gemini Enterprise data store connection and re-create it later.
- In Google Cloud Console, navigate to Gemini Enterprise > Data stores. Select your Custom MCP Server data store and delete it. You can re-create it later using the same Cloud Run URL.
- Your Cloud Run service, Artifact Registry image, and OAuth credential remain in place for when you return.
Delete - I don't want to use this again
Remove all project resources to start fresh. Follow each step below.
- Delete the Cloud Run service by running:
- Delete the Artifact Registry repository by running:
gcloud run services delete mcp-server --region=us-central1
gcloud artifacts repositories delete cloud-run-source-deploy --location=us-central1
- Delete the OAuth 2.0 Client ID. In Google Cloud Console, go to APIs & Services > Credentials. Find the OAuth 2.0 Client ID you created for this project, click the delete icon next to it, and confirm.
- Delete the Gemini Enterprise data store. In Google Cloud Console, navigate to Gemini Enterprise > Data stores. Select your Custom MCP Server data store and delete it.
- Revert the organization policy override. In Google Cloud Console, go to Organization Policies, filter for the Custom MCP Server connector policy, click Manage Policy, remove the override rule you added, and save.
- Remove the Gemini CLI MCP server configuration by deleting the my-mcp-server entry from ~/.gemini/settings.json. If you have no other MCP servers configured, you can delete the file entirely.
- Uninstall Gemini CLI globally by running:
npm uninstall -g @google/gemini-cli
That's a Wrap!
That's a Wrap!
Nice work! You've taken a locally-running MCP server and deployed it to production on Google Cloud Run, fully integrated with Gemini Enterprise.
You've learned how to:
- Adapt an MCP server to StreamableHTTP transport for Cloud Run compatibility using stateless mode.
- Deploy a secure, authenticated MCP server to Cloud Run that scales to zero and costs nothing when idle.
- Register your deployed server as a Custom MCP Server data store in Gemini Enterprise with OAuth 2.0 authentication.
- Connected your deployed server to Gemini CLI for local development and testing workflows.
Ready to quiz yourself?