跳到主要内容

自己写客户端 — 从命令行到接上 LLM

这一章讲三件事: 不用 Claude Desktop、不用 VS Code,自己写一个 MCP 客户端要写多少东西(答案:很少); 为什么这样的客户端「能跑但难用」,症结在哪; 接上一个大语言模型之后,职责怎么重新分——以及一个最容易误解的点:LLM 从头到尾碰不到服务器。 到这一章,本书的「造服务器」上半场结束,「用服务器」的下半场开始。

1. 顶层全景:客户端的五步,与为什么要写自己的

先把「客户端」这个身份说清。第 01 章讲过:服务器提供能力,客户端使用能力。 你每天都在用客户端——Claude Desktop、VS Code 都是1。 那为什么要自己写?作者给的场景很具体:你有一个自己的应用(比如电商后台), 想把 AI 能力做进去——MCP 服务器是独立的一个程序,客户端则是嵌在你应用里的那段代码2

自己写的客户端,全部职责只有五步3:

① 连接服务器 ② 列出特性 ③ 选一个特性 ④ 问齐参数 ⑤ 展示结果
(用什么传输) (它会什么) (用户挑一个) (这个工具要啥) (把回答摆出来)

图说:这五步是本章主走查的骨架。第 3 节先徒手走一遍;
第 5 节把 ③④ 两步交给 LLM,客户端就「活」了。

2. 徒手客户端:五十行以内出活

第 ① 步,连接。 STDIO 时代的老相识,只是这次从客户端视角看: StdioClientTransport 的参数就是「怎么把服务器启动起来」—— 因为按第 02 章的规矩,客户端负责把服务器 spawn 成子进程4:

const transport = new StdioClientTransport({ command: "node", args: ["./build/index.js"] });
const client = new Client({ name: "example-client", version: "1.0.0" });
await client.connect(transport); // 握手三步,SDK 替你走了

第 ② 步,列特性。 三行,各管一样5:

const tools = await client.listTools();
const resources = await client.listResources();
const templates = await client.listResourceTemplates(); // 模板资源,第 03 章的坑

第 ③④⑤ 步,选一个工具、问参数、调用。 书里的演示是命令行交互: 用 readline 问用户「第一个数?」「第二个数?」,然后6:

const result = await client.callTool({ name: "add", arguments: { a: firstArg, b: secondArg } });

跑通了。但作者紧接着自问自答:这玩意儿「太程序化」了,不好用7。 不好用在哪?用户得先知道有 add 这个工具、还得知道它要 ab—— 每个能力都靠用户「背下来」才能用。 服务器有五十个工具呢?五百个呢?

3. 症结与药方:把「背下来」从用户身上挪走

作者把两种客户端摆在一张对比里,这是全章的题眼8:

用户要做什么负担
无 LLM看清单、挑工具、按 schema 填参数knowing:得知道有什么、叫什么、要什么
有 LLM说一句人话:「把 1 和 2 加起来」只剩 doing:说清楚要干什么

「knowing 换成 doing」的翻译工作,交给大语言模型。 流程变成四步8:

① 客户端 listTools,拿到工具清单(带 inputSchema)
② 把清单翻译成 LLM 认得的「function」格式
③ 用户说人话 → 客户端把话和 function 清单一并发给 LLM
④ LLM 回:「用 add,参数 {"a":1,"b":2}」 → 客户端照此 callTool

4. 主走查:「Add 1 and 2」的一句话之旅

跟着这句话走完全程,每一步停在具体的数据上看。

第 ① 站:拿到工具清单。 就是第 03 章见过的那份:add, inputSchemaab 两个 number,均必填。

第 ② 站:翻译成 LLM 的格式。 这里要引入一个本书一直没解释透、 但对读者极承重的概念:现在主流的大语言模型 API 都支持工具调用** (tool calling)——你除了发文字,还可以附一份「你可以叫我做这些函数」的清单; 模型回答时不一定回文字,可以回「请调用某函数、参数如下」**。

它也叫函数调用(function calling)——模型「调」的每个工具,在程序里就是一个函数。 MCP 的 inputSchema 和这种 API 要的参数描述,格式几乎一样,但外壳不同, 所以要写一个转换函数 toLLMTool9:

