本教程聚焦 MCP(Model Context Protocol)的远程传输与生产级认证这一进阶主题。你将学到如何把一个本地 stdio MCP 工具升级为可被任何授权 Claude Code 客户端跨网络访问的远程服务,并使用 Streamable HTTP 传输、OAuth 2.1 + PKCE 认证、RFC 8707 资源指示符(resource indicator)以及无服务器平台部署完成上线。适合已经写过本地 MCP 工具、希望将其产品化或对企业/多租户场景开放的工程师。
stdio MCP 服务器(TypeScript 或 Python)@modelcontextprotocol/sdk 或 mcp/fastmcp)本地 stdio 传输是 MCP 的入门形态:客户端启动一个子进程,通过标准输入输出交换 JSON-RPC 消息。它简单、零网络成本,但有三条硬限制使其无法支撑生产场景:
| 维度 | stdio 本地传输 |
Streamable HTTP 远程传输 |
|---|---|---|
| 部署位置 | 调用方本机,每台机器一份 | 云端单实例,全球可达 |
| 凭证安全 | API Key 必须下发到每个客户端 | 凭证留在服务器,客户端只持有短期 token |
| 多租户/计费 | 无法实现 | 可按用户、按调用计费 |
| 状态与会话 | 进程级,重启即丢 | 可挂载 Redis/DB 做有状态会话 |
| 连接模型 | 一对一子进程 | 单端点服务任意并发客户端 |
一句话原则:只要你的 MCP 工具会接触到付费 API、企业内网数据或需要按用户授权,就必须走远程传输 + OAuth。 Anthropic、Cloudflare、微软等厂商在 2025 年下半年共同确立了以 Streamable HTTP 取代旧版 SSE 作为官方远程传输标准,OAuth 2.1 则是强制的认证层。
早期 MCP 远程方案使用一对 SSE 长连接 + POST 端点,存在连接数膨胀、断线重连复杂、不利于无服务器冷启动等问题。Streamable HTTP(2025-03 版规范引入,2025-06 版成为推荐)将其统一为:
/mcp):客户端 POST JSON-RPC 请求,服务器可选择返回普通 JSON(对非流式请求)或 Content-Type: text/event-stream(对需要流式的请求)。Mcp-Session-Id 颁发会话 ID,客户端在后续请求中回传,从而支持有状态交互;无服务器场景可关闭会话走无状态模式。Last-Event-ID 头重连续传,天然适配边缘网络抖动。这套模型让 MCP 服务器本质上就是一个"能说 JSON-RPC 的普通 Web 服务",因此可以无痛地部署到任何 HTTP 运行时。
flowchart LR
subgraph Client["Claude Code 客户端"]
A["/mcp add --transport http"]
end
subgraph Edge["边缘/网关层"]
B["HTTPS 终结<br/>OAuth 2.1 中间件"]
end
subgraph Server["MCP 服务器 (Streamable HTTP)"]
C["/mcp 端点<br/>JSON-RPC over HTTP/SSE"]
D["Tools / Resources / Prompts"]
end
subgraph Back["后端依赖"]
E["业务 API / DB"]
F["Token / 计费服务"]
end
A -->|POST JSON-RPC<br/>Bearer token| B
B --> C
C --> D
D --> E
B -.校验 token.-> F
以 TypeScript + 官方 SDK 为例。初始化项目:
mkdir remote-mcp && cd remote-mcp
npm init -y
npm install @modelcontextprotocol/sdk zexpress express
创建 src/server.ts,关键在于用 StreamableHTTPServerTransport 替代本地开发时用的 stdio 传输:
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
const app = express();
app.use(express.json());
// 1) 创建 MCP 服务器,声明能力
const server = new McpServer({
name: "invoice-mcp",
version: "1.0.0",
});
// 2) 注册一个工具:按用户身份查询发票
server.tool(
"list-invoices",
{ month: z.string().describe("YYYY-MM 格式的月份") },
async ({ month }, extra) => {
// extra.authInfo 由 OAuth 中间件注入(见第四节)
const user = extra.authInfo?.extra?.sub ?? "anonymous";
const invoices = await db.query(
"SELECT * FROM invoices WHERE owner = ? AND month = ?",
[user, month]
);
return {
content: [{ type: "text", text: JSON.stringify(invoices) }],
};
}
);
// 3) 挂载 Streamable HTTP 传输,支持无状态模式(适合无服务器)
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // 无状态:每次请求独立
});
await server.connect(transport);
await transport.handleRequest(req, res);
});
app.listen(3000, () => console.log("MCP on :3000/mcp"));
预期结果:启动后,对 http://localhost:3000/mcp 发起一个带 Accept: application/json, text/event-stream 的 POST,带上 {"jsonrpc":"2.0","id":1,"method":"initialize",...},应返回 200 与服务器能力声明。
常见问题:
- 报错 No transport found:忘记在 /mcp 路径实例化 StreamableHTTPServerTransport,或者把它和 stdio 混用了。
- 请求被 404/挂起:客户端必须同时声明 Accept: application/json, text/event-stream,否则服务器无法决定用哪种响应体。
- 无服务器冷启动超时:开启 sessionIdGenerator: undefined 走无状态模式,避免 Lambda 中维护会话映射。
MCP 规范要求远程服务器对外暴露 OAuth 2.1 Protected Resource Metadata(RFC 9728 衍生)以及兼容 OAuth 2.0 Authorization Server Metadata(RFC 8414)。流程要点:
resource=https://your-mcp.example.com/mcp,token 只能用于该资源,防止跨服务 token 误用。sequenceDiagram
participant CC as Claude Code
participant MCP as /mcp (Protected Resource)
participant AS as 授权服务器 (Auth0/Keycloak)
CC->>MCP: POST /mcp (无 token)
MCP-->>CC: 401 + resource_metadata URL
CC->>AS: GET /.well-known/oauth-authorization-server
AS-->>CC: {authorize_url, token_url, ...}
CC->>AS: GET /authorize?response_type=code&code_challenge=...&resource=<mcp>
Note over CC,AS: 浏览器打开登录页,用户授权
AS-->>CC: 302 回调 + authorization code
CC->>AS: POST /token (code + code_verifier)
AS-->>CC: access_token (短期) + refresh_token
CC->>MCP: POST /mcp (Authorization: Bearer <token>)
MCP-->>CC: 200 JSON-RPC 响应
下面是一个最小化的 Express 中间件,校验 Bearer token 并把用户身份注入 authInfo:
import jwt from "jsonwebtoken";
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(new URL(process.env.JWKS_URL!));
async function oauthMiddleware(req, res, next) {
// 1) 暴露 Protected Resource Metadata(规范要求)
if (req.path === "/.well-known/oauth-protected-resource") {
return res.json({
resource: "https://mcp.example.com/mcp",
authorization_servers: ["https://auth.example.com"],
bearer_methods_supported: ["header"],
});
}
// 2) 校验 Bearer token
const auth = req.headers.authorization || "";
if (!auth.startsWith("Bearer ")) {
return res
.status(401)
.header("WWW-Authenticate", `Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"`)
.json({ error: "missing_token" });
}
try {
const { payload } = await jwtVerify(auth.slice(7), JWKS, {
issuer: "https://auth.example.com",
audience: "https://mcp.example.com/mcp", // RFC 8707 resource
});
req.authInfo = {
token: auth.slice(7),
clientId: payload.client_id,
scopes: (payload.scope || "").split(" "),
extra: { sub: payload.sub, plan: payload.plan },
};
next();
} catch (e) {
return res.status(401).json({ error: "invalid_token" });
}
}
app.use(oauthMiddleware); // 必须挂在 /mcp 之前
随后在工具实现里通过 extra.authInfo.extra.sub 拿到用户 ID、用 extra.authInfo.extra.plan 做套餐限流。
常见问题:
- 浏览器登录后无限重定向:回调地址在授权服务器注册的 redirect_uri 与 Claude Code 实际使用的 http://localhost:<port>/callback 不一致,需在 Auth0/Keycloak 里登记本地回调(Claude Code 用本地 loopback 接收 code)。
- token 校验通过但 scope 不足:用 extra.authInfo.scopes.includes("invoices:read") 做细粒度判断,未命中则返回 jsonrpc 的 error 对象(code -32603)。
- resource indicator 报 invalid_target:授权服务器的客户端配置没有启用 RFC 8707;在 Auth0 中需在 API 设置打开 Allow Skipping Consent 之外还要允许 resource 参数。
Streamable HTTP 的无状态模式天然适配 FaaS。以 Cloudflare Workers 为例(最省心,自带边缘 HTTPS 与零冷启动):
npm create cloudflare@latest remote-mcp -- --type=hello-world
cd remote-mcp
npm install @modelcontextprotocol/sdk workers-oauth-provider
src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { MyWorkerAuth } from "./auth";
const authProvider = new MyWorkerAuth();
export default {
async fetch(req: Request, env: Env) {
const url = new URL(req.url);
if (url.pathname === "/.well-known/oauth-protected-resource") {
return Response.json(authProvider.resourceMetadata());
}
if (url.pathname === "/authorize" || url.pathname === "/token") {
return authProvider.handle(req, env); // 内置授权码 + token 端点
}
if (url.pathname === "/mcp") {
const token = req.headers.get("authorization")?.slice(7);
const identity = await authProvider.verify(token, env);
if (!identity) return new Response("unauthorized", { status: 401 });
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
const server = new McpServer({ name: "invoice-mcp", version: "1.0.0" });
// 注入身份到每个工具
registerTools(server, identity);
await server.connect(transport);
return transport.handleRequest(req, new Response());
}
return new Response("not found", { status: 404 });
},
};
部署与绑定自定义域:
npx wrangler deploy
npx wrangler domains attach mcp.example.com
在 AWS Lambda + API Gateway 上同理:把 app(Express/Fastify)用 serverless-http 包一层,开启 HTTP API 的 payload format v2,并确保响应里保留 Mcp-Session-Id 头(Lambda 默认会透传自定义头,但 API Gateway 需在"响应头"里放行)。
常见问题:
- Workers 报 "subrequest limit":工具实现里对后端 API 的调用要在 50 个子请求配额内;可改用批量接口或在 Workers 内做缓存。
- Vercel Edge 函数 30 秒超时:长耗时工具拆成"启动 + 轮询"两个工具,避免单次请求超时。
- 回调地址在无服务器下无法接收:Claude Code 的 PKCE 回调始终落在本地 loopback,与服务器部署位置无关;服务器侧只需实现 /authorize、/token 两个端点。
远程服务器就绪后,在 Claude Code 里注册:
# 方式一:HTTP 传输(无认证或自定义头)
claude mcp add --transport http invoice https://mcp.example.com/mcp
# 方式二:需要 OAuth 时,Claude Code 会自动触发浏览器登录
claude mcp add --transport http invoice https://mcp.example.com/mcp --scope user
首次调用 invoice 工具时,Claude Code 检测到 401 + resource metadata,自动打开浏览器完成 PKCE 授权,token 以加密形式存在本地钥匙串(macOS Keychain / Windows Credential Manager),并在过期前静默刷新。
验证连接:
claude mcp list # 应显示 invoice (http) ✓ connected
claude mcp get invoice # 查看工具列表与 token 状态
在会话里输入 "列出我本月发票",Claude Code 会调用 list-invoices,自动在请求头附上 Bearer token,并以你的 sub 作为查询条件——这就是远程 MCP 的核心价值:能力在云端,身份在本地,凭证永不离开服务器。
将 MCP 工具变成对外产品时,三个能力必须就位:
extra.authInfo.extra.sub,并在 DB 层加行级安全策略(PostgreSQL RLS)兜底,防止工具实现漏写条件。tool_name 维度写入计量表;超额时返回 JSON-RPC error code -32001(自定义资源耗尽),Claude Code 会把错误信息透传给用户。{sub, tool, args, latency, status} 结构化写入对象存储,便于合规与问题回溯。计费中间件示例:
async function meteringMiddleware(req, res, next) {
const start = Date.now();
res.on("finish", async () => {
const toolName = req.__mcp_tool; // 由传输层钩子填充
await env.METERING.put(
`${req.authInfo.extra.sub}:${Date.now()}`,
JSON.stringify({ tool: toolName, ms: Date.now() - start, status: res.statusCode })
);
});
if (await isOverQuota(req.authInfo.extra.sub)) {
return res.status(200).json({
jsonrpc: "2.0", id: req.body.id,
error: { code: -32001, message: "monthly quota exceeded" },
});
}
next();
}
把以上步骤串联成一个最小可运行项目 invoice-mcp,它对外提供两个工具:
list-invoices(month):列出当前登录用户的发票download-invoice(id):返回某张发票的 PDF 下载链接项目结构:
invoice-mcp/
├── src/
│ ├── server.ts # MCP server + Streamable HTTP
│ ├── auth.ts # OAuth 2.1 中间件 (JWKS 校验)
│ ├── metering.ts # 计费 / 配额
│ └── tools.ts # 两个业务工具
├── wrangler.toml # Cloudflare Workers 配置
└── .well-known/
└── oauth-protected-resource.json
端到端验证脚本(不依赖 Claude Code,用 mcp-cli 直连):
# 1) 部署
npx wrangler deploy
# 2) 用 mcp-cli 走完整 OAuth
npx @modelcontextprotocol/cli@latest \
--transport http https://mcp.example.com/mcp \
--tool list-invoices --arg month=2026-06
预期输出是一段 JSON,内含属于当前授权用户的发票数组;换一个用户授权后,结果集随之改变——这证明身份隔离生效。
| 症状 | 根因 | 解决 |
|---|---|---|
| 客户端一直卡在 "Discovering auth server" | metadata 端点返回的 authorization_servers 与实际 issuer 不一致 |
校对 /.well-known/oauth-protected-resource 的域名 |
| token 有效但调用返回 invalid_target | 未传 RFC 8707 resource 参数 |
在 Auth0/Keycloak 客户端启用 resource indicator |
| Workers 下偶发 502 | 长连接被 FaaS 砍断 | 关闭会话(sessionIdGenerator: undefined),改无状态 |
| 工具返回别人数据 | 缺少租户隔离 WHERE | 数据库层加 RLS,工具层强制注入 sub |
flowchart TB
User["最终用户"]
CC["Claude Code<br/>(PKCE + 本地 token 存储)"]
subgraph Cloud["云端 (mcp.example.com)"]
GW["Cloudflare Workers<br/>HTTPS + 边缘"]
AS["/authorize /token<br/>OAuth 2.1"]
MWP["/.well-known/<br/>resource metadata"]
MCP["/mcp Streamable HTTP"]
Tools["list-invoices<br/>download-invoice"]
Auth["JWKS 校验中间件"]
Meter["计费 + 配额"]
end
DB[("PostgreSQL + RLS<br/>租户隔离")]
Usage[("计量表 / 对象存储")]
User --> CC
CC -->|1 探测| MWP
CC -->|2 PKCE 授权| AS
CC -->|3 Bearer 调用| GW
GW --> Auth --> Meter --> MCP --> Tools --> DB
Meter --> Usage
stdio MCP 工具开发、OAuth 基础原理两门课程。[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
