---
title: 实现最小 MCP Server：注册工具与验证 stdio 调用
description: 用固定版本 TypeScript SDK 编写一个离线加法工具，从客户端完成初始化、发现、调用与参数错误检查，再对照 ChatCrystal 的适配边界。
image: "https://zengliangyi.com/og/blog/mcp-server-implementation.png"
---

启动一个进程并不等于完成 MCP 握手，工具出现在列表里也不证明参数校验生效。本文编写一个不访问模型、网络或知识库的加法工具，由真实 SDK 客户端经 stdio 验证初始化、发现和调用。之后再对照 ChatCrystal，理解协议适配层负责哪些事情。

## 固定练习版本和通信边界

ChatCrystal 固定提交的 [依赖清单](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/package.json)与[锁文件](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/pnpm-lock.yaml)解析到 `@modelcontextprotocol/sdk` 1.30.0。下文使用这个版本及 Zod 4.6.2，在 Node 24+ 上运行。SDK 包名与 API 会变化，不要把这个练习与其他大版本的导入写法混用。

MCP 的宿主组织 AI 应用，客户端负责连接某个服务端，服务端提供工具等能力；transport 是通信通道。stdio 通过子进程的 stdin/stdout 传递按行分隔的 JSON-RPC 消息。这里没有 HTTP 监听端口，但并不意味着服务端处理函数不能访问网络。协议边界见[stdio 传输规范](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports)。

stdio 的 stdout 留给协议消息，诊断信息使用 stderr。`server.connect` 安装传输与处理能力，初始化仍需要客户端发送请求；单独启动服务端后安静等待，并不是启动失败。

## 编写只有一个工具的服务端

在新临时目录安装教学依赖：



```sh
npm   init   -y  
  npm   install   --ignore-scripts   --no-audit   --no-fund   @modelcontextprotocol/sdk@1.30.0   zod@4.6.2
```



保存 `server.mjs`。这是**独立教学服务端，不是 ChatCrystal 的入口**，处理函数只做加法。



```js
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:   'lesson-add'  , version:   '1.0.0'   });  
  server.  registerTool  (  
    'add'  ,  
    {  
      description:   'Add two finite numbers in this offline lesson.'  ,  
      inputSchema: { a: z.  number  (), b: z.  number  () },  
    },  
    async   ({   a  ,   b   })   =>   ({ content: [{ type:   'text'  , text:   String  (a   +   b) }] })  
  );  
  await   server.  connect  (  new   StdioServerTransport  ());  
  console.  error  (  'lesson server connected to stdio'  );
```



工具名用于调用，description 解释用途，inputSchema 定义输入，处理函数返回 content。描述有助于阅读和选用工具，但不是参数校验生效的必需条件，也没有可据此保证调用准确率的测量。

## 用客户端证明握手和参数错误

在同一目录保存 `client.mjs`，然后运行 `node client.mjs`。客户端会启动刚才的服务端子进程；不需要同时手动启动另一个实例。



```js
import   assert   from   'node:assert/strict'  ;  
  import   { fileURLToPath }   from   'node:url'  ;  
  import   { Client }   from   '@modelcontextprotocol/sdk/client/index.js'  ;  
  import   { StdioClientTransport }   from   '@modelcontextprotocol/sdk/client/stdio.js'  ;  
  
  const   client   =   new   Client  ({ name:   'lesson-client'  , version:   '1.0.0'   });  
  const   transport   =   new   StdioClientTransport  ({  
    command: process.execPath,  
    args: [  fileURLToPath  (  new   URL  (  './server.mjs'  ,   import  .  meta  .url))],  
  });  
  try   {  
    await   client.  connect  (transport);  
    const   {   tools   }   =   await   client.  listTools  ();  
    assert.  deepEqual  (  
      tools.  map  ((  tool  )   =>   tool.name),  
      [  'add'  ]  
    );  
    const   good   =   await   client.  callTool  ({ name:   'add'  , arguments: { a:   4  , b:   3   } });  
    assert.  equal  (good.content[  0  ].text,   '7'  );  
    const   bad   =   await   client.  callTool  ({ name:   'add'  , arguments: { a:   'four'  , b:   3   } });  
    assert.  equal  (bad.isError,   true  );  
    console.  log  (  'initialize, list, valid call, and invalid arguments pass'  );  
  }   finally   {  
    await   client.  close  ();  
    await   transport.  close  ();  
  }
```



