Skip to content

Commit 4b20167

Browse files
feat(run): add FastMCP MCP server sample (#13425)
* feat(run): add FastMCP MCP server sample * chore: add region tags * chore: update region tags
1 parent 4891517 commit 4b20167

File tree

7 files changed

+923
-0
lines changed

7 files changed

+923
-0
lines changed

run/mcp-server/.dockerignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Dockerfile
2+
README.md
3+
*.pyc
4+
*.pyo
5+
*.pyd
6+
__pycache__
7+
.pytest_cache
8+
.env
9+
.venv/
10+
venv/

run/mcp-server/Dockerfile

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright 2025 Google, LLC.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START cloudrun_mcpserver_dockerfile_python]
16+
17+
# Use the official Python image
18+
FROM python:3.13-slim
19+
20+
# Install uv
21+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
22+
23+
# Install the project into /app
24+
COPY . /app
25+
WORKDIR /app
26+
27+
# Allow statements and log messages to immediately appear in the logs
28+
ENV PYTHONUNBUFFERED=1
29+
30+
# Install dependencies
31+
RUN uv sync
32+
33+
EXPOSE $PORT
34+
35+
# Run the FastMCP server
36+
CMD ["uv", "run", "server.py"]
37+
38+
# [END cloudrun_mcpserver_dockerfile_python]

