跳至正文
来两杯美式
返回

当 MCP Server 要"嵌进"对外平台:Node.js + 官方 SDK + API Key 的选型逻辑

By 来两杯美式
发布于更新于

这篇接着讲 选型总览 里的”工具平台”——那个嵌在 Next.js 里、对外多租户、既给自研 Agent 也给 Claude Desktop / Cursor 这类外部客户端用的 MCP Server。

版本说明(2026-08):下面的 sessionIdGenerator、POST/GET/DELETE 和进程内 Session 是项目基于 MCP 2025 兼容协议的历史实现。MCP 2026-07-28 已移除 Streamable HTTP 的 GET 端点与协议级 Session;新实现应优先采用只使用 POST 的无状态协议。

它和能力适配器(Python 栈那篇)是两个极端:那个是独立容器、翻译现成能力;这个是要嵌进现有平台、要细粒度权限、要审计、要应对不可控的外部调用方。所以选型重心完全不同——这边几乎不用高级框架,而是用官方低层 SDK 把每个工程化环节做扎实。

协议:为什么是 MCP

先回答最底层的问题:为什么用 MCP,而不是直接 REST API 或者 OpenAI Function Calling。

方案优势劣势
MCP标准化工具发现(tools/list)、Schema 驱动、官方 SDK、生态兼容相对新,生态发展中
自定义 REST API简单直接、完全可控无工具发现、每个工具各写一套路由
OpenAI Function Calling与 LLM 直接集成绑定 OpenAI 生态,非通用协议
gRPC + Protobuf强类型、高性能工具发现要自定义、Agent 生态支持弱

选 MCP 的决定性理由是工具发现生态兼容:外部客户端(Claude Desktop、Cursor、VS Code Copilot)原生支持 MCP,协议层通常不需要再写自定义适配器,但仍要配置服务地址和认证,并验证客户端支持的协议版本与能力;而 tools/list 让 Agent 自动发现可用工具和参数,不用人肉翻文档。

顺带一个设计:我们同时暴露了两种入口——MCP(/v1/mcp,给外部 MCP 客户端)和 REST Skill API(给自研 Agent、定时任务、CLI 这类不方便走 MCP 协议的调用方)。同一个工具,两种协议入口,底层逻辑共享。这避免了”为了 MCP 放弃内部 REST 调用”或”为了 REST 重写一套”。

SDK:v1.29,稳定优先

用的是 @modelcontextprotocol/sdk 1.29,而 2.0 当时还在 beta。

选 v1 的理由和 Python 栈不追 FastMCP v3 是一个逻辑:

v2 的包拆分、多框架适配器,以及面向 2026 无状态协议的迁移能力听着不错,但对当前单实例部署没有直接收益,而且我们已经在 SDK 之上加了一层封装(createMcpServer),包拆分的收益进一步被吃掉。

同样的规律:SDK 版本稳定优先 + 封装隔离。v2 等它正式发布、且某个特性真的解决了当前问题,再评估升级。

传输:Streamable HTTP 单端点

传输选了 Streamable HTTP,具体用的是 WebStandardStreamableHTTPServerTransport

const transport = new WebStandardStreamableHTTPServerTransport({
  sessionIdGenerator: crypto.randomUUID,
  onsessioninitialized: id => {
    sessionStore.set(id, transport, apiKeyId);
  },
});

在项目当时采用的 2025 兼容协议里,POST / GET / DELETE 共用一个 /v1/mcp 路由,分别承担请求、SSE 流和关闭会话。对比旧式 HTTP+SSE 的双端点,这种路由更集中;但它仍是会话式实现,不是 2026-07-28 的当前无状态协议。

如果从零实现当前协议,应移除 sessionIdGenerator、GET/DELETE 路由和 Mcp-Session-Id 依赖,只保留 POST。应用确实需要跨调用状态时,返回显式的任务或资源 ID,由客户端在后续工具参数中传回。

请求体大小限了 1MB(MAX_BODY_BYTES),防止异常大请求把服务拖垮。

传输选型本身(SSE vs Streamable-HTTP)的完整对比,我放到了 第四篇,这里只讲落地形态。

认证:API Key 独占

认证只接受 API Key,没用 OAuth,也没用 Session Cookie。

