手写一个 MCP Server 有多简单?30 分钟从零到部署

98 阅读3分钟

上周我花了一个下午,从零开始写了一个 MCP Server。在这之前,我对 MCP 的理解停留在"知道它是个协议"的层面。

最终成果是一个能连接 GitHub API 的 MCP Server——可以查仓库信息、读文件内容、创建 Issue。不算复杂,但足够让我理解这个协议到底在解决什么问题。

环境准备

需要 Node.js 18+ 和 TypeScript。MCP 官方提供了 SDK,支持 TypeScript、Python 和 Java。

npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

这里踩了第一个坑:SDK 版本。安装时默认拉到最新版,但文档和社区示例大多基于 0.x 版,API 差异不小。建议锁 "@modelcontextprotocol/sdk": "^0.5.0"。

理解核心概念

MCP 的通信模型很简单:Client(比如 Claude Code、Cursor)和 Server 之间走 JSON-RPC 2.0。Server 暴露三种能力:

  • Tools:可被 LLM 调用的函数,像 API 端点
  • Resources:暴露数据给 LLM 读取,类似只读接口
  • Prompts:预设的提示模板

大多数场景下,你只需要实现 Tools。

开始写代码

SDK 提供了 Server 类,调用它的 registerToolSchemas 或 .tool() 方法就能注册工具。

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

const server = new Server({
  name: 'github-helper',
  version: '1.0.0',
}, {
  capabilities: { tools: {} },
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'get_repo_info') {
    // 调用 GitHub API
    return { content: [{ type: 'text', text: result }] };
  }
});

const transport = new StdioServerTransport();
await server.connect(transport);

注册完让 Claude Code 使用的步骤更简单,在配置文件中加一段即可:

{
  "mcpServers": {
    "github-helper": {
      "command": "node",
      "args": ["path/to/server.js"]
    }
  }
}

配置完重启 Claude Code,在对话中就能直接让它"查一下这个仓库的信息"。

踩坑记录

1. 入参校验

第一个工具写好以后,传参数时总是报错。查了半天发现是 zod schema 和 MCP 的 inputSchema 格式不完全兼容。MCP 期望的是 JSON Schema 格式,zod 转换过去有时候会丢 required 字段。解决方案是手写 inputSchema:

inputSchema: {
  type: 'object',
  properties: {
    repo: { type: 'string', description: '仓库名,如 owner/repo' },
  },
  required: ['repo'],
}

2. 超时

GitHub API 偶尔响应慢,MCP Server 默认超时时间很短。Claude Code 那边会显示"工具调用失败"。加了重试和超时配置才好:

const transport = new StdioServerTransport();
transport.setTimeout?.(30000); // 30 秒超时

3. Stdio 通信的限制

stdio 传输层通过标准输入输出通信,Server 不能主动推送消息给 Client。如果需要推送能力(比如实时通知),就得用 Streamable HTTP 传输层。

关于鉴权

MCP Server 的鉴权是个容易被忽略的问题。如果 Server 通过 stdio 本地运行,鉴权不是问题——它只在本机启动,能调用它的 CLI 工具自然有权限。

但如果你把 Server 部署成远程服务(比如通过 Streamable HTTP + SSE),鉴权就成了头等大事。目前 MCP 协议在鉴权这块还在演进中,常见的做法有两种:

  1. 在 Tool 内部做鉴权:每个请求都带 API Key 或 Token,Server 端校验
  2. 通过传输层鉴权:如果走 HTTP,可以依赖标准的 Bearer Token 或 OAuth

我个人倾向第一种——简单直接,而且不依赖传输层。缺点是需要每个使用这个 Server 的人配 API Key。

调试技巧

写 MCP Server 的时候调试是个痛点。因为走的是 stdio 通信,console.log 会污染 JSON-RPC 数据流。几个建议:

  • 用 console.error 代替 console.log(stderr 不影响通信)
  • 开发时加一个简单的测试脚本,直接调用 Server 的 handler 函数
  • 用 MCP Inspector(官方提供的调试工具)可视化查看请求和响应

这些技巧能帮你省掉大量排查时间。

总结

30 分钟能写出一个可用的 MCP Server。如果只是为了连接一个 API,SDK 抽象得足够简单。但生产环境要考虑的事情不少:鉴权怎么做、多个工具之间的状态怎么共享、长时间运行的任务怎么处理。

下一步我打算把它部署成远程服务,用 Streamable HTTP 替代 stdio,再补上 SSE 事件推送。到时候再写一篇。

关注我,不错过后续内容。