TypeScript从零部署MCP Server实战
很多MCP的文档写得太碎片化,要么简单到没法直接用,要么被框架的样板代码给淹没了。其实撸一个支持Tools、Resources和Prompts且兼容stdio和HTTP传输的MCP Server没那么复杂,关键得避开几个深坑。
下一篇
让 Claude 闭嘴比让它说话难多了。很多时候它为了显得“聪明” →
最容易翻车的一个细节:如果你用stdio传输,绝对不能用 console.log 打印日志。因为stdout被用来传输JSON-RPC数据包了,随手写个 console.log 就会直接污染数据流,导致宿主端解析失败且不报错,你只会发现程序莫名其妙挂了。记得全部改成 console.error。
// ✅ 正确:日志走 stderr
console.error('Server started');
// ❌ 错误:会破坏 JSON-RPC 通道
console.log('Server started');项目初始化直接走这个配置,记得 type 要设为 module,依赖里得有 @modelcontextprotocol/sdk 和 zod 用来做类型校验。
{
"name": "@ts-ai/mcp-server",
"version": "1.0.0",
"type": "module",
"bin": {
"ts-ai-mcp": "./dist/index.js"
},
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"zod": "^3.23.0"
}
}核心逻辑其实就几行,SDK把握手和能力协商都封装好了。只要调用 server.tool() 或 server.resource(),SDK会自动推断出 server 的 capabilities,不需要手动声明。
最实用的部分是 Tool 的定义,声明和执行是合在一起的。这里分享一个典型的知识库查询工具实现,重点是用 zod 强制约束输入参数,这样 LLM 传参时才稳。
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const server = new McpServer({
name: 'ts-ai-mcp-server',
version: '1.0.0',
});
// 注册一个查询工具
server.tool(
'query_knowledge_base',
'Search internal documents and return relevant chunks with source citations.',
{
question: z.string().describe('The question to query'),
limit: z.number().optional().describe('Max results, default 5'),
},
async ({ question, limit = 5 }) => {
try {
const response = await fetch('http://localhost:3000/api/rag/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question, limit }),
});
if (!response.ok) throw new Error(`RAG service error: ${response.status}`);
const result = await response.json() as { answer: string; citations: any[] };
const citations = result.citations
.map((c, i) => `[Source ${i + 1}] ${c.documentName}\n${c.content}`)
.join('\n\n---\n\n');
return {
content: [{ type: 'text', text: `${result.answer}\n\nSources:\n${citations}` }],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error querying knowledge base: ${error}` }],
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP Server started');这种写法比那种把定义和处理逻辑分开的架构要高效得多,开发速度快,而且类型安全。