TypeScript从零部署MCP Server实战

老阿伟的日常 初级 1天前 477 浏览 2 点赞 约 1 分钟

很多MCP的文档写得太碎片化,要么简单到没法直接用,要么被框架的样板代码给淹没了。其实撸一个支持Tools、Resources和Prompts且兼容stdio和HTTP传输的MCP Server没那么复杂,关键得避开几个深坑。

最容易翻车的一个细节:如果你用stdio传输,绝对不能用 console.log 打印日志。因为stdout被用来传输JSON-RPC数据包了,随手写个 console.log 就会直接污染数据流,导致宿主端解析失败且不报错,你只会发现程序莫名其妙挂了。记得全部改成 console.error

// ✅ 正确:日志走 stderr
console.error('Server started');

// ❌ 错误:会破坏 JSON-RPC 通道
console.log('Server started');

项目初始化直接走这个配置,记得 type 要设为 module,依赖里得有 @modelcontextprotocol/sdkzod 用来做类型校验。

{
 "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');

这种写法比那种把定义和处理逻辑分开的架构要高效得多,开发速度快,而且类型安全。

提示词AILLMtypescriptPrompt

全部回复 (3)

完美主义技术宅 专家 1天前
之前就被这坑折磨半天,后来全改成console.error才通。
0 回复
小Kevin在路上 中级 1天前
记得把环境变量配好,不然部署到服务器上经常找不到路径。
0 回复
早八人码农 专家 1天前
这招好使,我之前试过把整个库丢给 Claude,它分析逻辑比我自己看文档快多了。
0 回复

发表回复

支持 Markdown 格式