用 CloudBase 云托管部署 MCP Server
一句话定义:用
@modelcontextprotocol/sdk写一个 MCP Server,Hono 做 HTTP 路由,多阶段 Dockerfile 打包,tcb cloudrun deploy部署到 CloudBase 云托管,Cursor / Claude Code / Windsurf 通过 Streamable HTTP transport 直连访问。预计耗时:30 分钟 | 难度:进阶
适用场景
MCP(Model Context Protocol)是 Anthropic 主导的、给 LLM agent 接入工具/数据源的开放协议。本篇覆盖的场景:你已经有一些业务能力(数据库查询、内部 API、文件检索),想 让 Cursor / Claude Code / Windsurf 等 agent 工具直接调用,又不想把内网账号给到第三方桌面客户端,那就把 MCP Server 跑在你自己的云托管里,agent 通过 HTTPS URL 接入。
- 适用:把内部 API、数据库查询、知识库检索包装成 MCP tool 给团队内部 agent 工具使用
- 适用:需要稳定 endpoint、自定义域名、HTTPS、和其他云资源(数据库、对象存储、云函数)内网互通
- 适用:想让多人共享同一个 MCP Server 而不是每人本地跑一份
- 不适用:纯本地工具(操作本机文件、读本机进程列表),用 stdio transport 跑本地进程更合适
- 不适用:实验性、单次调用的 prompt 工程,写 prompt 模板就够了
- 不适用:调用方只有自家前端、不接 agent 工具的场景,普通 HTTP API 即可
云托管和云函数的区别:MCP Server 是长驻进程、需要保持 session 状态、未来很可能用 SSE 推送 server-initiated 消息——这些场景走云托管更稳。如果只是无状态的 tool 调用,且能接受冷启动,云函数也能跑。
环境要求
| 依赖 | 版本 |
|---|---|
| Node.js(本地开发 + 镜像内运行时) | ≥ 20 |
@modelcontextprotocol/sdk | 1.29.0(v1.x 仍是当前 production 推荐,v2 待 2026 Q1 GA) |
hono + @hono/node-server | latest |
| MCP transport | Streamable HTTP(替代了 2024-11-05 spec 里已 deprecated 的 HTTP+SSE 双 endpoint 方案) |
| Docker(本地构建验证) | latest |
@cloudbase/cli | latest |
| 一个已开通的 CloudBase 环境 | 含云托管能力 |
关于 transport 选择多说一句:Streamable HTTP 把 server 缩成单一 endpoint URL(如 https://example.com/mcp),同时支持 POST(client → server 请求)和 GET(server → client 流式推送),通过 Mcp-Session-Id header 维持会话。这比老的 HTTP+SSE 方案(两个 endpoint、外加 query 参数串 session)更简单也更适合放在 CDN/网关后面。本篇全程只用 Streamable HTTP。
第一步:写 MCP Server 代码
新建项目:
mkdir cloudbase-mcp-demo && cd cloudbase-mcp-demo
npm init -y
npm install @modelcontextprotocol/sdk@1.29.0 hono @hono/node-server zod
npm install --save-dev typescript @types/node tsx
package.json 改成 ESM 工程,并补 start 脚本:
{
"name": "cloudbase-mcp-demo",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
}
}
tsconfig.json(最小可行):
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src"]
}
src/index.ts —— 一个示例 MCP Server,注册一个 query-cloudbase-database tool 演示用法(实际上你 换成自己的业务即可):
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { randomUUID } from "node:crypto";
import { z } from "zod";
// 1. 创建 MCP Server 实例并声明能力
const mcpServer = new Server(
{ name: "cloudbase-mcp-demo", version: "0.1.0" },
{ capabilities: { tools: {} } },
);
// 2. 列出本服务暴露的 tool
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "query-cloudbase-database",
description: "Query the CloudBase database collection by a where clause.",
inputSchema: {
type: "object",
properties: {
collection: { type: "string", description: "Collection name" },
where: { type: "object", description: "Filter conditions" },
},
required: ["collection"],
},
},
],
}));
// 3. 处理 tool 调用
const QueryArgs = z.object({
collection: z.string(),
where: z.record(z.unknown()).optional(),
});
mcpServer.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name !== "query-cloudbase-database") {
throw new Error(`Unknown tool: ${req.params.name}`);
}
const args = QueryArgs.parse(req.params.arguments ?? {});
// TODO: 这里换成 @cloudbase/node-sdk 真实查询;示例只回显参数
return {
content: [
{
type: "text",
text: `Would query collection=${args.collection} where=${JSON.stringify(args.where ?? {})}`,
},
],
};
});
// 4. 用 Hono 起 HTTP 路由,把 /mcp 转给 Streamable HTTP transport
const app = new Hono();
// 一个 transport 实例对应一个 client session
const transports = new Map<string, StreamableHTTPServerTransport>();
app.all("/mcp", async (c) => {
// 防 DNS rebinding:必须校验 Origin
const origin = c.req.header("origin");
const allowed = (process.env.ALLOWED_ORIGINS ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
if (origin && allowed.length > 0 && !allowed.includes(origin)) {
return c.json({ error: "forbidden_origin" }, 403);
}
const sessionId = c.req.header("mcp-session-id");
let transport = sessionId ? transports.get(sessionId) : undefined;
if (!transport) {
// 首次请求(InitializeRequest)时新建 transport,并生成 session id
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => transports.set(id, transport!),
});
transport.onclose = () => {
if (transport!.sessionId) transports.delete(transport!.sessionId);
};
await mcpServer.connect(transport);
}
// Hono 的 raw req/res 直接交给 transport
await transport.handleRequest(c.env.incoming, c.env.outgoing, await c.req.json().catch(() => undefined));
return c.body(null);
});
app.get("/health", (c) => c.json({ ok: true }));
const port = Number(process.env.PORT ?? 3000);
const hostname = process.env.HOSTNAME ?? "0.0.0.0";
serve({ fetch: app.fetch, port, hostname });
console.log(`MCP server listening on ${hostname}:${port}/mcp`);
几个关键点:
Server+StreamableHTTPServerTransport是 SDK 1.x 的标准组合;官方在 spec 2025-03-26 之后把老的 HTTP+SSE 双 endpoint 方案 deprecate 了,新项目直接用 Streamable HTTP- session id 由 server 生成:client 第一次发
InitializeRequest,server 在响应里通过Mcp-Session-Idheader 返回一个 UUID;client 后续所有请求都要把这个 header 带上。漏带就 400 - 必须校验
Originheader 防 DNS rebinding(spec 强制要求)。本地开发可以宽松点,生产把ALLOWED_ORIGINS配清楚 - 本地开发只 bind
127.0.0.1,部署到云托管才 bind0.0.0.0——下文 Dockerfile 处理这块 - 协议版本 header 是
MCP-Protocol-Version: 2025-06-18,SDK 会自动协商,业务代码不用关心
第二步:写多阶段 Dockerfile
项目根目录新建 Dockerfile:
# ===== Stage 1: deps =====
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# ===== Stage 2: builder =====
FROM node:20-alpine AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
# 仅保留生产依赖
RUN npm prune --omit=dev
# ===== Stage 3: runner =====
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
# 创建非 root 用户运行
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 mcpsrv
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
COPY /app/package.json ./package.json
USER mcpsrv
EXPOSE 3000
CMD ["node", "dist/index.js"]
几个关键细节:
HOSTNAME=0.0.0.0必须加,Node 默认监听localhost,容器外访问不到——这是云托管最常见的部署故障EXPOSE 3000/PORT=3000/tcb cloudrun deploy --port 3000三处必须一致- 构建阶段用
npm ci装全量(含 devDeptypescript、tsx),编译完用npm prune --omit=dev砍掉 dev 依赖再 COPY 进 runner,镜像更小 --chown=mcpsrv:nodejs给非 root 用户读权限,漏了会报EACCES
本地验证镜像能跑通:
docker build -t cloudbase-mcp-demo:local .
docker run -p 3000:3000 -e ALLOWED_ORIGINS=http://localhost:3000 cloudbase-mcp-demo:local
# 另开一个终端
curl -i http://localhost:3000/health
# 预期: HTTP/1.1 200 OK
第三步:tcb cloudrun deploy 部署
CI/本地都用 CLI 一行部署:
# 本地交互登录
tcb login
# 或者 CI 环境用 API key
tcb login --apiKeyId <你的 API key id> --apiKey <你的 API key>
tcb cloudrun deploy --port 3000
CLI 会问你三件事:
- 选择环境 ID
- 服务名(建议跟项目同名,例如
cloudbase-mcp-demo) - 是否启用公网访问(必须选是,agent 工具要从公网接入)
CLI 会把当前目录打包上传,触发云端构建(云端用你的 Dockerfile 打镜像并部署)。整个过程一般几分钟。
部署完成后控制台「云托管 → 服务 → 你的服务名」能看到默认域名,形如 https://cloudbase-mcp-demo-abc123.ap-shanghai.app.tcloudbase.com。最终给 client 用的 endpoint 就是这个 + /mcp:
https://cloudbase-mcp-demo-abc123.ap-shanghai.app.tcloudbase.com/mcp
如果要换自定义域名、加鉴权 header 校验、配环境变量(比如 ALLOWED_ORIGINS、上游服务的 API key),步骤跟前一篇 Next.js 部署到云托管 完全一样,不再重复。
第四步:在 Cursor / Claude Code / Windsurf 里配置
三家主流 agent 工具都已经支持 Streamable HTTP transport,直接给 URL 就行。
Cursor:编辑 ~/.cursor/mcp.json(macOS / Linux):
{
"mcpServers": {
"cloudbase-mcp-demo": {
"url": "https://cloudbase-mcp-demo-abc123.ap-shanghai.app.tcloudbase.com/mcp",
"headers": {
"Authorization": "Bearer YOUR_KEY"
}
}
}
}
存盘后 Cursor 设置面板的 MCP 区域会自动重载。
Claude Code:用 CLI 一行加:
claude mcp add --transport http cloudbase-demo \
https://cloudbase-mcp-demo-abc123.ap-shanghai.app.tcloudbase.com/mcp
claude mcp list 能看到新增的 server,/mcp 命令进入交互模式查看 tool 列表。
Windsurf:编辑 ~/.codeium/windsurf/mcp_config.json,格式跟 Cursor 完全一样:
{
"mcpServers": {
"cloudbase-mcp-demo": {
"url": "https://cloudbase-mcp-demo-abc123.ap-shanghai.app.tcloudbase.com/mcp",
"headers": {
"Authorization": "Bearer YOUR_KEY"
}
}
}
}
Authorization header 是可选的——如果你的 server 端代码加了 token 校验(参考 connect-openai-api-cloud-function 的 requireAuth 中间件,把它加到 /mcp 路由上),这里就要带上对应的 token;本篇示例 server 没做鉴权,仅靠 Origin 校验,生产环境一定要加 token。
运行验证
部署完后跑这几个:
# 1. 健康检查
curl -i https://cloudbase-mcp-demo-abc123.ap-shanghai.app.tcloudbase.com/health
# 预期: HTTP/2 200, body {"ok":true}
# 2. MCP 初始化握手 - 模拟 client 发 InitializeRequest
curl -i -X POST 'https://cloudbase-mcp-demo-abc123.ap-shanghai.app.tcloudbase.com/mcp' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-06-18' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "curl", "version": "0.1" }
}
}'
# 预期: 响应 header 里有 Mcp-Session-Id: <uuid>,body 是 InitializeResult JSON
- 在 Cursor 里:设置面板 → MCP → 找到
cloudbase-mcp-demo,状态应该是「Connected」,下面能看到 1 个 tool(query-cloudbase-database)。 - 在对话里直接让 agent 调用:「请用 query-cloudbase-database 查
collection=users where={"status":"active"}」——agent 会调用 tool,server 端在云托管「服务详情 → 日志」里能看到对应请求。
常见错误
| 错误信息 | 原因 | 修复 |
|---|---|---|
| 服务部署成功但 client 一直连不上、curl 也 timeout | 容器内进程 bind 在 127.0.0.1,容器外访问不到 | Dockerfile 加 ENV HOSTNAME=0.0.0.0,代码里读 process.env.HOSTNAME 传给 serve() |
400 Bad Request: No valid session ID provided | client 第二次以后的请求漏带了 Mcp-Session-Id header | 检查 client 是否正确缓存了首次 InitializeResult 响应里的 Mcp-Session-Id;自实现 client 时所有后续请求都要带上 |
403 forbidden_origin | Origin header 不在白名单里(防 DNS rebinding 校验) | 把 client 的来源(如 https://cursor.sh 或 null)加到 ALLOWED_ORIGINS 环境变量;浏览器 / 桌面 client 行为不同,本地调试时也能临时清空白名单 |
流式响应在浏览器/agent 里被截断、出现 Connection closed | 中间某层(CDN / 自定义域名网关)开了响应缓冲或超时太短 | 自定义域名配置里关掉 buffering;云托管「服务设置 → 超时时间」按需调长(默认 30s,对 MCP 长连接通常要 ≥ 300s) |
404 Not Found 在 /mcp | 路由写错或 Hono 没匹配到方法 | 确认是 app.all("/mcp", ...) 而不是 app.post;Streamable HTTP 同时用 POST 和 GET |
EACCES: permission denied, open '/app/...' | Dockerfile 里 COPY 没加 --chown,非 root 用户读不了 | 所有 COPY --from=builder 都加 --chown=mcpsrv:nodejs |
| Cursor MCP 面板红色 Failed,无具体错误 | TLS 证书问题 / URL 末尾多/少 / / 公网访问没开 | 先 curl -i 确认 endpoint 可达;URL 必须 /mcp 结尾不带斜杠; 控制台确认服务的「公网访问」是开启状态 |
错误码定义参考 https://docs.cloudbase.net/error-code/,部署/构建阶段的失败可在云托管「服务详情 → 部署历史 → 查看日志」里看到完整堆栈。
相关文档
- 把 Next.js 14+ App Router 应用部署到 CloudBase 云托管 — 多阶段 Dockerfile /
tcb cloudrun deploy命令的更详细版本,本篇假设你已经熟悉 - 用云函数代理 OpenAI / Anthropic 等海外 LLM API — 对比版:把 LLM API 代理放到云函数(短任务、按请求计费),跟本篇的 MCP Server(长驻进程)正好是云函数 vs 云托管两个方向