方案优势劣势
API Key适合机器间调用、与 REST 认证统一无用户身份绑定
OAuth 2.0标准化、支持动态注册实现复杂、对客户端要求高
Session Cookie支持 Web 用户MCP 客户端通常不支持 Cookie

API Key 是 Agent 场景最自然的认证方式——它就是个机器凭证,和内部 REST API 的认证模型统一,每个 Key 还能配独立 scope。

认证流程是这样的:

Client Request
  → hashToken(rawToken)            // SHA-256,不存明文
  → resolveApiKeyUserByTokenHash   // Redis 缓存命中直接返回
                                   // 未命中 → 查库 + 回填缓存
  → 检查 revokedAt / expiresAt     // 是否吊销、是否过期
  → { apiKeyUser }                 // 通过

两个细节值得点出来:token 只存 SHA-256 哈希(泄露了库也拿不到原文),解析结果走 Redis 缓存(避免每次请求都查库)。还有一步会话归属校验——比对当前 sessionId 对应的 apiKeyId 和本次请求的 API Key,防止 A 的 Key 去复用 B 的会话。

权限:Scope-gated 工具注册

这一块是工具平台的精髓,但它和”权限码怎么设计”是两件事。这篇只讲机制层面:每个 API Key 只注册它被授权的工具集

// 1. 查这个 API Key 有哪些 mcp-tool scope
const toolScopes = await db.apiKeyScope.findMany({
  where: { apiKeyId, resourceType: "mcp-tool" },
});

// 2. 只注册被授权的工具集
for (const scope of toolScopes) {
  SCOPE_TOOL_MAP[scope.resourceId]?.(server, ctx);
}

默认拒绝:没在 scope 里的工具,McpServer 根本不注册,Agent 连发现都发现不了。这比”注册全部工具再在调用时拦截”安全得多——工具从一开始就不暴露。

至于 scope 内部怎么按 read / write / dangerous 分级、高风险动作怎么走审批状态机,那是权限码设计的事,我专门写过:

MCP 权限怎么分级?只读、可操作和高风险动作MCP 工具权限码怎么设计?read、write、dangerous 三层模型

会话:进程内存为什么够用

会话存进程内存,用一个 McpSessionStore:Map + 30 分钟 TTL(无活动自动清理)+ 5 分钟清理周期。

这里有个 Next.js 特有的坑:开发模式热更新(HMR)会让模块重新加载,可能产生多个 sessionStore 实例。解法是把单例挂在 globalThis 上,跨 HMR 复用同一个实例。

为什么敢用进程内存,不怕水平扩展?因为当前是单实例部署

已知限制也清楚:这套 2025 兼容实现不支持多实例(需要 sticky session 或共享会话存储),进程重启会丢会话。2026-07-28 协议已经提供了无状态升级方向,但它包含破坏性变化,不能假设只升级依赖就会自动兼容;需要同步改路由、握手与客户端协商。

会话存哪,看规模。单实例够用就别引入外部存储——这是”按当前规模选最简方案”的典型例子,但要同时看清升级路径。

审计:先可靠接收,再异步落库

因为是对外服务,每次工具调用都要留痕。做法是给每个工具 handler 包一层 withMcpCallLogging

export function withMcpCallLogging(toolName, handler, ctx) {
  return async input => {
    const start = Date.now();
    try {
      const result = await handler(input);
      await enqueueAgentCallAudit(meta, {
        channel: "mcp",
        toolName,
        input,
        resultStatus: result.isError ? "error" : "success",
        durationMs: Date.now() - start,
      });
      return result;
    } catch (err) {
      await enqueueAgentCallAudit(meta, {/* ... */});
      throw err;
    }
  };
}

关键设计不是 fire-and-forget,而是先确认审计事件已被可靠接收,再把慢速写库异步化enqueueAgentCallAudit 可以写入持久消息队列、事务 Outbox 或具备确认机制的日志管道;只有拿到接收确认后才返回工具结果。若合规要求“每次调用都留痕”,队列不可用时就应拒绝高风险调用或进入明确的降级状态,不能只写 console.error 后继续。

