MCP - Implementing a simple mcp server

Record how to implement a simple two-number sum server, client joint debugging and key information description


最近MCP一词处处都出现在我的视野里,看了不少关于MCP的文章,脑海里总结出来的就是个应用间的一种协议充当着类似USB接口的角色。但究竟是什么样子,还是实践一下才知道。翻阅了好几遍 MCP 文档,终于整明白了MCP是怎么个事, 同时自己实现一个最最最简单的MCP Server。本篇文章带着大家快速理解 MCP。


什么是 MCP ?

文档中有着相关的定义,用简单通俗的理解就是:
客户端和服务端通过标准输入/输出(或者可流式传输的 HTTP),以 JSON-RPC 协议进行通讯。
服务端(MCP Server)暴露能力(tools、resources 等, 客户端基于这些能力完成任务。

什么是标准输入/输出?

stdio 是程序与计算机系统之间进行基本数据交互的三个标准通信通道:输入(stdin)、输出(stdout)和错误(stderr)。
观看概念有点不太清楚,写过 node 服务的同学再熟悉不过了这不就是 process.stdio,当然每一种语言都有对应的标准输入输出写,本文以 node 的 stdio 展开。

什么是 JSON-RPC 协议

JSON-RPC 是一种使用 JSON 编码消息的 RPC 协议。本质还是 JSON, 只不过遵循特定的格式。举一个例子:

复制代码
//请求
{
  jsonrpc: "2.0";
  id: string | number;
  method: string;
  params?: {
    [key: string]: unknown;
  };
}

//响应
{
  jsonrpc: "2.0";
  id: string | number;
  result?: {
    [key: string]: unknown;
  }
  error?: {
    code: number;
    message: string;
    data?: unknown;
  }
}

这就是我们客户端和服务端之间的通信格式。
更多细节请阅读 JSON-RPC 2.0 规范

最小可用 MCP 服务器的组成

可以参考官方提供的生命周期图

一个基础的 MCP Server 有以下几部分组成。

初始化相关

  • initialize:客户端告知协议版本/能力;服务端返回自身能力与信息。
  • initialized:通知,表示客户端已完成初始化(通知无 id,无需响应)。

JSON-RPC 请求的 method 的值就是 initialize,initialized。后面讲到的方法同理。

json 复制代码
{
  jsonrpc: "2.0";
  id: 1;
  method: 'initialize';
  params?: {
    [key: string]: unknown;
  };
}

工具相关

  • tools/list:返回可用工具列表(名称、描述、输入参数 schema)。
  • tools/call:执行工具,返回规范化的 result.content
  • 其他常见方法
  • ping:健康检查。
  • shutdown/exit:退出(可选但常用)。

协议注意点

  • 遵循 JSON-RPC 2.0;只有带 id 的请求才需要响应。
  • 错误响应必须只含 error,不可同时含 result

两数求和的 MCP Server 的实现

在了解了标准输入输出,JSON-RPC 和最小 MCP Server 构成后,我们就可以实现我们的 MCP Server 了。

目录与文件

新建项目, 创建两个文件:
index.js:主循环(stdin 读消息 → 分发 → stdout 回应)
utils.js:工具定义与实现(本文示例为 sum

交互主循环:读、分发、写

核心是监听 stdin,解析为请求,根据 method 分发,构造 JSON-RPC 响应并写回 stdout

javascript 复制代码
// index.js
// MCP服务器状态
let serverState = {
    initialized: false,
    clientInfo: null
};

// 处理MCP请求
process.stdin.on('data', data => {
    try {
        const req = JSON.parse(data);
        // 处理initialized通知 - 这是通知,不需要响应
        if (req.method === 'initialized') {
            return;
        }
        // 处理exit通知 - 直接退出,不发送响应
        if (req.method === 'exit') {
            process.exit(0);
        }
        // 只有有id的请求才需要响应
        if (req.id === undefined || req.id === null) {
            return;
        }
        let res = {
            jsonrpc: '2.0',
            id: req.id,
            result: null
        };
        // 处理initialize请求
        if (req.method === 'initialize') {
        }
        // 处理tools/list请求
        else if (req.method === 'tools/list') {
        }
        // 处理tools/call请求
        else if (req.method === 'tools/call') {
        }
        // 处理ping请求
        else if (req.method === 'ping') {
        }
        // 处理shutdown请求
        else if (req.method === 'shutdown') {
        }
        // 未知方法
        else {

        }

        // 发送响应
        process.stdout.write(JSON.stringify(res) + '
');
    } catch (error) {
        // 处理JSON解析错误
        const errorRes = {
            jsonrpc: '2.0',
            id: null,
            error: {
                code: -32700,
                message: "解析错误: " + error.message
            }
        };
        process.stdout.write(JSON.stringify(errorRes) + '
');
    }
});

我们根据 最小可用 MCP 服务器的组成 我们搭出了 server 的架子。

serverState 来维护当前连接状态和客户端信息。

process.stdin.on 监听并读取客户端的数据。

process.stdout.write 将服务端生成的 JSON-RPC 写入,响应客户端。

我们只要实现依据不同的 req.method 就好了。

initialize实现

js 复制代码
  let res = {
      jsonrpc: '2.0',
      id: req.id,
      result: null
  };
  if (req.method === 'initialize') {
     if (serverState.initialized) {
        res.error = {
            code: -32602,
            message: "服务器已经初始化"
        };
        delete res.result;
    } else {
        serverState.clientInfo = req.params;
        serverState.initialized = true;

        res.result = {
            protocolVersion: "2024-11-05",
            capabilities: {
                tools: {
                    listChanged: false
                }
            },
            serverInfo: {
                name: "MCP求和服务器",
                version: "1.0.0"
            }
        };
    }
  }

if 块
我们先判断了连接是否已经初始化过了,如果初始化过了,返回给客户端 error, error code 以及后面的 error code 请参考JSON-RPC 2.0 规范

同时删除了 result (错误响应里不能包含 result 字段)。

else 块
将客户端请求的参数保存起来, 一般的 initialize 请求如下:

JSON 复制代码
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "example-client",
      "version": "1.0.0"
    }
  }
}

protocolVersion 表示客户端希望使用日期戳为 2024-11-05 的协议规范版本, 不同的协议版本可
能会不兼容。

capabilities 用于描述客户端支持哪些功能和特性。客户端通过这个对象告诉服务器:“我具备这些能力,你可以向我发送这些类型的通知或请求,我可以处理这种格式的参数。例子中 elicitation 功能对象({}) 内部是空的,通常意味着客户端支持该功能的所有默认行为。

clientInfo 提供一些客户端的基本信息。

然后设置result, 包含的服务器端信息基本与请求的信息格式一致,重点说一下 listChanged

js 复制代码
capabilities: {
    tools: {
        listChanged: false
    }
}

listChanged 是一种通知机制,用于告知服务器:当服务器所管理的“工具列表”发生变化时,是否需要主动通知客户端。如果设置为true,那么当服务器端tools(后面会将) 列表变动时,服务器端会向客户端发送一个 listChanged 的通知。客户端收到通知后通过调用 tools/list 获取最新的 tool list来与服务器端保持同步。更多信息请移步 lifecycle

组装好jsonrpc后将其用标准输出流返回客户端。这样我们的 initialize 部分就实现了, 初始化完成之后,客户端就可能发起 tools/list 请求 查看一下 服务端都有什么工具供我调遣。

tools/list 实现

在实现这部分的逻辑之前我们需要了解一下什么tools。

一个工具需要“元信息 + 实现”两部分。

  • tools/list 会把 tools 返回给客户端,客户端据此生成 UI。
  • tools/callname 分发到实现(这里是 sum)。

工具定义包括:
name:工具的唯一标识符
title:用于显示目的的可选的、人类可读的工具名称。
description:人类可读的功能描述
inputSchema:定义预期参数的 JSON Schema
outputSchema:可选 JSON Schema 定义预期输出结构
annotations:描述工具行为的可选属性
光看这些属性可能有点懵。结合代码理解就容易的多

js 复制代码
//utils.js

export const sum = ({ a, b }) => {
    return a + b;
}

// MCP工具定义
export const tools = [
    {
        name: "sum",
        description: "calculate the sum of two numbers",
        inputSchema: {
            type: "object",
            properties: {
                a: {
                    type: "number",
                    description: "the first number"
                },
                b: {
                    type: "number",
                    description: "the second number"
                }
            },
            required: ["a", "b"]
        }
    }
];

在tools数组里我们增加了一个对象,而对象的属性满足以上我们列出的属性。整个对象基本以自然语言的形式描述了tool的功能,参数以及参数的类型。通俗理解就是告诉客户端这个工具可以做什么,要怎么用。更多细节移步 tools

而在主逻辑里我们只需要将tools数组(tools 列出了 MCP Server 的所有能力)返回给客户端就可以了。

js 复制代码
import { tools } from './utils.js';

else if (req.method === 'tools/list') {
    if (!serverState.initialized) {
        res.error = {
            code: -32002,
            message: "服务器未初始化"
        };
        delete res.result;
    } else {
        res.result = {
            tools: tools
        };
    }
}

客户端收到了服务端的信息后,知道了服务端给它提供了一个两数求和的工具。那就来用一下吧,于是乎按照tool的格式填好信息发起了tools/call, 希望利用tool的功能帮我计算出一个结果。

tools/call 实现

js 复制代码
import { sum } from './utils.js';

else if (req.method === 'tools/call') {
    if (!serverState.initialized) {
        res.error = {
            code: -32002,
            message: "服务器未初始化"
        };
        delete res.result;
    } else {
        const { name, arguments: args } = req.params;

        if (name === 'sum') {
            try {
                // 参数验证
                if (typeof args.a !== 'number' || typeof args.b !== 'number') {
                    throw new Error('参数a和b必须是数字');
                }

                const result = sum(args);
                res.result = {
                    content: [
                        {
                            type: "text",
                            text: `计算结果: ${args.a} + ${args.b} = ${result}`
                        }
                    ]
                };
            } catch (error) {
                res.error = {
                    code: -32603,
                    message: "内部错误: " + error.message
                };
                delete res.result;
            }
        } else {
            res.error = {
                code: -32601,
                message: "工具未找到: " + name
            };
            delete res.result;
        }
    }
}

通过结构从req.params 中获取请求调用的工具名称name以及参数arguments,我们需要对参数做校验(一般采用ts + zod),这很重要!校验通过后,调用我们的sum函数执行结果采用test格式加入到result中。这样客户端便通过调用我们的工具实现了两数求和。

ping请求实现

ping请求是客户端用来检测连接状态的。这里我们返回一个空对象就可以,没有采用返回体result.content: [{ type, text }] 这样的结构是因为在做测试的时候报错了,后面会讲到。

js 复制代码
else if (req.method === 'ping') {
   res.result = {};
}

shutdown 实现

js 复制代码
else if (req.method === 'shutdown') {
    if (!serverState.initialized) {
        res.error = {
            code: -32002,
            message: "服务器未初始化"
        };
        delete res.result;
    } else {
        res.result = null;
        // 延迟退出,让客户端收到响应
        setTimeout(() => {
            process.exit(0);
        }, 100);
    }
}

需要注意的是我们需要等到客户端接收到响应后再关掉我们的node进程。

完整代码

js 复制代码
import { tools, sum } from './utils.js';

// MCP服务器状态
let serverState = {
    initialized: false,
    clientInfo: null
};

// 处理MCP请求
process.stdin.on('data', data => {
    try {
        const req = JSON.parse(data);

        // 处理initialized通知 - 这是通知,不需要响应
        if (req.method === 'initialized') {
            return;
        }

        // 处理exit通知 - 直接退出,不发送响应
        if (req.method === 'exit') {
            process.exit(0);
        }

        // 只有有id的请求才需要响应
        if (req.id === undefined || req.id === null) {
            return;
        }

        let res = {
            jsonrpc: '2.0',
            id: req.id,
            result: null
        };

        // 处理initialize请求
        if (req.method === 'initialize') {
            if (serverState.initialized) {
                res.error = {
                    code: -32602,
                    message: "服务器已经初始化"
                };
                delete res.result;
            } else {
                serverState.clientInfo = req.params;
                serverState.initialized = true;

                res.result = {
                    protocolVersion: "2024-11-05",
                    capabilities: {
                        tools: {
                            listChanged: false
                        },
                        resources: {
                            subscribe: false
                        }
                    },
                    serverInfo: {
                        name: "MCP求和服务器",
                        version: "1.0.0"
                    }
                };
            }
        }
        // 处理tools/list请求
        else if (req.method === 'tools/list') {
            if (!serverState.initialized) {
                res.error = {
                    code: -32002,
                    message: "服务器未初始化"
                };
                delete res.result;
            } else {
                res.result = {
                    tools: tools
                };
            }
        }
        // 处理tools/call请求
        else if (req.method === 'tools/call') {
            if (!serverState.initialized) {
                res.error = {
                    code: -32002,
                    message: "服务器未初始化"
                };
                delete res.result;
            } else {
                const { name, arguments: args } = req.params;

                if (name === 'sum') {
                    try {
                        // 参数验证
                        if (typeof args.a !== 'number' || typeof args.b !== 'number') {
                            throw new Error('参数a和b必须是数字');
                        }

                        const result = sum(args);
                        res.result = {
                            content: [
                                {
                                    type: "text",
                                    text: `计算结果: ${args.a} + ${args.b} = ${result}`
                                }
                            ]
                        };
                    } catch (error) {
                        res.error = {
                            code: -32603,
                            message: "内部错误: " + error.message
                        };
                        delete res.result;
                    }
                } else {
                    res.error = {
                        code: -32601,
                        message: "工具未找到: " + name
                    };
                    delete res.result;
                }
            }
        }
        // 处理ping请求
        else if (req.method === 'ping') {
            res.result = {};
        }
        // 处理shutdown请求
        else if (req.method === 'shutdown') {
            if (!serverState.initialized) {
                res.error = {
                    code: -32002,
                    message: "服务器未初始化"
                };
                delete res.result;
            } else {
                res.result = null;
                // 延迟退出,让客户端收到响应
                setTimeout(() => {
                    process.exit(0);
                }, 100);
            }
        }
        // 未知方法
        else {
            res.error = {
                code: -32601,
                message: "方法未找到: " + req.method
            };
            delete res.result;
        }

        // 发送响应
        process.stdout.write(JSON.stringify(res) + '
');
    } catch (error) {
        // 处理JSON解析错误
        console.error('JSON解析错误:', error.message);
    }
});

// 输出启动信息
console.error('MCP求和服务器已启动,等待初始化...');

值得注意的一点是,服务器启动的log是用 error 打印的,这样写的目的是方便调试。
总结一下以上实现要点:

  • 通知(initializedexit)不需要响应。
  • 只有带 id 的请求才写回响应。
  • 错误响应只能有 error,不能同时带 result
  • tools/call 的成功返回体必须是 result.content: [{ type, text }] 这样的结构。
    至此,我们的两数求和MCP Server就实现了。接下来是测试环节。

端到端调用流程

既然我们已经准备好了MCP Server, 那我们还需要一个Client来调用我们的服务,这样才能完成完整的测试流程。
那我们的客户端到底应该是怎样的呢?
支持标准输入输出,实现 JSON-RPC 的程序理论上都可以做客户端。以下分别介绍三个客户端:

命令行工具

命令行工具支持标准的输入输出流,只要我们按照JSON—RPC的格式进行通信,那么就可以与我们的MCP服务通信。
打开一个新的命令行窗口,cd到我们的项目目录下,执行node index.js。启动我们的服务后,在命令行输入我们的初始化数据, 需要注意的是在命令行输入不要有换行。

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "teminal-client",
      "version": "1.0.0"
    }
  }
}

