作者在MCP规范发布后写了多个服务器,总结了stdio传输优于SSE的理由、输入验证、结构化日志和优雅关闭的最佳实践,并提供可复用的模板代码。
我从 MCP 规范发布时就开始构建 MCP 服务器了。写了第五遍相同的传输层配置、错误处理和工具注册代码之后,我把它提取成了模板。这就是我学到的东西。
规范很简单。底层的 plumbing(连接、协议、状态管理)不简单。
MCP 的核心概念很直接:服务器暴露工具,客户端调用它们。JSON-RPC 协议也很简洁。但每个服务器都需要相同的外围基础设施:
这些都不难。只是繁琐。而且每次都一模一样。
大多数 MCP 服务器应该使用 stdio 传输。原因如下:
SSE 会增加 HTTP 服务器复杂度、连接管理和 CORS 问题。你需要一个运行中的服务器、一个端口和一个 URL。对于一个读取文件或查询数据库的工具来说,这些都是没有任何收益的额外开销。
stdio 就是能用。客户端启动你的进程,通过 stdin/stdout 通信,完成后杀掉进程。没有端口、没有 CORS、没有服务器生命周期。Claude Desktop 和 Cursor 都原生支持它。
const transport = new StdioServerTransport();
await server.connect(transport);
就这样。无需 HTTP 服务器、无需端口管理、无需健康检查。
我看到很多人太早选择 SSE,因为"我可能有一天想远程部署这个"。你大概不会。而且就算真的会,传输层的切换也只需要改 10 行代码。
MCP SDK 让你用纯 JSON 定义工具 schema。别这么做。用 Zod:
import { z } from "zod";
const querySchema = z.object({
sql: z.string().refine(
(s) => !s.match(/\b(insert|update|delete|drop|alter|create|truncate)\b/i),
"Only SELECT queries are allowed"
),
limit: z.number().min(1).max(100).default(50),
});
运行时验证(在逻辑处理前拒绝坏输入)
TypeScript 类型推导(无需手动定义接口)
人类可读的错误消息,会传回给 LLM
没有 Zod,你要么写手动验证(冗长、易出错),要么跳过验证(危险——LLM 最终会给你发送垃圾数据)。
我的数据库查询模板有一个只读 guard。第一版本检查 SQL 字符串中的禁用关键词。LLM 在 30 秒内用 CTE 绕过了它:
WITH delete_me AS (DELETE FROM users RETURNING *) SELECT * FROM delete_me;
修复方案不是更好的正则。而是用数据库自身的权限:
// Create a read-only role and connect with it
const client = new Client({ connectionString: readOnlyConnectionString });
应用层的检查仍然作为快速路径存在,但数据库角色才是真正的安全边界。如果 LLM 找到了绕过方法,数据库仍然会拒绝写操作。
这就是模式:纵深防御。应用检查是为了用户体验,数据库检查是为了安全。
Python MCP SDK 是 async 的。TypeScript SDK 不是(它用回调)。这意味着:
在 TypeScript 中,工具处理器是返回结果的一步函数。简单。
在 Python 中,工具处理器是协程。你需要考虑:
阻塞调用(对同步 DB 驱动使用 asyncio.to_thread)
取消(客户端可以在中途取消)
资源清理(使用 async context manager)
@mcp.tool()
async def query(sql: str) -> str:
async with pool.acquire() as conn:
result = await conn.fetch(sql)
return json.dumps([dict(r) for r in result])
async 连接池很重要。如果每个请求创建一个新连接,高负载下会耗尽连接池。如果共享单个连接,并发请求会串行化。连接池让你同时拥有并发和复用。
我的流式服务器模板使用 SSE 传输。最难的部分不是传输层——而是背压(backpressure)。
当一个工具逐步产生输出时(比如处理一个大文件),你想把结果流式发送给客户端。但客户端可能很慢。如果你推送数据的速度超过客户端消费的速度,就需要背压。
MCP SDK 不会帮你处理这个。你需要:
发送前检查客户端是否还连接着
用有界队列存放发出消息
队列满时丢弃或批量处理
我在生产环境中吃过这个亏——一个流式工具因为客户端比生产者慢导致了 OOM。
MCP 服务器开发中最容易被忽视的部分是客户端配置。你的服务器能工作,但用户需要告诉 Claude Desktop 去哪里找它。
我发布的每个模板都包含一个 client-configs/ 目录,里面有可以直接粘贴的 Claude Desktop、Cursor 和 Windsurf 的 JSON 配置:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"]
}
}
}
绝对路径很重要。在 Claude Desktop 的配置中相对路径不生效。每个新学 MCP 的开发者都会在这里栽跟头。
如果让我重新做这些模板:
模板少一些,深度深一些。10 个模板很难维护。我宁愿有 5 个久经考验的,也不要 10 个只做了 80% 的。
每个模板都要有测试。hello-world 模板有测试。其他模板没有。这是一个缺口。每个模板至少应该有一个集成测试来验证工具实际能工作。
Docker 优先。我给每个模板都加了 Docker 配置,但默认说明仍然写的是"npm install && npm run dev"。Docker 应该是主要路径。它从根本上消除了"在我机器上能跑"的问题。
更好的错误消息。大多数模板返回通用错误。LLM 无法从"Error: something went wrong"调试。错误应该包含工具尝试了什么、期望什么、以及实际得到了什么。
模板在 GitHub 上:thenextfreud/agentforge。MIT 许可。如果你用它们构建了什么,我很想知道。