日志保留期应由实际合规与业务政策确定;90 天只是本项目配置,不是通用标准。入参出参超 4096 字符会截断,因此还要记录摘要、对象 ID 或受控归档位置,避免关键证据丢失。进入异步队列前,应把 IP、UA、requestId、调用者和权限判定等必要字段复制成普通可序列化对象,不依赖框架请求对象的生命周期。

错误处理:协议层与工具层分离

错误分两层,别混:

传输层先使用 HTTP 状态码:缺少或无效凭证返回 401,权限不足返回 403,请求体过大返回 413。进入 JSON-RPC 后,再使用标准错误码:

错误码标准含义
-32700Parse error,JSON 解析失败
-32600Invalid Request,请求对象无效
-32601Method not found,方法不存在
-32602Invalid params,参数无效
-32603Internal error,内部错误

-32000-32099 是服务端自定义错误区间。如果项目用 -32000 表示旧协议会话无效,必须在接口文档中注明它是应用约定;认证失败则优先在 HTTP 层返回 401/403,不要把自定义 -32001 写成 JSON-RPC 标准定义。

工具层用 MCP 的 isError: true,把业务错误包在 content 里返回,不抛异常打断协议:

catch (err) {
  return {
    content: [{ type: "text", text: `发送异常:${msg}` }],
    isError: true,
  };
}

还有个挺有意思的业务保护:update_user_memory 工具有个合并保护——当新内容长度小于已有记忆的 50%(且已有记忆 ≥ 100 字符),拒绝写入并返回 merge_required。这是防止 Agent 拿一小段内容把一大段已有记忆整个覆盖掉。

架构总览

把上面拼起来,整个 MCP 子系统长这样:

            AI Agent / MCP Client
                  │  JSON-RPC 2.0 over Streamable HTTP

         /v1/mcp  (Next.js Route Handler)

      ┌───────────┼───────────┐
      ▼           ▼           ▼
   Auth        Scope        Session
  (API Key)   (Redis)     (In-Memory)


         McpServer (per-session)
   registerToolsForScopes + withMcpCallLogging

      ┌───────────┼───────────┐
      ▼           ▼           ▼
  Message     Memory      Agent Log
  Service    Service    (durable audit queue)

每一层都对应前面讲的一个选型决策——这张图本身就是这份选型的总结。

动手验证

这篇的载体是 agent-runtime-lab,可以实际跑起来:

AI 工程化相关阅读


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
查看系列全部文章
  1. 01.MCP Server 选型决策树:从语言到审计,六个维度一次说清
  2. 02.当 MCP Server 只是"翻译"现成能力:FastMCP + Mem0 + Redis 的选型逻辑
  3. 03.当 MCP Server 要"嵌进"对外平台:Node.js + 官方 SDK + API Key 的选型逻辑
  4. 04.SSE 已被 MCP 判为 legacy:存量服务怎么迁、新服务怎么选
  5. 05.MCP 权限怎么分级?只读、可操作和高风险动作(附完整源码)
  6. 06.MCP 工具权限码怎么设计?read、write、dangerous 三层模型(附完整源码)
  7. 07.自建 SearXNG + MCP:给 AI 工具接入可控的网页搜索
  8. 08.Agent Skill 是快照,快照会腐烂:我把业务流程全部搬到了服务端
  9. 09.能力目录与协议下发:把接口设计和控制流一起交给 Agent
  10. 10.四套版本号:把 Agent Skill 的版本管理挪回服务端之后
  11. 11.让用户自定义 Agent 能力,而不打开注入的后门
  12. 12.MCP 2026-07-28 改了什么:从有状态会话到无状态核心
  13. 13.Anthropic 发布 Agent Skills:AI Agent 的能力该如何被打包
  14. 14.Agent Skills 设计模式:用文件夹给 Agent 装上专业能力
  15. 15.Agent Skills 最佳实践:从评测、结构拆分到安全审查
  16. 16.MCP Server 升级 2026-07-28:不是换依赖,而是重画协议边界
  17. 17.MCP Client 升级 2026-07-28:服务端能留兼容窗口,我们偏不兼容旧协议
  18. 18.MCP 爆炸之后,Agent 怎么从一堆工具里选对那一个

上一篇
MCP Server 选型决策树:从语言到审计,六个维度一次说清
下一篇
当 MCP Server 只是"翻译"现成能力:FastMCP + Mem0 + Redis 的选型逻辑