可以看到成功获取到了服务器的初始化响应,并获取到了server info.

接着测试我们的tool/list, 继续输入tool/list请求

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}

同样的我们获取到了服务端的tools响应。

最后我们测试tools/call, 继续命令行输入

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "sum",
    "arguments": {
      "a": 3,
      "b": 4
    }
  }
}


我们成功调用了server tool 中sum的功能帮我实现了两数求和。
当然大家可能觉得这样测试不太正规。接着我们来使用官方提供的测试工具 MCP Inspector

MCP Inspector

首先一九四打开命令行窗口,cd到我们的项目目录下,执行以下命令会帮我们创建一个MCP Inspector Client

bash 复制代码
npx @modelcontextprotocol/inspector

MCP Inspector界面如下

在连接我们的MCP Server之前我们需要做一些配置。

  • Transport Type 选择 STDIO ,因为我们是本地的传输。
    Command 填写node 因为我们的服务是基于node实现的
    Arguments 就是我们的启动文件路径。由于我们的Inspector本身就在项目的路径下启动的,这里我们直接添index.js就好了。

配置完成后点击connect, Inspector 会用node 执行Arguments下的脚本,相当于本地执行了 node index.js.

发现左下角打印出了服务器启动的log, 这里解释了为什么用console.error便于调试。连接成功后,Inspector 头部菜单栏展示出了一系列工具,这里就重点讲tools 和 ping。

