跳到主要内容

用 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/sdk1.29.0(v1.x 仍是当前 production 推荐,v2 待 2026 Q1 GA)
hono + @hono/node-serverlatest
MCP transportStreamable HTTP(替代了 2024-11-05 spec 里已 deprecated 的 HTTP+SSE 双 endpoint 方案)
Docker(本地构建验证)latest
@cloudbase/clilatest
一个已开通的 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-Id header 返回一个 UUID;client 后续所有请求都要把这个 header 带上。漏带就 400
  • 必须校验 Origin header 防 DNS rebinding(spec 强制要求)。本地开发可以宽松点,生产把 ALLOWED_ORIGINS 配清楚
  • 本地开发只 bind 127.0.0.1,部署到云托管才 bind 0.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 --from=deps /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 --from=builder --chown=mcpsrv:nodejs /app/dist ./dist
COPY --from=builder --chown=mcpsrv:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=mcpsrv:nodejs /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 装全量(含 devDep typescripttsx),编译完用 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 会问你三件事:

  1. 选择环境 ID
  2. 服务名(建议跟项目同名,例如 cloudbase-mcp-demo
  3. 是否启用公网访问(必须选是,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-functionrequireAuth 中间件,把它加到 /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
  1. 在 Cursor 里:设置面板 → MCP → 找到 cloudbase-mcp-demo,状态应该是「Connected」,下面能看到 1 个 tool(query-cloudbase-database)。
  2. 在对话里直接让 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 providedclient 第二次以后的请求漏带了 Mcp-Session-Id header检查 client 是否正确缓存了首次 InitializeResult 响应里的 Mcp-Session-Id;自实现 client 时所有后续请求都要带上
403 forbidden_originOrigin header 不在白名单里(防 DNS rebinding 校验)把 client 的来源(如 https://cursor.shnull)加到 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/,部署/构建阶段的失败可在云托管「服务详情 → 部署历史 → 查看日志」里看到完整堆栈。

相关文档