STDIO MCP服务器

一个通过STDIO实现模型上下文协议(MCP)服务器的端到端指南,专注于JSON-RPC 2.0消息框架和健壮的生命周期管理。

  • by Joseph19820124
  • 本地部署
  • 更新于
在 GitHub 查看 joseph19820124/mcp-stdio-server-guide

使用说明

构建STDIO MCP服务器

一份简明的端到端指南,用于在STDIO上实现模型上下文协议(MCP)服务器,重点介绍JSON-RPC 2.0消息框架和强大的生命周期管理。包括可运行的Node.js/TypeScript示例。


你将建造什么

  • 通过STDIN/STDOUT与JSON-RPC 2.0通信的最小MCP服务器
  • 使用适当的消息框架 Content-Length 标头
  • 能力和工具注册
  • 请求/响应和通知处理
  • 优雅的关机和健康检查

先决条件

  • Node.js≥18
  • npm或pnpm

协议概述(快速)

MCP服务器通常使用JSON-RPC 2.0,并使用类似HTTP的标头进行成帧。 典型的消息如下:

Content-Type: application/json

Content-Length: 123\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"ping","params":{}}

关键规则:

  • 每条JSON消息前面都有标头(至少 Content-Length)
  • 空白行(\r\n\r\n)将标头与JSON正文分开
  • 车身长度匹配 Content-Length 以字节为单位
  • 双向:双方都可以发送请求和通知

工程脚手架

mkdir mcp-stdio-server && cd $_
npm init -y
npm i -D typescript ts-node @types/node
npx tsc --init --rootDir src --outDir dist --esModuleInterop true
mkdir src

最小服务器(TypeScript)

创建 src/server.ts:

import { TextDecoder } from 'node:util';

interface JsonRpcRequest {
  jsonrpc: '2.0';
  id?: number | string;
  method: string;
  params?: unknown;
}

interface JsonRpcResponse {
  jsonrpc: '2.0';
  id: number | string | null;
  result?: unknown;
  error?: { code: number; message: string; data?: unknown };
}

const decoder = new TextDecoder('utf-8');
let buffer = Buffer.alloc(0);

process.stdin.on('data', (chunk) => {
  buffer = Buffer.concat([buffer, chunk]);
  parseFrames();
});

function parseFrames() {
  while (true) {
    const headerEnd = buffer.indexOf('\r\n\r\n');
    if (headerEnd === -1) return;

    const header = buffer.slice(0, headerEnd).toString('utf8');
    const match = /Content-Length:\s*(\d+)/i.exec(header);
    if (!match) {
      // invalid frame, drop
      buffer = buffer.slice(headerEnd + 4);
      continue;
    }

    const length = Number(match[1]);
    const total = headerEnd + 4 + length;
    if (buffer.length < total) return; // wait for more data

    const body = buffer.slice(headerEnd + 4, total);
    buffer = buffer.slice(total);

    handleMessage(JSON.parse(decoder.decode(body)) as JsonRpcRequest);
  }
}

function send(message: JsonRpcRequest | JsonRpcResponse) {
  const payload = Buffer.from(JSON.stringify(message), 'utf8');
  const headers = `Content-Length: ${payload.byteLength}\r\n\r\n`;
  process.stdout.write(headers);
  process.stdout.write(payload);
}

function handleMessage(msg: JsonRpcRequest) {
  if (msg.method === 'ping') {
    if (msg.id !== undefined) send({ jsonrpc: '2.0', id: msg.id, result: { pong: true } });
    return;
  }

  // Example: server advertises capabilities
  if (msg.method === 'initialize') {
    send({
      jsonrpc: '2.0',
      id: msg.id ?? null,
      result: {
        serverInfo: { name: 'example-stdio-mcp', version: '0.1.0' },
        capabilities: {
          tools: [
            { name: 'echo', description: 'Echo back your input' },
          ],
          resources: [],
        },
      },
    });
    return;
  }

  if (msg.method === 'callTool') {
    const { name, args } = (msg.params as any) ?? {};
    if (name === 'echo') {
      send({ jsonrpc: '2.0', id: msg.id ?? null, result: { output: args } });
    } else {
      send({ jsonrpc: '2.0', id: msg.id ?? null, error: { code: -32601, message: 'Tool not found' } });
    }
    return;
  }

  // Fallback for unknown methods
  if (msg.id !== undefined) {
    send({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: `Unknown method: ${msg.method}` } });
  }
}

process.on('SIGINT', () => graceful('SIGINT'));
process.on('SIGTERM', () => graceful('SIGTERM'));

function graceful(reason: string) {
  // optional: send a shutdown notification before exiting
  send({ jsonrpc: '2.0', id: null, method: 'shutdown' } as any);
  setTimeout(() => process.exit(0), 50);
}

将运行脚本添加到 package.json:

{
  "type": "module",
  "scripts": {
    "dev": "ts-node src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js"
  }
}

快速测试

在一个终端中,运行服务器:

npm run dev

在另一个终端中,发送一个框架JSON-RPC请求:

node -e "const m=JSON.stringify({jsonrpc:'2.0',id:1,method:'ping'});process.stdout.write('Content-Length: '+Buffer.byteLength(m)+'\r\n\r\n'+m)" | npm run dev

你应该得到一个框架式的回应 { pong: true }.

生产准备提示

  • 验证标头并拒绝超大有效载荷
  • 添加请求超时和心跳(周期性 ping)
  • 使用十六进制转储记录帧/解析错误
  • 使用结构化日志记录;避免STDIO上的控制台噪音
  • 如果双方都支持,请考虑使用MessagePack进行正文编码
  • 对工具调用实施背压和并发限制

有用的参考资料

  • JSON-RPC 2.0:https://www.jsonrpc.org/specification
  • 语言服务器协议帧(类似标头):https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#headerPart
  • MCP社区示例:在GitHub上搜索“MCP stdio服务器”