如图操作:

实际上我们在图中已经实现了对 tool/list 和 tool/call的测试。可以说是很优雅了。

点击ping 按钮 可以收到服务端的响应,注意面板的下方有History记录,记录了客户端的所有操作行为。如果把ping的result 采用 result.content: [{ type, text }]形式,你会发现客户端报错了,说明 Inspector 不希望以这种标准输出的格式作为ping的返回结果。感兴趣可以试试。

至此,关于Inspector的基本使用以及测试也告一段落了。接下来我们要介绍一个大人物 claude code desktop

claude desktop

首先我们需要下载安装 claude desktop, 还需要注册一个账号(需要点魔法,不懂可以cc我)。

安装登录好我们的claude 桌面端后,我们需要选择setting, 点击developer, 选择edit Config.

由于我已经添加过了,这里列出了我添加的server。
编辑 claude_desktop_config_fixed.json 文件。

json 复制代码
{
  "mcpServers": {
    "sum-server": {
      "command": "node",
      "args": ["a:\\b\\c\\index.js"],
      "env": {}
    }
  }
}

看配置有没有觉得很熟悉? 这我们刚刚不才给MCP Inspector 配置过吗。
需要注意一点的是,server的命名格式 example-server
保存更改后,我们关掉 claude desktop并重新启动。

