---
title: MCP 协议的边界：发现、实例身份与调用权限
description: 对照 ChatCrystal 的七个 MCP 工具，用固定 SDK 的内存传输验证同名工具可指向不同实例，并区分参数 schema、行为提示与应用权限。
image: "https://zengliangyi.com/og/blog/mcp-protocol-boundaries.png"
---

MCP 减少了应用之间重复实现工具发现与调用协议的工作。它规定通信方式和能力描述，实际知识库、权限和输出语义仍由应用承担。前篇[最小 MCP 服务](/blog/mcp-server-implementation/)已经验证 stdio 握手；这篇进一步检查同名工具的实例身份，以及“能发现”与“允许调用”的差别。

## Host、Client 与 Server 分工

[2025-11-25 架构规范](https://modelcontextprotocol.io/specification/2025-11-25/architecture)区分 Host、Client 和 Server。Host 管理模型交互、多个连接和调用策略；Client 与一个 Server 通信；Server 暴露工具等能力。本文固定 SDK 1.30.0 的 LATEST_PROTOCOL_VERSION 也是 2025-11-25，因此引用这一修订，不将其称为当前最新规范。

模型的工具调用表达某项操作的请求，应用负责执行；MCP 让发现与调用可跨应用复用。执行器可以访问远程服务，也可以在本地运行。不能用“Function Calling 必在同一进程、MCP 必在远程”判断集成边界。

该修订的标准传输包括 stdio 与 Streamable HTTP，参见[传输规范](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports)。stdio 不要求 MCP HTTP 监听端口，handler 仍可能访问网络。子进程权限、连接参数、服务端鉴权和业务写入检查不会因此消失。

## ChatCrystal 暴露七个工具，背后仍是 HTTP

[MCP server.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/mcp/server.ts)先创建 CrystalClient、请求 status，再注册七个工具并连接 StdioServerTransport。版本来自 CLI 包。四个知识读取工具是 search_knowledge、get_note、list_notes、get_relations；另外三个是 recall_for_task、validate_task_memory、write_task_memory。注册名与原稿相符，不能把旧版硬编码版本或“不足150行”当作当前证据。

handler 委托 [CrystalClient](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/client.ts) 并把结果序列化为文本。语义检索、关键词列表和任务记忆回忆是不同入口；validate 的预检也不替代 write 的实际业务校验。协议层没有直接打开知识库数据库。

[mcp command](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/commands/mcp.ts)调用 [resolveConnection](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/cli/connection.ts)解析 base URL 和 token，可能连接本地默认、显式、环境或已保存目标。stdio 连接相同而 HTTP 目标不同，会访问不同实例；显式 loopback 的实例模式检查也与隐式本地默认不同。工具名、成功 status 和某条返回文字都不足以证明连到了预期数据集。本文不执行实际 crystal mcp，避免启动服务或访问真实知识库。

## 用两个内存 Server 检查边界

独立目录安装 `npm install --ignore-scripts --save-exact @modelcontextprotocol/sdk@1.30.0 zod@4.6.2`，保存 lesson.mjs，以 Node 24+ 运行。两个真实 SDK Client/Server 通过 InMemoryTransport 通信；只有临时字符串状态，没有 stdio 子进程、HTTP、模型或实际 ChatCrystal。allowedCall 是本篇手写的教学 Host 策略，不能代表某款 AI Host 的权限实现。



```js
import   assert   from   'node:assert/strict'  ;  
  import   { Client }   from   '@modelcontextprotocol/sdk/client/index.js'  ;  
  import   { McpServer }   from   '@modelcontextprotocol/sdk/server/mcp.js'  ;  
  import   { InMemoryTransport }   from   '@modelcontextprotocol/sdk/inMemory.js'  ;  
  import   { z }   from   'zod'  ;  
  
  async   function   peer  (  instance  ) {  
    let   value   =   instance;  
    let   reads   =   0  ;  
    const   server   =   new   McpServer  ({ name:   'fixture-'   +   instance, version:   '1.0.0'   });  
    server.  registerTool  (  
      'read_fixture'  ,  
      {  
        description:   'Read one synthetic value in this instance.'  ,  
        inputSchema: { id: z.  number  () },  
        annotations: { readOnlyHint:   true   },  
      },  
      async   ()   =>   {  
        reads   +=   1  ;  
        return   { content: [{ type:   'text'  , text: value }] };  
      }  
    );  
    server.  registerTool  (  
      'write_fixture'  ,  
      {  
        description:   'Replace one temporary in-memory fixture value.'  ,  
        inputSchema: { value: z.  string  ().  min  (  1  ) },  
        annotations: { readOnlyHint:   false   },  
      },  
      async   (  input  )   =>   {  
        value   =   input.value;  
        return   { content: [{ type:   'text'  , text: value }] };  
      }  
    );  
    const   client   =   new   Client  ({ name:   'fixture-client'  , version:   '1.0.0'   });  
    const   [  clientTransport  ,   serverTransport  ]   =   InMemoryTransport.  createLinkedPair  ();  
    await   server.  connect  (serverTransport);  
    await   client.  connect  (clientTransport);  
    return   {  
      client,  
      server,  
      get   reads  () {  
        return   reads;  
      },  
    };  
  }  
  async   function   allowedCall  (  client  ,   name  ,   args  ,   allowed  ) {  
    if   (  !  allowed.  has  (name))   throw   new   Error  (  'blocked by teaching host policy'  );  
    return   client.  callTool  ({ name, arguments: args });  
  }  
  const   a   =   await   peer  (  'A'  );  
  const   b   =   await   peer  (  'B'  );  
  try   {  
    const   names   =   (  await   a.client.  listTools  ()).tools.  map  ((  tool  )   =>   tool.name).  sort  ();  
    assert.  deepEqual  (names, [  'read_fixture'  ,   'write_fixture'  ]);  
    assert.  deepEqual  ((  await   b.client.  listTools  ()).tools.  map  ((  tool  )   =>   tool.name).  sort  (), names);  
    const   allowed   =   new   Set  ([  'read_fixture'  ]);  
    const   first   =   await   allowedCall  (a.client,   'read_fixture'  , { id:   1   }, allowed);  
    const   second   =   await   allowedCall  (b.client,   'read_fixture'  , { id:   1   }, allowed);  
    assert.  equal  (first.content[  0  ].text,   'A'  );  
    assert.  equal  (second.content[  0  ].text,   'B'  );  
    const   before   =   a.reads;  
    const   bad   =   await   a.client.  callTool  ({ name:   'read_fixture'  , arguments: { id:   'one'   } });  
    assert.  equal  (bad.isError,   true  );  
    assert.  equal  (a.reads, before);  
    await   assert.  rejects  (  
      ()   =>   allowedCall  (a.client,   'write_fixture'  , { value:   'changed'   }, allowed),  
      /  blocked by teaching host policy  /  
    );  
    assert.  equal  (  
      (  await   a.client.  callTool  ({ name:   'read_fixture'  , arguments: { id:   1   } })).content[  0  ].text,  
      'A'  
    );  
    await   a.client.  callTool  ({ name:   'write_fixture'  , arguments: { value:   'changed'   } });  
    assert.  equal  (  
      (  await   a.client.  callTool  ({ name:   'read_fixture'  , arguments: { id:   1   } })).content[  0  ].text,  
      'changed'  
    );  
    console.  log  (  'instance identity, schema errors, and host policy boundaries pass'  );  
  }   finally   {  
    await   a.client.  close  ();  
    await   b.client.  close  ();  
    await   a.server.  close  ();  
    await   b.server.  close  ();  
  }
```



两实例发现相同工具名，却读到 A 和 B。错误 id 触发 isError，read handler 计数不增加。教学策略阻止写入，直接 SDK 调用仍能修改临时状态：行为标注和 schema 不是业务授权。

将第二实例的期望 B 改成 A，实例断言失败；删掉 allowedCall 的名单检查，预期拒绝断言失败。恢复后应通过。它们验证指定样本，没有证明任意 Host 都支持这一 SDK 修订或采用相同策略。

## 发现之后还要验收业务结果

[工具规范](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)把 annotations 作为行为描述，并要求客户端审慎对待不可信来源。即使工具声明只读，也需核查真实 handler、连接权限和返回数据。参数合法不证明查询相关、内容正确或写入有权。

实际集成分别验收协议初始化与版本、工具清单、目标实例、schema 拒绝、鉴权、业务拒绝、输出和退出。对写工具增加重复调用与失败后的状态检查。MCP 统一了可复用接口；可靠性仍需要这些具体证据。需要配置真实连接时，继续读[MCP 接入](/blog/chatcrystal-mcp-setup/)。

分享：

##  相关文章

[##  实现最小 MCP Server：注册工具与验证 stdio 调用

用固定版本 TypeScript SDK 编写一个离线加法工具，从客户端完成初始化、发现、调用与参数错误检查，再对照 ChatCrystal 的适配边界。          曾亮懿       2026年10月6日      阅读约 4 分钟

#ChatCrystal

#MCP

#Node.js](/blog/mcp-server-implementation)    [##  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 协议的边界：发现、实例身份与调用权限",
  "description": "对照 ChatCrystal 的七个 MCP 工具，用固定 SDK 的内存传输验证同名工具可指向不同实例，并区分参数 schema、行为提示与应用权限。",
  "url": "https://zengliangyi.com/blog/mcp-protocol-boundaries/",
  "image": "https://zengliangyi.com/images/blog/mcp-protocol-boundaries.svg",
  "datePublished": "2026-10-07T00:00:00.000Z",
  "dateModified": "2026-10-07T00: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-protocol-boundaries/"
  }
}
{
  "@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 协议的边界：发现、实例身份与调用权限",
      "item": "https://zengliangyi.com/blog/mcp-protocol-boundaries/"
    }
  ]
}
```
