摘要:TypeScript MCP SDK进阶教程,涵盖高级工具定义、上下文管理、自定义传输、中间件机制和类型安全最佳实践,帮助开发者构建生产级MCP Server。
TypeScript MCP SDK进阶 高级特性与最佳实践
上周有个朋友问我,他用 Python 的 FastMCP 写了个 MCP 服务器,跑得好好的,为啥我非得折腾 TypeScript SDK。原因很简单,我手头的项目前端是 React 全家桶,后端是 Node.js,用 TS 写 MCP 服务器可以跟现有代码库共享类型定义,不用来回切语言。这篇就来聊聊我在实际项目里用 TypeScript MCP SDK v2 踩出来的一些经验和模式。
TypeScript SDK v2 的核心变化
先说个大背景。TypeScript SDK 在 2026 年 7 月跟着 MCP 2026-07-28 规范一起发布了 v2 版本。包名从之前的 @modelcontextprotocol/sdk 拆成了两个独立包,@modelcontextprotocol/server 和 @modelcontextprotocol/client。
最大的变化是 schema 验证引入了 Standard Schema 标准。你可以用 Zod v4、Valibot、ArkType 等任何兼容的库来定义工具参数的 schema,SDK 不再绑死某一种验证器。这对我来说是个好消息,因为项目里已经在用 Zod 了。
先看一个最基本的服务器搭建
// 导入 MCP Server 核心类和 stdio 传输层
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
// 使用 Zod v4 做 schema 验证
import * as z from 'zod/v4';
// 创建服务器实例,指定名称和版本号
// 客户端连接时会拿到这些信息用于识别
const server = new McpServer({
name: 'my-advanced-server',
version: '1.0.0',
});
// 注册一个工具,第二参数是配置对象,第三参数是处理函数
// inputSchema 用 Zod 定义,SDK 自动转换为 JSON Schema 发给客户端
server.registerTool(
'greet',
{
description: '根据名字打招呼',
inputSchema: z.object({
name: z.string().min(1).describe('对方的名字'),
formal: z.boolean().optional().default(false).describe('是否使用正式语气'),
}),
},
async ({ name, formal }) => {
// 处理函数接收的是已经验证过的参数
// 类型由 Zod schema 推断,不用手动写类型
const greeting = formal ? `尊敬的 ${name},您好` : `嗨 ${name}`;
return {
content: [{ type: 'text', text: greeting }],
};
}
);
// 启动服务器,使用 stdio 传输
// stdio 适合本地命令行工具场景
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main();
这里有个我踩过的坑。v2 刚出来的时候,很多教程还在用 server.tool() 方法注册工具,那是 v1 的 API。v2 统一改成了 server.registerTool(),而且参数结构也变了。如果你从 v1 迁移过来,一定注意这个方法名的变化。
类型推断的魔法
TypeScript SDK 最让我爽的一点是类型推断。你看上面那段代码,处理函数的参数 { name, formal } 完全不需要手动声明类型,Zod schema 定义好了之后,TypeScript 自动推断出来。
对比一下 Python 那边。FastMCP 通过函数签名的类型注解来生成 schema,虽然也很方便,但有个限制是你必须在函数签名上写完整的类型。TypeScript 这边可以更灵活地组合 schema
// 定义一个可复用的 schema 片段
// 多个工具可以共享这个 schema
const paginationSchema = z.object({
page: z.number().int().min(1).default(1).describe('页码,从1开始'),
pageSize: z.number().int().min(1).max(100).default(20).describe('每页条数'),
});
// 用 extend 组合出更复杂的 schema
// 这种组合方式在 Python 里要靠 Pydantic 的继承,不如这个灵活
const userListSchema = paginationSchema.extend({
keyword: z.string().optional().describe('搜索关键词'),
status: z.enum(['active', 'inactive', 'banned']).optional().describe('用户状态'),
});
// 注册工具时直接传入组合后的 schema
// 处理函数的参数类型自动推断,包含 page, pageSize, keyword, status
server.registerTool(
'search-users',
{
description: '搜索用户列表,支持分页和过滤',
inputSchema: userListSchema,
},
async (params) => {
// params 类型完全由 schema 推断
// IDE 里自动补全和类型检查都正常工作
const { page, pageSize, keyword, status } = params;
// 模拟数据库查询
const results = await mockDbQuery(page, pageSize, keyword, status);
return {
content: [{ type: 'text', text: JSON.stringify(results) }],
};
}
);
// 模拟数据库查询函数
async function mockDbQuery(
page: number,
pageSize: number,
keyword?: string,
status?: string
) {
return {
page,
pageSize,
total: 42,
items: [{ id: 1, name: 'test' }],
};
}
中间件机制
v2 引入了中间件包的概念。这些中间件包是针对特定运行时或 Web 框架的薄适配器,帮你把 MCP 服务器挂到 Express、Fastify、Hono 或者原生 Node.js HTTP 上。
这里我要分享一个独家踩坑经验。我一开始用 Express 中间件包挂载 MCP 服务器,结果发现客户端连接时一直报 400 错误。查了半天才发现,Express 的 body-parser 默认限制 body 大小为 100kb,而某些包含大量上下文的请求会超过这个限制。解决办法是在 MCP 路由之前设置更大的 body 限制
// 导入 Express 中间件包和 Express 本身
import express from 'express';
import { hostValidation } from '@modelcontextprotocol/express';
const app = express();
// 关键坑点 必须在 MCP 路由之前提高 body 大小限制
// 默认 100kb 对某些包含大量上下文的请求不够用
// 我设成了 10mb,根据你的实际场景调整
app.use(express.json({ limit: '10mb' }));
// 启用 Host header 验证,防止 DNS 重绑定攻击
// 这是中间件包提供的安全功能
app.use(hostValidation());
// MCP 路由挂载点
// 客户端通过 http://localhost:3000/mcp 连接
app.use('/mcp', mcpExpressHandler);
app.listen(3000, () => {
console.log('MCP server running on http://localhost:3000/mcp');
});
对比 Python 这边,FastMCP 也支持 HTTP 传输,但配置方式完全不同。FastMCP 内置了 uvicorn 或 Starlette 的集成,你直接调用 mcp.run(transport="http") 就行。TypeScript 这边需要你自己选 Web 框架,灵活但多了一步配置。
上下文管理
MCP 的请求处理函数可以接收一个上下文对象,里面包含了当前会话的信息和可以调用的方法。在 TypeScript SDK 里,这个上下文是通过处理函数的第二个参数传入的
// 导入 Server 工具上下文类型
import type { ServerToolContext } from '@modelcontextprotocol/server';
server.registerTool(
'process-large-file',
{
description: '处理大文件,支持进度上报和日志',
inputSchema: z.object({
filePath: z.string().describe('文件路径'),
}),
},
async ({ filePath }, context: ServerToolContext) => {
// context 里可以访问到进度上报和日志功能
// 这些是 MCP 协议层面的能力
// 发送日志通知给客户端
// 客户端可以在 UI 上展示这些日志
context.sendLoggingMessage({
level: 'info',
logger: 'file-processor',
data: { message: `开始处理文件 ${filePath}` },
});
// 模拟文件处理过程
const totalLines = 10000;
for (let i = 0; i < totalLines; i++) {
// 每处理 1000 行上报一次进度
if (i % 1000 === 0) {
context.sendLoggingMessage({
level: 'debug',
logger: 'file-processor',
data: { progress: `${i}/${totalLines}` },
});
}
}
return {
content: [{ type: 'text', text: `文件处理完成,共 ${totalLines} 行` }],
};
}
);
这里有个坑我踩了好几次。上下文对象的 sendLoggingMessage 方法只有在客户端先发送了 logging/setLevel 请求之后才会真正工作。如果客户端没设置日志级别,你发的日志通知会被静默忽略。调试的时候很容易以为代码有 bug,其实是客户端那边没开启日志接收。
与 Python SDK 的对比
我两个 SDK 都用过不少,总结一下核心差异
| 维度 | TypeScript SDK v2 | Python FastMCP |
|---|---|---|
| Schema 验证 | Standard Schema 标准,支持 Zod/Valibot/ArkType | 基于 Python 类型注解,底层用 Pydantic |
| 类型推断 | Zod schema 自动推断处理函数参数类型 | 函数签名注解直接作为类型 |
| 传输层 | stdio + Streamable HTTP,中间件适配多框架 | stdio + SSE + HTTP,内置 uvicorn 集成 |
| 运行时 | Node.js / Bun / Deno | CPython 3.10+ |
| 异步模型 | 原生 async/await,事件循环由运行时管理 | asyncio,需要注意事件循环嵌套问题 |
| 包管理 | npm / bun / deno add | pip / uv add |
| 部署体积 | 较小,适合 serverless | 较大,Docker 镜像通常 200MB+ |
我的建议是这样。如果你的项目已经用了 Node.js 或者前端技术栈,选 TypeScript SDK,类型定义可以跨前后端复用。如果你做的是数据分析、机器学习相关的 MCP 服务器,选 Python,生态里有大量现成的库可以直接用。
完整代码
下面是一个完整的 TypeScript MCP 服务器项目,包含工具、资源和提示三大能力,可以直接运行
// file: src/server.ts
// 完整的 MCP 服务器示例,包含工具、资源和提示
// 运行方式: npx tsx src/server.ts
// 依赖安装: npm install @modelcontextprotocol/server zod
// ===== 导入依赖 =====
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
// ===== 创建服务器实例 =====
const server = new McpServer({
name: 'advanced-demo-server',
version: '1.0.0',
});
// ===== 工具部分 =====
// 一个带完整参数校验和错误处理的计算器工具
const calculatorSchema = z.object({
operation: z.enum(['add', 'subtract', 'multiply', 'divide'])
.describe('运算类型'),
a: z.number().describe('第一个操作数'),
b: z.number().describe('第二个操作数'),
});
server.registerTool(
'calculator',
{
description: '四则运算计算器',
inputSchema: calculatorSchema,
},
async ({ operation, a, b }) => {
// 根据运算类型执行对应计算
let result: number;
switch (operation) {
case 'add':
result = a + b;
break;
case 'subtract':
result = a - b;
break;
case 'multiply':
result = a * b;
break;
case 'divide':
// 除法需要处理除零错误
if (b === 0) {
// 返回 isError 为 true 的结果
// 这属于工具执行错误,不是协议错误
return {
content: [{ type: 'text', text: '错误: 除数不能为零' }],
isError: true,
};
}
result = a / b;
break;
default:
// 理论上不会走到这里,因为 Zod 已经校验过
return {
content: [{ type: 'text', text: '不支持的运算类型' }],
isError: true,
};
}
// 正常返回计算结果
return {
content: [{ type: 'text', text: `${a} ${operation} ${b} = ${result}` }],
};
}
);
// ===== 资源部分 =====
// 注册一个文件资源模板,支持动态 URI
// 客户端可以通过 file:///path/to/file 的形式读取内容
server.registerResource(
'config',
'config://app/settings',
'应用配置',
'获取当前应用的配置信息',
async (uri) => {
// 模拟读取配置文件
const config = {
appName: 'advanced-demo',
version: '1.0.0',
environment: process.env.NODE_ENV || 'development',
};
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify(config, null, 2),
}],
};
}
);
// ===== 提示部分 =====
// 注册一个代码审查提示模板
server.registerPrompt(
'code-review',
'对给定代码进行审查并给出改进建议',
{
// 参数定义
code: z.string().describe('要审查的代码'),
language: z.string().optional().describe('编程语言'),
},
async ({ code, language }) => {
// 构造审查提示消息
// 可以返回多条消息,模拟多轮对话
const langText = language ? `这是一段 ${language} 代码` : '这是一段代码';
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `${langText},请审查以下代码并给出改进建议:\n\n\`\`\`\n${code}\n\`\`\``,
},
},
],
};
}
);
// ===== 启动服务器 =====
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP server started on stdio');
// 注意这里用 console.error 而不是 console.log
// 因为 stdout 被 MCP 协议占用了,日志只能输出到 stderr
}
main().catch(console.error);
效果验证
把上面的代码保存为 src/server.ts,安装依赖后用 MCP Inspector 测试
# 安装依赖
npm install @modelcontextprotocol/server zod
# 用 MCP Inspector 启动测试
npx @modelcontextprotocol/inspector npx tsx src/server.ts
在 Inspector 里你可以看到三个能力都注册成功了。调用 calculator 工具,传入 { "operation": "divide", "a": 10, "b": 0 } 会返回除零错误。传入 { "operation": "add", "a": 1, "b": 2 } 返回 1 add 2 = 3。
资源部分访问 config://app/settings 可以拿到 JSON 格式的配置信息。提示部分调用 code-review 并传入代码,会返回构造好的审查提示消息。
常见问题与避坑
1. stdout 被污染导致协议解析失败
这是最常见的问题。MCP stdio 传输模式下,stdout 是协议通道,你不能往里面 console.log 任何东西。一旦写了,客户端就会收到无法解析的 JSON-RPC 消息然后报错。所有日志必须走 console.error 输出到 stderr。我之前有一次在工具处理函数里加了个 console.log 调试,客户端直接连接断开,排查了好久。
2. Zod 版本不兼容
v2 SDK 要求 Zod v4。如果你项目里还装着 Zod v3,导入路径要写成 zod/v4 而不是 zod。两个版本可以共存,但别搞混了。混用的后果是 schema 验证通过但类型推断对不上,编译器报一堆莫名其妙的类型错误。
3. 中间件包的 Host header 验证
Express 和 Fastify 的中间件包默认开启了 Host header 验证,防止 DNS 重绑定攻击。本地开发的时候如果你用 IP 地址而不是 localhost 访问,会被拒绝。调试时可以先临时关掉这个验证,但上线前一定记得开回来。
4. 异步错误没有被捕获
处理函数里的 async 错误如果没 try-catch,会变成 unhandled promise rejection,服务器进程可能直接崩掉。建议在每个处理函数外面包一层 try-catch,把异常转成 isError: true 的工具结果返回。
5. registerTool vs tool 方法混淆
网上很多教程还在用 v1 的 server.tool() 方法。v2 改成了 server.registerTool(),参数结构也变了。从 v1 迁移时一定要对照官方文档改 API 调用,别直接复制旧代码。
小结
这篇聊了 TypeScript MCP SDK v2 的几个核心特性。类型推断配合 Zod schema 让开发体验非常丝滑,处理函数的参数类型完全自动推断。中间件包帮你快速接入各种 Web 框架,但要注意 body 大小限制和 Host 验证这两个坑。上下文对象提供了日志和进度上报能力,但需要客户端先开启对应功能才能生效。跟 Python SDK 相比,TypeScript 更适合 Node.js 技术栈的项目,类型定义可以跨前后端共享。下一篇我们会深入工具开发的参数校验和错误处理细节。
相关推荐
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/zy_dreamer/article/details/163626220