run/mcp-server/README.md

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Cloud Run MCP Server Sample
2+
3+
This sample shows how to deploy a remote MCP server to Cloud Run.
4+
5+
This sample uses the `streamable-http` transport, which allows for running MCP
6+
servers remotely. You can read more about MCP transports in the
7+
[official MCP docs](https://modelcontextprotocol.io/docs/concepts/architecture#transport-layer).
8+
9+
## Benefits of running an MCP server remotely
10+
11+
Running an MCP server remotely on Cloud Run can provide several benefits:
12+
13+
- **📈 Scalability**: Cloud Run is built to [rapidly scale out to handle all incoming requests](https://cloud.google.com/run/docs/about-instance-autoscaling).
14+
Cloud Run will scale your MCP server automatically based on demand.
15+
- **👥 Centralized server**: You can share access to a centralized MCP server
16+
with team members through IAM privileges, allowing them to connect to it from
17+
their local machines instead of all running their own servers locally. If a
18+
change is made to the MCP server, all team members will benefit from it.
19+
- **🔐 Security**: Cloud Run provides an easy way to force authenticated
20+
requests. This allows only secure connections to your MCP server, preventing
21+
unauthorized access.
22+
23+
> [!IMPORTANT]
24+
> The security aspect mentioned above is critical. If you don't enforce
25+
authentication, anyone on the public internet can potentially access and
26+
call your MCP server.
27+
28+
## Math MCP Server
29+
30+
LLMs are great at **non-deterministic tasks**: understanding intent, generating
31+
creative text, summarizing complex ideas, and reasoning about abstract
32+
concepts. However, they are notoriously unreliable for **deterministic tasks**
33+
– things that have one, and only one, correct answer.
34+
35+
Enabling LLMs with **deterministic tools** (such as math operations) is one
36+
example of how tools can provide valuable context to improve the use of LLMs
37+
using MCP.
38+
39+
This sample uses [FastMCP](https://gofastmcp.com/getting-started/welcome) to create
40+
a simple math MCP server that has two tools: `add` and `subtract`. FastMCP
41+
provides a fast, Pythonic way to build MCP servers and clients.
42+
43+
44+
## Prerequisites
45+
46+
- Python 3.10+
47+
- Uv (for package and project management, see [docs for installation](https://docs.astral.sh/uv/getting-started/installation/))
48+
- Google Cloud SDK (gcloud)
49+
50+
## Setup
51+
52+
Set your Google Cloud credentials and project.
53+
54+
```bash
55+
gcloud auth login
56+
export PROJECT_ID=<your-project-id>
57+
gcloud config set project $PROJECT_ID
58+
```
59+
60+
## Deploy
61+
62+
You can deploy directly from source or using a container image.
63+
64+
Both options use the `--no-allow-unauthenticated` flag to require authentication.
65+
66+
This is important for security reasons. If you don't require authentication,
67+
anyone can call your MCP server and potentially cause damage to your system.
68+
69+
<details open>
70+
<summary>Option 1 - Deploy from source</summary>
71+
72+
```bash
73+
gcloud run deploy mcp-server --no-allow-unauthenticated --region=us-central1 --source .
74+
```
75+
76+
</details>
77+
78+
<details>
79+
<summary>Option 2 - Deploy from a container image</summary>
80+
81+
Create an Artifact Registry repository to store the container image.
82+
83+
```bash
84+
gcloud artifacts repositories create mcp-servers \
85+
--repository-format=docker \
86+
--location=us-central1 \
87+
--description="Repository for remote MCP servers" \
88+
--project=$PROJECT_ID
89+
```
90+
91+
Build the container image and push it to Artifact Registry with Cloud Build.
92+
93+
```bash
94+
gcloud builds submit --region=us-central1 --tag us-central1-docker.pkg.dev/$PROJECT_ID/mcp-servers/mcp-server:latest
95+
```
96+
97+
Deploy the container image to Cloud Run.
98+
99+
```bash
100+
gcloud run deploy mcp-server \
101+
--image us-central1-docker.pkg.dev/$PROJECT_ID/mcp-servers/mcp-server:latest \
102+
--region=us-central1 \
103+
--no-allow-unauthenticated
104+
```
105+
106+
</details>
107+
108+
If your service has successfully deployed you will see a message like the following:
109+
110+
```bash
111+
Service [mcp-server] revision [mcp-server-12345-abc] has been deployed and is serving 100 percent of traffic.
112+
```
113+
114+
## Authenticating MCP Clients
115+
116+
Since you specified `--no-allow-unauthenticated` to require authentication, any
117+
MCP client connecting to the remote MCP server will need to authenticate.
118+
119+
The official docs for [Host MCP servers on Cloud Run](https://cloud.google.com/run/docs/host-mcp-servers#authenticate_mcp_clients)
120+
provides more information on this topic depending on where the MCP client is
121+
running.
122+
123+
For this sample, run the [Cloud Run proxy](https://cloud.google.com/sdk/gcloud/reference/run/services/proxy)
124+
to create an authenticated tunnel to the remote MCP server on your local
125+
machine.
126+
127+
By default, the URL of Cloud Run service requires all requests to be
128+
authorized with the [Cloud Run Invoker](https://cloud.google.com/run/docs/securing/managing-access#invoker)
129+
(`roles/run.invoker`) IAM role. This IAM policy binding ensures that a
130+
strong security mechanism is used to authenticate your local MCP client.
131+
132+
You should make sure that you or any team members trying to access the remote
133+
MCP server have the `roles/run.invoker` IAM role bound to their Google Cloud
134+
account.
135+
136+
> [!TIP]
137+
> The below command may prompt you to download the Cloud Run proxy if it is
138+
> not already installed. Follow the prompts to download and install it.
139+
140+
```bash
141+
gcloud run services proxy mcp-server --region=us-central1
142+
```
143+
144+
You should see the following output:
145+
146+
```bash
147+
Proxying to Cloud Run service [mcp-server] in project [<YOUR_PROJECT_ID>] region [us-central1]
148+
http://127.0.0.1:8080 proxies to https://mcp-server-abcdefgh-uc.a.run.app
149+
```
150+
151+
All traffic to `http://127.0.0.1:8080` will now be authenticated and forwarded to
152+
the remote MCP server.
153+
154+
## Testing the remote MCP server
155+
156+
To test the remote MCP server use the
157+
[test_server.py](test_server.py) test script. It uses the FastMCP client to
158+
connect to `http://127.0.0.1:8080/mcp` (note the `/mcp` at the end for the
159+
`streamable-http` transport) and calls the `add` and `subtract` tools.
160+
161+
> [!NOTE]
162+
> Make sure you have the Cloud Run proxy running before running the test server.
163+
164+
In a **new terminal** run:
165+
166+
```bash
167+
uv run test_server.py
168+
```
169+
170+
You should see the following output:
171+
172+
```bash
173+
>>> 🛠️ Tool found: add
174+
>>> 🛠️ Tool found: subtract
175+
>>> 🪛 Calling add tool for 1 + 2
176+
<<< Result: 3
177+
>>> 🪛 Calling subtract tool for 10 - 3
178+
<<< Result: 7
179+
```
180+
181+
You have successfully deployed a remote MCP server to Cloud Run and tested it
182+
using the FastMCP client.

run/mcp-server/pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[project]
2+
name = "mcp-server"
3+
version = "0.1.0"
4+
description = "Example of deploying an MCP server on Cloud Run"
5+
readme = "README.md"
6+
requires-python = ">=3.10"
7+
dependencies = [
8+
"fastmcp==2.8.0",
9+
]

run/mcp-server/server.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START cloudrun_mcpserver]
16+
import asyncio
17+
import logging
18+
import os
19+
20+
from fastmcp import FastMCP
21+
22+
logger = logging.getLogger(__name__)
23+
logging.basicConfig(format="[%(levelname)s]: %(message)s", level=logging.INFO)
24+
25+
mcp = FastMCP("MCP Server on Cloud Run")
26+
27+
@mcp.tool()
28+
def add(a: int, b: int) -> int:
29+
"""Use this to add two numbers together.
30+
31+
Args:
32+
a: The first number.
33+
b: The second number.
34+
35+
Returns:
36+
The sum of the two numbers.
37+
"""
38+
logger.info(f">>> 🛠️ Tool: 'add' called with numbers '{a}' and '{b}'")
39+
return a + b
40+
41+
@mcp.tool()
42+
def subtract(a: int, b: int) -> int:
43+
"""Use this to subtract two numbers.
44+
45+
Args:
46+
a: The first number.
47+
b: The second number.
48+
49+
Returns:
50+
The difference of the two numbers.
51+
"""
52+
logger.info(f">>> 🛠️ Tool: 'subtract' called with numbers '{a}' and '{b}'")
53+
return a - b
54+
55+
if __name__ == "__main__":
56+
logger.info(f"🚀 MCP server started on port {os.getenv('PORT', 8080)}")
57+
# Could also use 'sse' transport, host="0.0.0.0" required for Cloud Run.
58+
asyncio.run(
59+
mcp.run_async(
60+
transport="streamable-http",
61+
host="0.0.0.0",
62+
port=os.getenv("PORT", 8080),
63+
)
64+
)
65+
66+
# [END cloudrun_mcpserver]

run/mcp-server/test_server.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import asyncio
16+
17+
from fastmcp import Client
18+
19+
async def test_server():
20+
# Test the MCP server using streamable-http transport.
21+
# Use "/sse" endpoint if using sse transport.
22+
async with Client("http://localhost:8080/mcp") as client:
23+
# List available tools
24+
tools = await client.list_tools()
25+
for tool in tools:
26+
print(f">>> 🛠️ Tool found: {tool.name}")
27+
# Call add tool
28+
print(">>> 🪛 Calling add tool for 1 + 2")
29+
result = await client.call_tool("add", {"a": 1, "b": 2})
30+
print(f"<<< ✅ Result: {result[0].text}")
31+
# Call subtract tool
32+
print(">>> 🪛 Calling subtract tool for 10 - 3")
33+
result = await client.call_tool("subtract", {"a": 10, "b": 3})
34+
print(f"<<< ✅ Result: {result[0].text}")
35+
36+
if __name__ == "__main__":
37+
asyncio.run(test_server())

0 commit comments

Comments
 (0)