发现我们已经成功添加了我们的sum-server, 但是是无法勾选的。

检查log文件,发现原来是MCP服务器在处理输入时遇到了以下问题:

  1. JSON解析错误。我们需要改造一下原来的代码,使得我们的服务支持多行JSON数据数据分片

  2. 'notifications/cancelled''prompts/list''resources/list' 请求没有响应。
    说明claude 默认发送了这些请求,但我们服务端没有对应的处理,我们需要在服务端处理对应的请求。

处理JSON解析问题

js 复制代码
process.stdin.on('data', data => {
    try {
        // 将新数据添加到缓冲区
        inputBuffer += data.toString();

        // 处理缓冲区中的所有完整JSON消息
        let lines = inputBuffer.split('
');

        // 保留最后一个可能不完整的行
        inputBuffer = lines.pop() || '';

        // 处理所有完整的行
        for (const line of lines) {
            const trimmedLine = line.trim();
            if (trimmedLine) {
                processMessage(trimmedLine);
            }
        }
    } catch (error) {
        console.error('处理stdin数据时出错:', error);
        inputBuffer = '';
    }
});

我们通过使用了一个inputBuffer 来处理分片的JSON数据按行分割数据,确保每次处理完整的JSON消息。

新请求的处理:

js 复制代码
case 'prompts/list':
    if (!serverState.initialized) {
        res.error = {
            code: -32002,
            message: "服务器未初始化"
        };
    } else {
        res.result = {
            prompts: []
        };
    }
    break;

case 'resources/list':
    if (!serverState.initialized) {
        res.error = {
            code: -32002,
            message: "服务器未初始化"
        };
    } else {
        res.result = {
            resources: []
        };
    }
    break;

由于我们mcp不支持prompts和resourses, 而且在返回tools信息的时候已经配置了prompts和 resources的相关信息,这里我们直接返回空数组。

改造后的完整代码

js 复制代码
import { tools, sum } from './utils.js';

// MCP服务器状态
let serverState = {
    initialized: false,
    clientInfo: null
};

let inputBuffer = '';


function sendResponse(response) {
    const responseStr = JSON.stringify(response);
    process.stdout.write(responseStr + '
');
}

function processMessage(messageStr) {
    try {
        const req = JSON.parse(messageStr);

        if (req.method === 'notifications/initialized' ||
            req.method === 'notifications/cancelled') {
            console.error(`收到通知: ${req.method}`);
            return;
        }

        if (req.method === 'exit') {
            console.error('收到exit通知,退出...');
            process.exit(0);
        }

        if (req.id === undefined || req.id === null) {
            console.error('请求没有id,跳过');
            return;
        }

        let res = {
            jsonrpc: '2.0',
            id: req.id
        };

        // 处理不同的方法
        switch (req.method) {
            case 'initialize':
                if (!serverState.initialized) {
                    serverState.clientInfo = req.params;
                    serverState.initialized = true;
                    console.error('服务器已初始化');
                }

                res.result = {
                    protocolVersion: "2024-11-05",
                    capabilities: {
                        tools: {
                            listChanged: false
                        },
                        prompts: {
                            listChanged: false
                        },
                        resources: {
                            subscribe: false,
                            listChanged: false
                        }
                    },
                    serverInfo: {
                        name: "MCP求和服务器",
                        version: "1.0.0"
                    }
                };
                break;

            case 'tools/list':
                if (!serverState.initialized) {
                    res.error = {
                        code: -32002,
                        message: "服务器未初始化"
                    };
                } else {
                    res.result = {
                        tools: tools
                    };
                }
                break;

            case 'prompts/list':
                if (!serverState.initialized) {
                    res.error = {
                        code: -32002,
                        message: "服务器未初始化"
                    };
                } else {
                    res.result = {
                        prompts: []
                    };
                }
                break;

            case 'resources/list':
                if (!serverState.initialized) {
                    res.error = {
                        code: -32002,
                        message: "服务器未初始化"
                    };
                } else {
                    res.result = {
                        resources: []
                    };
                }
                break;

            case 'tools/call':
                if (!serverState.initialized) {
                    res.error = {
                        code: -32002,
                        message: "服务器未初始化"
                    };
                } else {
                    const { name, arguments: args } = req.params;

                    if (name === 'sum') {
                        try {
                            if (typeof args.a !== 'number' || typeof args.b !== 'number') {
                                throw new Error('参数a和b必须是数字');
                            }

                            const result = sum(args);
                            res.result = {
                                content: [
                                    {
                                        type: "text",
                                        text: `计算结果: ${args.a} + ${args.b} = ${result}`
                                    }
                                ]
                            };
                        } catch (error) {
                            res.error = {
                                code: -32603,
                                message: "内部错误: " + error.message
                            };
                        }
                    } else {
                        res.error = {
                            code: -32601,
                            message: "工具未找到: " + name
                        };
                    }
                }
                break;

            case 'ping':
                res.result = {};
                break;

            case 'shutdown':
                if (!serverState.initialized) {
                    res.error = {
                        code: -32002,
                        message: "服务器未初始化"
                    };
                } else {
                    res.result = null;
                    sendResponse(res);
                    setTimeout(() => {
                        console.error('关闭服务器...');
                        process.exit(0);
                    }, 100);
                    return;
                }
                break;

            default:
                res.error = {
                    code: -32601,
                    message: "方法未找到: " + req.method
                };
                break;
        }

        sendResponse(res);

    } catch (error) {
        // 尝试解析部分请求以获取ID
        try {
            const partialReq = JSON.parse(messageStr);
            if (partialReq.id !== undefined) {
                sendResponse({
                    jsonrpc: '2.0',
                    id: partialReq.id,
                    error: {
                        code: -32700,
                        message: "解析错误: " + error.message
                    }
                });
            }
        } catch (e) {
            console.error('无法发送错误响应:', e);
        }
    }
}

// 处理stdin数据 - 使用缓冲区处理分片数据
process.stdin.on('data', data => {
    try {
        // 将新数据添加到缓冲区
        inputBuffer += data.toString();

        // 处理缓冲区中的所有完整JSON消息
        let lines = inputBuffer.split('
');

        // 保留最后一个可能不完整的行
        inputBuffer = lines.pop() || '';

        // 处理所有完整的行
        for (const line of lines) {
            const trimmedLine = line.trim();
            if (trimmedLine) {
                processMessage(trimmedLine);
            }
        }
    } catch (error) {
        console.error('处理stdin数据时出错:', error);
        inputBuffer = '';
    }
});

process.stdin.setEncoding('utf8');

process.on('SIGINT', () => {
    console.error('收到SIGINT信号,正在关闭服务器...');
    process.exit(0);
});

process.on('SIGTERM', () => {
    console.error('收到SIGTERM信号,正在关闭服务器...');
    process.exit(0);
});

process.on('uncaughtException', (error) => {
    console.error('未捕获的异常:', error);
    setTimeout(() => {
        process.exit(1);
    }, 1000);
});

process.on('unhandledRejection', (reason, promise) => {
    console.error('未处理的Promise拒绝:', reason);
    setTimeout(() => {
        process.exit(1);
    }, 1000);
});

process.stdout.setDefaultEncoding('utf8');

console.error('MCP求和服务器已启动,等待初始化...');
console.error('Process ID:', process.pid);
console.error('Node.js版本:', process.version);

保存文件后重启claude,我们可以看到sum-server被默认选中了。同时点击sum-server还可以看到我们server提供的tool工具。

接着我们就可以借助mcp-server的能力做一些事情。如图

可以看到在claude中我们成功使用了sum-server计算两数求和的能力。

常见坑与排查

总结了我们实现sum-server时遇到的一些问题:

  • 对“通知”误回响应 → 导致校验失败。
  • 错误响应同时带了 result → 违反 JSON-RPC 规范。
  • tools/call 返回体未包含 result.content → 客户端无法展示内容。
  • 未初始化就调用工具 → 应返回「服务器未初始化」的标准错误。
  • Windows 路径在配置中要使用双反斜杠转义。
  • 支持多行JSON数据数据分片
  • 更多请求的支持如resouses/list

Summary

我们实现了简单Sum-server的开发和与客户端的联调的整个流程。
总结最小 MCP Server 的核心:

  • 1.要严格遵循 JSON-RPC/MCP 响应结构。
  • 2.清晰声明能力(tools/list)并标准化返回结果(tools/callresult.content)。
    阅读过官方文档的同学可能会看到官方提供了@modelcontextprotocol/sdk, sdk 帮我们封装好了很多功能,便于我们做复杂的MCP Server开发。