mcp-for-beginners 实战指南:用 TypeScript 编写、运行并调试 MCP 客户端(stdio 传输)
mcp-for-beginners 实战指南:用 TypeScript 编写、运行并调试 MCP 客户端(stdio 传输)
本篇指南基于 mcp-for-beginners 仓库 03-GettingStarted/02-client 章节的 TypeScript 解决方案,完整讲解如何通过 npm 安装依赖、编译服务器并启动一个基于 stdio 传输的 MCP 客户端,实现工具(tool)、资源模板(resource template)与提示(prompt)三大能力的发现与调用。读完本文,你将掌握一套可直接复现的命令行操作流程,并能对照源码理解 MCP 客户端与服务器之间的协议交互细节。
一、这个示例解决什么问题
在 MCP(Model Context Protocol)生态中,客户端是直接与 MCP 服务器通信的自定义应用或脚本,用于请求资源、工具与提示。与图形化的 MCP Inspector 不同,手写客户端可以嵌入你自己的工作流,实现自动化与定制化集成。
本示例位于 03-GettingStarted/02-client/solution/typescript/ 目录,包含一个 TypeScript 编写的 MCP 服务器与客户端:
solution/typescript/
├── package.json # 依赖与 npm scripts
├── tsconfig.json # TypeScript 编译配置
├── src/
│ ├── index.ts # MCP 服务器(Demo Server)
│ └── client.ts # MCP 客户端(example-client)
└── README.md # 运行说明
其中 index.ts 扮演服务器角色,暴露一个 add 工具、一个 file 动态资源模板和一个 review-code 提示;client.ts 作为客户端,通过子进程启动服务器并逐一调用这些能力。
二、运行前的环境准备
关联文档开头提到:推荐安装 uv,但并非必需。需要说明的是,uv 是 Python 生态的包管理器,与本 TypeScript 示例无关——只有当你在 python solution 中运行 client.py 时才需要它。本示例唯一硬性要求是 Node.js 运行时,建议使用能运行 @modelcontextprotocol/sdk 的新版本。
整个流程只需要三条命令:
npm install—— 安装依赖;npm run build—— 编译 TypeScript 并启动服务器;npm run client—— 编译并运行客户端。
三、第一步:安装依赖
在 solution/typescript 目录下执行:
npm install
根据 package.json 的依赖声明,此命令会安装以下关键包:
| 依赖 | 版本要求 | 作用 |
|---|---|---|
@modelcontextprotocol/sdk |
>=1.26.0 |
MCP 官方 TypeScript SDK,提供服务器/客户端核心类 |
zod |
^3.24.2 |
参数 schema 校验,用于声明工具与提示的参数结构 |
uuid |
^14.0.0 |
生成唯一标识(SDK 内部使用) |
typescript |
^5.3.3(dev) |
编译 .ts 源码 |
@types/node |
^20.11.24(dev) |
Node.js 类型定义 |
注意到 package.json 还声明了 "type": "module",即采用 ES Module 规范,因此源码中使用 import 语法,并且 SDK 导入路径带有 .js 后缀(如 @modelcontextprotocol/sdk/client/index.js),这是 Node ESM 解析规则的要求。
四、第二步:编译并启动服务器
npm run build
该命令由 package.json 中的脚本定义展开为:
tsc && node ./build/index.js
也就是说它做了两件事:先用 tsc 将 src/ 下的 TypeScript 编译到 build/ 目录,再直接运行编译产物启动服务器。编译行为由 tsconfig.json 控制:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
关键选项包括:outDir: ./build 与 rootDir: ./src 定义了编译输入输出目录;strict: true 开启严格类型检查;module: Node16 与 moduleResolution: Node16 配合 "type": "module" 使用 ESM 解析策略。
那么服务器启动后提供了什么能力?看 src/index.ts 的核心实现:
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Create an MCP server
const server = new McpServer({
name: "Demo",
version: "1.0.0"
});
// Add an addition tool
server.tool("add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
server.tool() 的第一个参数是工具名 add,第二个参数用 zod 声明了 a、b 两个数字参数,第三个参数是执行函数。注意返回内容采用 MCP 标准内容结构:content: [{ type: "text", text: ... }]。
接着是动态资源模板与提示:
// Add a dynamic greeting resource
server.resource(
"file",
new ResourceTemplate("file://{path}", { list: undefined }),
async (uri, { path }) => ({
contents: [{
uri: uri.href,
text: `File, ${path}!`
}]
})
);
server.prompt(
"review-code",
{ code: z.string() },
({ code }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Please review this code:\n\n${code}`
}
}]
})
);
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
server.resource("file", new ResourceTemplate("file://{path}", ...))声明了一个 URI 模板:客户端传入file://example.txt时,example.txt会被映射到{path}占位符;server.prompt("review-code", { code: z.string() }, ...)声明了一个名为review-code的提示模板,接受一个code字符串参数;- 最后通过
StdioServerTransport将服务器挂到标准输入/输出上——这正是 stdio 传输协议的核心:从 stdin 接收 JSON-RPC 消息、向 stdout 返回响应。
五、第三步:运行客户端
npm run client
此命令展开为:
tsc && node ./build/client.js
由于 npm run build 可能尚未执行(或 build 目录需要更新),client 脚本先编译再运行,确保 build/client.js 存在。客户端核心逻辑在 src/client.ts:
5.1 实例化传输与客户端
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
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);
这里有两个值得注意的细节:
StdioClientTransport接收command与args,指示客户端如何启动服务器——本示例中客户端会以node ./build/index.js的方式自行拉起服务器子进程,并与之建立 stdio 双向通道。这与 02-client 课程中command: "node", args: ["server.js"]的教学示例一致,只是把服务器指向了本 solution 的编译产物;new Client({ name, version })按 MCP 协议要求声明客户端自身标识;await client.connect(transport)完成协议初始化握手。
5.2 发现并调用提示(prompt)
// List prompts
const prompts = await client.listPrompts();
// Get a prompt
const prompt = await client.getPrompt({
name: "review-code",
arguments: {
code: "console.log(\"hello\");"
}
});
console.log("Prompt: ", prompt.messages[0].content);
listPrompts() 用于发现服务器注册的提示,getPrompt() 则按名称并携带参数获取提示内容。此处 name 必须与服务器端 server.prompt("review-code", ...) 声明的名称完全一致,code 参数与服务器端 { code: z.string() } 的 schema 匹配。
5.3 发现资源模板
// List resources
const resources = await client.listResources();
for(let resource in resources.resources) {
console.log("Resource: ", resource);
}
// List resource templates
const templates = await client.listResourceTemplates();
for(let template of templates.resourceTemplates) {
console.log("Resource template: ", template.name);
}
客户端分别调用 listResources() 与 listResourceTemplates()。由于服务器端 ResourceTemplate("file://{path}", { list: undefined }) 显式关闭了静态资源列表(list: undefined),因此 resources.resources 为空,而 resourceTemplates 中会列出 file 模板。
5.4 调用工具
// Call a tool
const result = await client.callTool({
name: "add",
arguments: {
a: 1,
b: 8
}
});
console.log("Tool result: ", result);
callTool() 按工具名 add 与参数 { a: 1, b: 8 } 调用服务器工具,服务器执行 1 + 8 并返回内容数组 [{ type: "text", text: "9" }]。
六、预期输出解读
运行 npm run client 后,关联文档给出的标准输出如下:
Prompt: {
type: 'text',
text: 'Please review this code:\n\nconsole.log("hello");'
}
Resource template: file
Tool result: { content: [ { type: 'text', text: '9' } ] }
逐行解读:
Prompt:—— 客户端getPrompt()拿到review-code提示后打印prompt.messages[0].content。内容类型为text,文本把传入的console.log("hello");嵌入到Please review this code:模板中,验证了提示模板的参数注入逻辑;Resource template: file—— 来自listResourceTemplates()的遍历输出,证明客户端成功发现服务器端注册的动态资源模板;Tool result: { content: [ { type: 'text', text: '9' } ] }——callTool()调用add工具的结果,1 + 8 = 9,以 MCP 标准content数组返回。
这三行输出恰好覆盖了 MCP 服务器的三大能力面:提示、资源、工具,是验证"客户端能否正确发现并调用服务器能力"的最直接手段。
七、调试与进阶:利用 Inspector 脚本
package.json 中还预置了一个调试脚本:
"inspector": "npx @modelcontextprotocol/inspector node build/index.js"
运行 npm run inspector 会启动官方 MCP Inspector 图形界面,将 node build/index.js 作为服务器进程加载。相比手写客户端,Inspector 提供了可视化的连接测试、请求构造与响应查看能力,可以作为客户端开发过程中的交叉验证工具——手写客户端与 Inspector 互为补充,正如 02-client 课程 Key Takeaways 所强调的:客户端既能发现也能调用服务器能力,是除 Inspector 之外测试服务器能力的有效途径。
八、扩展:完整的参考客户端实现
如果你希望看到更完整的 TypeScript 客户端——包含启动横幅、服务器发现、加减乘除与 help 工具调用、资源与提示的容错读取、结果文本提取函数——可以研读 client_example_typescript.ts。其中值得借鉴的实现要点包括:
extractTextResult()工具函数:从content数组中按type === "text"提取纯文本,失败时回退为JSON.stringify或toString(),使各类工具返回结果都能以可读形式打印;- 针对资源/提示列表使用
try/catch容错:当服务器不提供某类能力(如listResources返回空或抛错)时,客户端不崩溃而是打印提示继续执行; - 对
add、subtract、multiply、divide、help等工具逐一调用并格式化输出,演示了一个真实客户端如何编排多个工具调用。
九、关键要点
回顾整个 TypeScript MCP 客户端示例,可沉淀以下经验:
- 三步运行模型:
npm install→npm run build(编译并启动服务器)→npm run client(编译并运行客户端),顺序不可颠倒,因为客户端需要通过 stdio 子进程拉起build/index.js服务器; - stdio 传输的本质:
StdioClientTransport通过command+args定义服务器启动方式,双方在标准输入输出上交换 JSON-RPC 消息,适合本机运行场景;HTTP/SSE 等远程传输见后续章节; - 名称即契约:客户端调用
getPrompt、callTool、readResource时使用的名称、参数结构必须与服务器端server.prompt、server.tool、server.resource的声明严格对齐; - 三大能力对应三个客户端 API:
listPrompts/getPrompt对应提示,listResources/listResourceTemplates对应资源,callTool/listTools对应工具,是构建任何 MCP 客户端的基本动作集。
下一步,你可以基于此客户端接入大型语言模型,构建具备自主调用 MCP 工具能力的智能体客户端,参见下一章节 03-llm-client(原文档位于 translations/ar/03-GettingStarted/02-client/solution/typescript/README.md)。