function toLLMTool(tool) {
return {
type: "function",
function: {
name: tool.name, // "add"
description: tool.description, // "Add two numbers"
parameters: { type: "object",
properties: tool.inputSchema.properties, // {a:…, b:…}
required: tool.inputSchema.required }, // ["a","b"]
},
};
}

第 ③ 站:发话。 书里用的模型服务是 GitHub Models—— 一个用 GitHub 账号就能免费调一批主流模型的服务,地址是 https://models.github.ai/inference,调用方式与 OpenAI 的 API 兼容10。 调用要带一个访问凭据:令牌(token)——一串证明「你是你」的字符, 放在请求的 Authorization 头里当「持票人凭证」(bearer token:谁拿着这张票,谁就进得去); 书里用 GitHub 的 personal access token,并特意提醒:放环境变量 (操作系统层面、程序启动时读得到的键值对,不进代码库,是放密钥的常规位置)里, 别写死在源码中11。调用本身12:

const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Add 1 and 2" }],
tools: llmTools, // 第 ② 站翻译好的那份清单
});

第 ④ 站:读回答。 模型的回答里有一个 tool_calls 字段—— 它不说「答案是 3」,它说「请用 add,参数 {"a":1,"b":2}13:

for (const choice of response.choices) {
if (choice.message.tool_calls) { // 模型点名要调函数了
await callTools(choice.message.tool_calls);
}
}

第 ⑤ 站:执行。 注意——真正碰 MCP 服务器的,是客户端自己14:

const toolResult = await client.callTool({
name: tool_call.function.name, // "add"
arguments: JSON.parse(tool_call.function.arguments), // {a:1, b:2}
});

服务器按第 03 章的老规矩回应 content: [{type:"text", text:"3"}]。 一句话的旅程结束:LLM 只做了一件事——把「Add 1 and 2」翻译成 「add + {"a":1,"b":2}」;翻译之外,它既不知道服务器的存在,也碰不到它。

5. 这个分工为什么值得盯住

主走查走完,回头看,这个架构里藏着两层「隔离」,都值得记住:

第一层:LLM 与服务器隔离。 模型只看到工具清单的「描述」, 永远看不到服务器本体。好处是双向的:服务器不用信任模型(它只收合法的 tools/call); 模型也不用部署在服务器旁边——第 08 章的「采样」会把这个隔离反过来用。

第二层:用户与「knowing」隔离。 增加一个 MCP 服务器, 只是 listTools 的结果变长了、llmTools 的清单变长了—— 用户那一侧零学习成本。这就是第 01 章「会写提示词就是 Neo」在代码里的样子, 也是为什么作者说接上 LLM 后「用户体验的差别相当大」8

书里还顺带提了几个调模型时的旋钮(temperaturemax_tokenstop_p), 但明说不展开——「控制回答的随机性与长度」,细节指给了 OpenAI 文档15。 (这几个旋钮是什么,我们的《这就是 ChatGPT》拆解第一章有从零讲起的版本16。)

6. 作者的判断与证据

有证据的:

  • 客户端五步、StdioClientTransport/callTool 用法、toLLMTool 转换、 tool_calls 处理,书里都有完整代码34691314;
  • GitHub Models 免费、需要令牌,是书中的说明1011

作者的判断:

  • 「把 LLM 放在客户端这一侧」是本书一以贯之的立场——服务器赋能、客户端智能; 书里测验题甚至把「把 LLM 放服务器更好」设为错误选项17。这是架构主张,不是协议规定;
  • 演示用 gpt-4o-mini 和 GitHub Models,是「免费且只需 GitHub 账号」的便利性选择10

判断(我们的,不是书里的): 这一章的四步流程(listTools → 翻译 → 问模型 → 执行) 就是今天所有「MCP 宿主」内部的缩影——Claude Desktop、VS Code 的 agent 模式, 做的都是同一件事,只是界面更漂亮。 读懂这五十行,你看任何 agent 产品都能看到里面的这四步。 如果错,会错在: 真实宿主比这复杂——多轮对话(和模型来回好几个回合, 而不是一问一答就结束)、多个服务器、工具结果回喂给模型继续推理 (模型可能调完一个工具再调下一个)。

这一章只走了一(一个回合:用户说一句、模型回一次)、一个工具; 多轮那套属于 agent 循环的范畴,超出了本书的覆盖(见总纲「不覆盖什么」)。