把结果期望从 `'7'` 改为 `'8'`，程序会失败；恢复后通过。错误输入验证的是字符串不能通过数值 schema，并不是语言模型“自动改正了参数”。这里只对 SDK 客户端与教学服务端做了协议检查，没有测试任何 AI 宿主的安装路径、授权界面或自动选工具行为。

## ChatCrystal 的 MCP 层是 HTTP 适配层

当前 [MCP 实现](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/mcp/server.ts)先创建 `CrystalClient` 并调用 status，再注册七个工具：search_knowledge、get_note、list_notes、get_relations、recall_for_task、validate_task_memory、write_task_memory。服务端版本读取 CLI 包版本，不是固定写成早期版本号。

工具处理函数通过 HTTP 客户端委托已有 API，并把结果序列化为 text content。记忆相关工具复用独立请求 shape。MCP 进程没有直接持有知识库数据库，但客户端还有连接检查等进程内状态；不能把重启协议进程理解成任意在途写入都绝不会丢失或重复。

`crystal mcp` 的 [命令入口](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/commands/mcp.ts)解析 base URL 和 token，连接目标才决定工具访问哪个知识库。名称相同的 MCP 工具不代表相同实例；真实接入与连接配置见[MCP 使用指南](/blog/chatcrystal-mcp-setup/)。本文没有运行这个命令，避免让教学检查访问真实数据或触发后台服务。

## 扩展工具时先定义失败结果

增加一个工具之前，写清输入、读取或写入范围、成功输出和失败类型。输入 schema 不等于业务授权；工具描述也不构成调用者权限。若处理函数调用 HTTP API，还需区分连接失败、认证拒绝、业务错误和协议层错误，不要把它们都包装成成功文本。

本练习的完成条件是初始化成功、工具列表正确、有效输入得到预期结果、无效输入被拒绝、关闭客户端后子进程收尾。模型能否在合适时机选择工具，应由另一个包含具体任务和宿主的评估来验证。

分享：

##  相关文章

[##  MCP 协议的边界：发现、实例身份与调用权限

对照 ChatCrystal 的七个 MCP 工具，用固定 SDK 的内存传输验证同名工具可指向不同实例，并区分参数 schema、行为提示与应用权限。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#MCP

#Node.js](/blog/mcp-protocol-boundaries)    [##  AI 对话格式差异：事件、快照与跨库消息定位

对照五个固定适配器，区分 JSONL 容器、事件语义与 SQLite 键布局，用两个内存数据库观察消息索引、孤立数据和未知版本。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#SQLite](/blog/ai-conversation-format-divergence)    [##  AI 对话管理方案：四种存储路径与迁移验收

比较 Claude Code、Cursor、Copilot、Trae 的固定适配器输入，区分原始副本、可读导出与知识库，并用合成清单验证身份和信息保留。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#SQLite](/blog/ai-conversation-management-options)

##  从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录，欢迎交流实现细节。

[访问我的 GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "实现最小 MCP Server：注册工具与验证 stdio 调用",
  "description": "用固定版本 TypeScript SDK 编写一个离线加法工具，从客户端完成初始化、发现、调用与参数错误检查，再对照 ChatCrystal 的适配边界。",
  "url": "https://zengliangyi.com/blog/mcp-server-implementation/",
  "image": "https://zengliangyi.com/images/blog/mcp-server-implementation.svg",
  "datePublished": "2026-10-06T00:00:00.000Z",
  "dateModified": "2026-10-06T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/blog/mcp-server-implementation/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首页",
      "item": "https://zengliangyi.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "https://zengliangyi.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "实现最小 MCP Server：注册工具与验证 stdio 调用",
      "item": "https://zengliangyi.com/blog/mcp-server-implementation/"
    }
  ]
}
```
