在上一章中,你深入探索了 MCP 的核心,把服务器从简单的“命令—响应”系统升级为能与 LLM 协作的上下文感知伙伴。你学会了用 Resources 暴露只读数据、用 Prompts 引导 LLM,并借助 Context 对象实现日志、进度汇报与引导提问(elicitation)的交互。
可以说,你已经为 AI 打造了一间功能强大的“互动工坊”。但现在,这间工坊既没有上锁,又运行在临时车库(你的本机)里。这对开发很完美;可如果要构建真实世界的应用,你需要安全与稳定的托管环境。
本章的目标,是把你的 MCP 服务器从原型晋级为可用于生产的服务。我们将解决上线最关键的两个方面:安全(只允许授权客户端使用你的服务器)与部署(将其运行在稳健、可扩展、可 7×24 运行的环境中)。
在本章结束时,你将能够:
- 使用 JWT Bearer Token 身份验证保护你的 MCP 服务器。在工具内部**访问令牌声明(claims)**以进行授权检查。用 Docker 将服务器容器化,以便可移植部署。将服务器部署到现代无服务器平台 Google Cloud Run。或者在传统 虚拟机 + Nginx 的方式下手动部署。
让我们先“给门上锁”,再把工坊搬到“黄金地段”。
身份验证:守住你的工具
到目前为止,任何找到你服务器 URL 的人都能调用其工具。对一个“字符计数”工具也许无伤大雅;但如果工具会访问私有数据库、代用户发邮件、或调用付费 API 呢?不受限的访问将是安全与成本的双重灾难。
**身份验证(Authentication)**就是验证客户端“是谁”。对 MCP 服务器,我们采用一种现代、标准的方法:JWT Bearer Token 身份验证。
基本思路如下:
- 一个授权服务器/身份提供商(Authorization Server / Identity Provider)向客户端签发一个JSON Web Token(JWT) ,这是带数字签名的“通行证”。客户端在请求头中附带该令牌:
Authorization: Bearer <很长的 JWT 字符串>。我们的 MCP 服务器作为资源服务器(Resource Server) ,无需与授权服务器通信;它只需要授权服务器的公钥,用来验证令牌签名的有效性与未被篡改。这种方式既安全又易扩展。你的 MCP 服务器不处理密码或机密,只需验证签名令牌。下面分别用 fastmcp 与 mcp 来实现。
保护 fastmcp 服务器
fastmcp 对 Bearer Token 的配置非常简洁:既提供验证令牌的辅助类,也提供(仅用于开发演示的)创建令牌的工具。
让我们构建一个带受保护资源的服务器。
图 54. walled_mcp_server.py(fastmcp)
from fastmcp import FastMCPfrom fastmcp.server.auth.providers.bearer import RSAKeyPairfrom fastmcp.server.auth import BearerAuthProviderfrom fastmcp.server.dependencies import get_access_token, AccessToken# 1. Generate a public/private key pair for signing/verifying tokens.# In production, the private key lives on an Authorization Server.key_pair = RSAKeyPair.generate()# 2. Configure the authentication provider.# The server only needs the public key to verify tokens.auth = BearerAuthProvider( public_key=key_pair.public_key, issuer="https://your-awesome-mcp-server.com", audience="my-mcp-server")# 3. Attach the auth provider to the MCP server instance.mcp = FastMCP("My MCP Server", auth=auth)# 4. For demonstration, create a valid token signed with the private key.token = key_pair.create_token( subject="mcp-user", issuer="https://your-awesome-mcp-server.com", audience="my-mcp-server", scopes=["read", "write"])# 5. Save the token for our client to use.with open("token.txt", "w") as f: f.write(token)# 6. Define a protected resource.@mcp.resource("data://database")def get_data() -> str: # 7. Access the validated token's claims inside the function. access_token: AccessToken = get_access_token() print(f"Access granted! Scopes: {access_token.scopes}, Subject: {access_token.client_id}") return "Secret data from the database"if __name__ == "__main__": mcp.run(transport="http", port=9000)逐点说明:
- 密钥对:
RSAKeyPair.generate() 生成 RSA 公私钥。私钥用于签发令牌,公钥用于验证。认证提供者:BearerAuthProvider 定义安全策略:传入 public_key 校验签名,并指定期望的 issuer(签发者)与 audience(受众)。不匹配直接拒绝。挂载到服务器:构造 FastMCP 时传入 auth=auth,即可“筑墙”——所有端点都需要有效令牌。创建令牌(开发用途) :create_token() 用私钥签出带有 subject(用户/客户端 ID)与 scopes(权限)的 JWT。保存令牌:把令牌写到文件,便于客户端读取。受保护资源:被 @mcp.resource 装饰的函数现在受保护。未认证请求将被拒绝。访问声明:在受保护函数内部,get_access_token() 可获取已验证的令牌对象 AccessToken,从而进行细粒度授权(如基于 scopes 或 client_id 的判断)。接着是客户端:读取并携带令牌访问服务器。
图 55. walled_mcp_client.py(fastmcp)
import asynciofrom fastmcp import Clientfrom fastmcp.client.transports import ( StreamableHttpTransport,)from pathlib import Path# 1. Read the token from the file.token = Path("token.txt").read_text().strip()# 2. Pass the token to the Client constructor.client = Client(transport=StreamableHttpTransport("http://localhost:9000/mcp"), auth=token)async def main(): async with client: data = await client.read_resource("data://database") print(data)asyncio.run(main())客户端逻辑非常直接:
- 读取我们生成的令牌;通过
auth=token 传给 Client;fastmcp 会自动在每个请求里加上 Authorization: Bearer ... 头。试运行:
图 56. 终端
> python walled_mcp_server.py再在新终端运行客户端:
图 57. 终端
> python walled_mcp_client.py[TextResourceContents(uri=AnyUrl('data://database'), mimeType='text/plain', meta=None, text='Secret data from the database')]成功!服务器验证了令牌,并在 get_data 中可读取到其 claims。若去掉客户端的 auth=token,服务器将返回 401 Unauthorized。
保护 mcp 库服务器
如你所料,用更底层的 mcp 库实现会更显式,但原理完全相同:你需要手工搭建 fastmcp 已帮你封装好的组件。
图 58. walled_mcp_server.py(mcp)
import timefrom typing import Anyfrom jose import jwtfrom cryptography.hazmat.primitives import serializationfrom cryptography.hazmat.primitives.asymmetric import rsafrom mcp.server.fastmcp.server import FastMCPfrom mcp.server.auth.provider import TokenVerifier, AccessTokenfrom mcp.server.auth.middleware.auth_context import get_access_tokenfrom mcp.server.auth.settings import AuthSettings# 1. Create a custom token verifier class.class SimpleJWTVerifier(TokenVerifier): def __init__(self, public_key: str, audience: str, issuer: str): self.public_key = public_key self.audience = audience self.issuer = issuer async def verify_token(self, token: str) -> AccessToken | None: try: payload = jwt.decode( token, self.public_key, algorithms=["RS256"], audience=self.audience, issuer=self.issuer, ) return AccessToken( token=token, client_id=payload.get("sub"), scopes=payload.get("scopes", []), expires_at=payload.get("exp"), ) except jwt.JWTError: return None# 2. Manually generate RSA keys using the cryptography library.private_key_obj = rsa.generate_private_key(public_exponent=65537, key_size=2048)private_key_pem = private_key_obj.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(),).decode("utf-8")public_key_pem = private_key_obj.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo,).decode("utf-8")# 3. Instantiate the verifier and configure auth settings.token_verifier = SimpleJWTVerifier( public_key=public_key_pem, audience="my-mcp-server", issuer="https://your-awesome-mcp-server.com",)auth_settings = AuthSettings( issuer_url="https://auth.my-mcp-server.com", resource_server_url="http://localhost:9000", required_scopes=["data:read"])# 4. Attach the verifier and settings to the server.mcp = FastMCP( name="My Simple SDK Server", token_verifier=token_verifier, auth=auth_settings, host="127.0.0.1", port=9000)# 5. Manually create the token claims and encode it using jose.claims: dict[str, Any] = { "iss": "https://your-awesome-mcp-server.com", "aud": "my-mcp-server", "sub": "mcp-user", "exp": int(time.time()) + 3600, "iat": int(time.time()), "scopes": ["data:read", "data:write"],}token = jwt.encode(claims, private_key_pem, algorithm="RS256")with open("token.txt", "w") as f: f.write(token)@mcp.resource("data://database")def get_data() -> str: access_token: AccessToken = get_access_token() print(f"Scopes of token: {access_token.scopes}") print(f"Client id or subject: {access_token.client_id}") return "Secret data from the database"if __name__ == "__main__": mcp.run(transport="streamable-http")差异要点:
- 自定义验证器:实现
TokenVerifier 接口;用 python-jose 解码 JWT,校验签名、audience 与 issuer。手动生成密钥:使用 cryptography 生成并序列化 RSA 密钥对(这就是 fastmcp 内部做的事)。认证配置:实例化自定义验证器,并提供 AuthSettings。挂载到服务器:FastMCP(来自 mcp.server.fastmcp)接收 token_verifier 与 auth。手动创建令牌:自己构造 claims,用私钥 jwt.encode 签发。在此之前,先安装依赖库:
图 59. 安装依赖
> uv add python-jose[cryptography]客户端也更“手工化”,需要自行构造 Authorization 请求头。
图 60. walled_mcp_client.py(mcp)
import asynciofrom pathlib import Pathfrom datetime import timedeltafrom mcp.client.streamable_http import streamablehttp_clientfrom mcp.client.session import ClientSessionSERVER_URL = "http://localhost:9000/mcp"async def main(): token = Path("token.txt").read_text().strip() # Manually create the headers dictionary. auth_headers = {"Authorization": f"Bearer {token}"} # Pass the headers to the client transport. async with streamablehttp_client( url=SERVER_URL, headers=auth_headers, timeout=timedelta(seconds=30) ) as (read_stream, write_stream, get_session_id): async with ClientSession(read_stream, write_stream) as session: await session.initialize() data = await session.read_resource("data://database") print(data)if __name__ == "__main__": asyncio.run(main())关闭先前的 fastmcp 服务器,运行新的 mcp 版本服务器;再运行该客户端。结果与 fastmcp 相同,说明两种抽象层级都能达到同样安全的效果。
图 61. 终端
> python walled_mcp_client.pymeta=None contents=[TextResourceContents(uri=AnyUrl('data://database'), mimeType='text/plain', meta=None, text='Secret data from the database')]至此,你已经掌握了保护 MCP 服务器的基础。接下来,是时候把它们从本地“搬出去”,走向真实的线上环境了。
部署:正式上线(Going Live)
在本机终端里跑一个服务器适合开发阶段,但并不是现实世界的解决方案。生产部署意味着让你的代码运行在可靠、可扩展、始终可用的服务器上。我们将探讨两种常见策略:把应用部署到现代无服务器平台(Google Cloud Run),以及部署到传统虚拟机(VM) 。
使用 Docker 与 Cloud Run 的无服务器部署
像 Google Cloud Run、AWS Lambda、Azure Functions 这样的无服务器平台是部署应用的绝佳方式。你提供容器中的代码,平台负责剩下的一切:启动服务器、在无流量时自动停止、在高并发时自动扩容。你只为实际用量付费。
我们将把一个简单的 fastmcp 服务器部署到 Google Cloud Run。流程分四步:创建项目文件、构建 Docker 镜像、部署到 Cloud Run、测试。
第 1 步:创建项目文件
先为部署项目创建一个新目录。在目录内创建以下四个文件。
应用(server.py)
这是一个最简的 MCP 服务器。注意 host 与 port 的设置。
图 62. server.py
from fastmcp import FastMCP mcp = FastMCP("MCP Server on Cloud Run")@mcp.tool()def count_characters(string: str) -> int: return len(string)if __name__ == "__main__": # Host '0.0.0.0' listens on all network interfaces, which is required for containers. # Cloud Run provides the port via the $PORT environment variable, which Uvicorn uses automatically. # 8080 is a common default. mcp.run(transport="http", host="0.0.0.0", port=8080)依赖(pyproject.toml)
告诉 uv 需要安装哪些库。
图 63. pyproject.toml
[project]name = "mcp-on-cloudrun"version = "0.1.0"description = "MCP on Cloud Run"requires-python = ">=3.10"dependencies = [ "fastmcp==2.10.5",]容器构建脚本(Dockerfile)
Dockerfile 是构建容器镜像的指令集。下面这个示例使用现代的多阶段构建与 uv,兼顾速度与体积。
图 64. Dockerfile
# Start with a small, official Python image.FROM python:3.12-slim# Use a multi-stage build to copy the `uv` binary without its build environment.COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/# Copy our application code into the image.COPY . /appWORKDIR /app# Install dependencies using the copied `uv` binary.RUN uv sync# Tell Docker which port the container will listen on.EXPOSE $PORT# The command to run when the container starts.CMD ["uv", "run", "server.py"]测试客户端(client.py)
稍后用于测试部署;它会连接到本地代理。
图 65. client.py
import asynciofrom fastmcp import Clientfrom fastmcp.client.transports import ( StreamableHttpTransport,)client = Client(transport=StreamableHttpTransport("http://localhost:8080/mcp"))async def main(): async with client: tools = await client.list_tools() print(tools) output = await client.call_tool("count_characters", {"string": "Strawberry is delicious!"}) extracted_text = output.content[0].text print(extracted_text)asyncio.run(main())第 2 步:构建并部署到 Google Cloud
接下来使用 gcloud 命令行工具构建镜像并部署。确保你已安装并配置好 Google Cloud SDK。
首先在 Artifact Registry 中创建一个仓库来存放 Docker 镜像。把 your-gcp-project 替换为你的实际 GCP 项目 ID。
图 66. 终端
> gcloud artifacts repositories create mcp-servers --repository-format=docker --location=asia-southeast1Created repository [mcp-servers].使用 Cloud Build 按 Dockerfile 构建镜像并推送到刚创建的仓库。
图 67. 终端
> gcloud builds submit --region=asia-southeast1 --tag asia-southeast1-docker.pkg.dev/your-gcp-project/mcp-servers/mcp-server:latest...IMAGES: asia-southeast1-docker.pkg.dev/your-gcp-project/mcp-servers/mcp-server (+1 more)STATUS: SUCCESS该命令会打包项目文件、发送到 Google Cloud Build,随后按 Dockerfile 的步骤构建镜像并保存。
权限提示:首次使用 Cloud Run 时,可能需要为你的账号授予管理部署及以服务账号身份运行的权限。通常每个项目只需执行一次。将 your-gcp-project 与 youremail@googlecloud.com 替换为你的信息。
gcloud projects add-iam-policy-binding your-gcp-project --member="user:youremail@googlecloud.com" --role="roles/run.admin"# 获取项目编号gcloud projects describe your-gcp-project --format='value(projectNumber)'# 使用项目编号为默认计算服务账号授予 Service Account User 角色gcloud iam service-accounts add-iam-policy-binding "PROJECT_NUMBER-compute@developer.gserviceaccount.com" --member="user:youremail@googlecloud.com" --role="roles/iam.serviceAccountUser"最后,部署镜像到 Cloud Run。--no-allow-unauthenticated 使服务默认私有,这符合安全最佳实践。
图 68. 终端
> gcloud run deploy mcp-server --image asia-southeast1-docker.pkg.dev/your-gcp-project/mcp-servers/mcp-server:latest --region=asia-southeast1 --no-allow-unauthenticatedDeploying container to Cloud Run service [mcp-server]... Done....Service URL: https://mcp-server-214935060214.asia-southeast1.run.app你的 MCP 服务器现在已经上线!
第 3 步:测试已部署的服务
由于我们部署的是私有服务,不能直接访问 URL。不过,gcloud 提供了一个安全本地代理,可为你处理认证。
图 69. 终端
> gcloud run services proxy mcp-server --region=asia-southeast1Proxying to Cloud Run service [mcp-server]...http://127.0.0.1:8080 proxies to https://mcp-server-bgynkccowq-as.a.run.app这条命令创建了一条隧道:你在本机发往 http://127.0.0.1:8080 的请求会被安全转发到线上 Cloud Run 服务。
现在打开一个新终端,运行先前创建的 client.py。
图 70. 终端
> python .\client.py[Tool(name='count_characters', ...)]24成功!你的本地客户端脚本已经与全球可达、可扩展且安全的 MCP 服务器完成了通信。
第 4 步:清理资源
不再使用的资源最好及时清理,以免产生费用。
图 71. 终端
> gcloud run services delete mcp-server --region=asia-southeast1Service [mcp-server] will be deleted.Do you want to continue (Y/n)? YDeleted service [mcp-server].在虚拟机上手动部署
有时你需要比无服务器平台更高的掌控力。将应用部署到 VM 能让你完全控制操作系统、网络与已安装软件。代价是你需要自己负责运维与安全。
通用做法:让 Python 服务器监听一个高位端口(如 8080 或 9000),并用成熟的 Web 服务器 Nginx 作为反向代理。Nginx 监听标准端口(80/443),再把流量转发给应用。
第 1 步:准备 VM 与应用
先在你喜欢的云商处开一台 VM(例如 GCP/AWS/DigitalOcean 上的 Ubuntu 24.04 LTS)。确保服务器能接收 HTTP 流量。SSH 登录后:
创建服务器目录、用 uv 配环境、安装 fastmcp。
图 72. VM 终端
$ mkdir mcp_server$ cd mcp_server$ wget -qO- https://astral.sh/uv/install.sh | sh$ source $HOME/.local/bin/env$ uv init$ uv add fastmcp在 VM 上创建 server.py。
图 73. server.py
from fastmcp import FastMCPmcp = FastMCP("My MCP Server")@mcp.tool()def count_characters(string: str) -> int: return len(string)if __name__ == "__main__": mcp.run(transport="http", host="0.0.0.0", port=9000)这与 Cloud Run 的示例相同,只是端口换成 9000。
第 2 步:让 MCP 服务器常驻运行
不能直接 python server.py 后关闭 SSH,因为进程会随会话终止。一个简单有效的后台运行工具是 screen。
图 74. VM 终端
# 启动名为 'mcp' 的 screen 会话$ screen -S mcp# 在 screen 中进入项目并运行服务器$ cd mcp_server$ source .venv/bin/activate$ python server.py# 按 Ctrl+A 再 Ctrl+D 退出会话,进程仍在后台运行# 随时可用 `screen -r mcp` 重新连接第 3 步:将 Nginx 配置为反向代理
安装 Nginx 并设置为开机自启。
图 75. VM 终端
$ sudo apt update$ sudo apt install nginx -y$ sudo systemctl enable nginx为服务创建 Nginx 配置。假定域名是 app.example.com,希望在路径 /mcp-server/ 下提供 MCP 服务。
图 76. VM 终端
$ sudo vim /etc/nginx/sites-available/app.example.com.conf加入以下配置:
图 77. /etc/nginx/sites-available/app.example.com.conf
server { listen 80; listen [::]:80; server_name app.example.com; location /mcp-server/ { proxy_pass http://localhost:9000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } root /var/www/app.example.com; index index.html; location / { try_files $uri $uri/ =404; }}启用该配置、创建测试页并重启 Nginx:
图 78. VM 终端
$ sudo ln -s /etc/nginx/sites-available/app.example.com.conf /etc/nginx/sites-enabled/$ sudo mkdir -p /var/www/app.example.com$ echo "Nginx is working!" | sudo tee /var/www/app.example.com/index.html$ sudo systemctl restart nginx第 4 步:测试
在本地机器上,先让你的电脑知道 app.example.com 指向 VM 的公网 IP。编辑 hosts 文件:
- Windows:
C:\Windows\System32\drivers\etc\hostsmacOS/Linux:/etc/hosts添加一行:YOUR_VM_IP_ADDRESS app.example.com
在 Windows 上可通过下列命令刷新 DNS 解析缓存:
图 79. 刷新 DNS 缓存
> ipconfig /flushdns在本地创建一个指向新 Nginx 代理 URL 的客户端脚本:
图 80. local_vm_client.py
import asynciofrom fastmcp import Clientfrom fastmcp.client.transports import ( StreamableHttpTransport,)client = Client(transport=StreamableHttpTransport("http://app.example.com/mcp-server/mcp/"))async def main(): async with client: tools = await client.list_tools() print(tools) output = await client.call_tool("count_characters", {"string": "Strawberry is delicious!"}) extracted_text = output.content[0].text print(extracted_text)asyncio.run(main())运行该客户端。它会连接到 app.example.com,由 Nginx 接收请求并转发到正在运行的 Python 进程,你将获得返回结果。至此,你已在传统 VM 上成功部署并对外暴露你的 MCP 服务器。
关键要点(Key Takeaways)
本章带你完成了从本地原型到生产级服务的关键旅程,为你的 MCP 工具箱新增了两项至关重要的能力。
认证不可或缺:你学会了使用 JWT Bearer Token 为服务器加固——这是一种标准且可扩展的方法。现在你可以保护你的工具,防止未授权访问。
授权让工具更有力量:借助 get_access_token(),你的工具可以检查客户端的身份(client_id)与权限(scopes),从而实现强大而细粒度的访问控制逻辑。
多样的部署选项:你不再被本地机器束缚。你已经掌握如何:
- 使用 Docker 打包应用,获得最大可移植性;部署到 Google Cloud Run 等无服务器平台,获得可扩展性与易运维;部署到传统 VM + Nginx,获得最大掌控力与灵活性。
至此,你已具备构建、加固、部署健壮的、面向真实场景的 MCP 应用的完整能力。
下一章我们将探索高级服务器架构:你已经构建了独立的 MCP 服务器,但如果要把 MCP 能力集成进现有 Web 应用呢?我们将深入讲解如何将 MCP 与标准 Python Web 框架(如 Starlette)结合,把 MCP 服务器挂载为更大 API 的一部分,并构建强大的 ASGI 中间件。你将学会让 MCP 成为你既有 Web 生态中的“一等公民”。