7. 边界与局限

  • 没有回喂:书里调完工具就结束了;真实 agent 会把工具结果再发给模型, 让它决定「答给用户」还是「再调一个」——这一步本书通篇没做;
  • 书中的 toLLMTool 有一行 z.object(tool.inputSchema)伪代码级的简化—— inputSchema 已是 JSON Schema,不能直接这么转 Zod;真正的转换不需要这一步 (书里同函数后面也没用到它,属于残留);
  • 错误路径缺席:模型乱点工具名、参数不符合 schema、服务器报错, 演示代码都没处理(第 05 章的「三关」在这里正好用上);
  • 演示里两个 baseURL 前后不一致(models.github.ai/inferencemodels.inference.ai.azure.com 各出现一次),是 GitHub Models 服务迁移留下的新旧两个地址, 书里没有说明(补充,不在书里,来自通用知识:旧地址会跳转或停用,以官方文档为准)。

8. 可带走的

  1. 客户端五步:连接、列特性、选特性、问参数、展示;
  2. StdioClientTransport 的参数 = 怎么启动服务器;client.callTool({name, arguments}) 是全部动作;
  3. 接 LLM 的本质:tools/list 翻译成模型 API 的 function 格式——inputSchema 基本可以直接搬;
  4. 模型只回 tool_calls(名字 + 参数 JSON),执行永远是你——它是个翻译,不是个操作员;
  5. 令牌放环境变量,Authorization 头走 bearer 形式;
  6. 「knowing → doing」:MCP + LLM 的用户体验公式;
  7. 工具描述(description)写得好不好,直接决定模型选不选对工具——它是写给模型读的文案。

9. 原文地图

主题原书章原文位置
为什么自己写客户端Consuming Servers by Building Bespoke Clients/Agentstext/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:25(搜「build in AI capabilities」)
五步同上text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:63(搜「Set up the client to connect」)
连接代码同上text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:98(搜「StdioClientTransport」)
列特性同上text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:173(搜「listTools」)
问参与调用同上text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:249(搜「callTool」)
knowing vs doing同上text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:383(搜「abstract away the」)
两种流程对比同上text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:401(搜「List server features」)
GitHub Models 与令牌同上text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:445(搜「GitHub Models」)· :463(搜「bearer token」)
toLLMTool同上text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:632(搜「toLLMTool」)
tool_calls同上text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:700(搜「tool_calls」)
callTools 执行同上text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:736(搜「callTools」)
旋钮不展开同上text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:545(搜「temperature」)

Footnotes

  1. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 9-21 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:11,搜「Claude Desktop」)。

  2. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 25-29 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:25,搜「build in AI capabilities as part of your app」)。

  3. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 63-71 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:63,搜「Set up the client to connect to the server」)。 2

  4. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 97-117 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:100,搜「StdioClientTransport」)。 2

  5. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 173-187 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:174,搜「listTools」)。

  6. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 233-255 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:249,搜「callTool」)。 2

  7. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 283-285 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:285,搜「programmatic and not very user-friendly」)。

  8. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 383-431 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:383,搜「abstract away the」;:413,搜「they only communicate with prompts」)。 2 3

  9. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 632-661 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:632,搜「toLLMTool」)。 2

  10. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 437-471 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:445,搜「GitHub Models」;:471,搜「Ollama」)。 2 3

  11. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 461-473 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:463,搜「bearer token」;:473,搜「environment variable」)。 2

  12. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 505-551 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:505,搜「OpenAI」)。

  13. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 696-704 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:700,搜「tool_calls」)。 2

  14. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 736-754 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:746,搜「client.callTool」)。 2

  15. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 556-574 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:574,搜「context window」)。

  16. 补充(不在书里,依据我们的 book 书架):温度等采样旋钮的原理,《这就是 ChatGPT》拆解第一章有完整走查。 依据: book=what-is-chatgpt-doing §01-one-word-at-a-time 事实=该章讲可能性清单与温度怎么控制挑选的随机性。

  17. 出处:「Consuming Servers by Building Bespoke Clients/Agents」第 823-829 段(text/09-fm-consuming-servers-by-building-bespoke-clients-ag.txt:823,搜「benefit of adding an LLM」)。