目标与架构
在本课程中,你将构建一个本地 MCP 服务器,提供一项结构化能力:根据包裹重量、目的地区域和服务级别计算运费报价。该能力以 MCP 工具的形式实现,因为客户端会主动携带参数调用它并接收结果。完成本课程后,兼容 MCP 的客户端可以发现该工具、查看其输入 schema、验证调用,并同时接收面向人类的可读文本和结构化数据。
服务器将使用 TypeScript、Node.js 20 或更高版本,以及官方 MCP TypeScript 服务器包。它将通过 stdio 传输进行通信。在此模型中,MCP 客户端会将服务器作为子进程启动,并通过标准输入和标准输出交换 JSON-RPC 消息。这种传输方式适用于本地 AI 应用、命令行宿主和开发工具。远程部署的共享服务器通常应改用 Streamable HTTP。
请求流程很简单。客户端启动进程,完成 MCP 初始化握手,列出可用工具,然后调用选定的工具。SDK 会在运行你的处理程序之前验证所提供的参数。处理程序执行业务计算并返回 MCP 结果。这种分工十分重要:MCP 定义发现和通信机制,而具体应用代码负责运费规则。

项目设置
创建一个新项目,并安装服务器 SDK、Zod、TypeScript 和 tsx runner。由于当前 SDK 使用 ES module 导入,该包配置为 ES module。tsx runner 可以直接执行 TypeScript 入口文件,使本次练习能够专注于 MCP,而不是构建工具。
bash
mkdir shipping-mcp-server
cd shipping-mcp-server
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod
npm install --save-dev typescript tsx @types/node
mkdir src
向 package.json 添加脚本,使启动命令保持稳定。服务器脚本运行 stdio 进程,而 inspect 脚本启动 MCP Inspector,并告知它如何启动同一个进程。
json
{
"name": "shipping-mcp-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"inspect": "npx @modelcontextprotocol/inspector npx tsx src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/server": "^2.0.0",
"zod": "^4.0.0"
},
"devDependencies": {
"@types/node": "^24.0.0",
"tsx": "^4.0.0",
"typescript": "^6.0.0"
}
}
安装依赖时,实际解析到的依赖版本可能会更新。重要的架构细节是:新的服务器项目应使用专用的服务器包,而不是较旧的单体 SDK 包。如果软件包安装失败,请确认 Node.js 和 npm 可用,并且 Node.js 版本至少为 20。
实现服务器
创建 `src/index.ts`,并写入以下实现。`createServer` 函数会构建一个全新的 MCP 服务器,并注册 `calculate-shipping` 工具。输入 schema 接受正数包裹重量、三个目的地区域之一,以及两种服务级别之一。输出 schema 描述结构化结果,客户端无需解析自然语言即可使用这些结果。
typescript
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
const inputSchema = z.object({
weightKg: z.number().positive().max(50),
zone: z.enum(['domestic', 'regional', 'international']),
service: z.enum(['standard', 'express'])
});
const outputSchema = z.object({
currency: z.literal('USD'),
baseCost: z.number(),
serviceSurcharge: z.number(),
totalCost: z.number(),
estimatedDays: z.number().int()
});
function createServer(): McpServer {
const server = new McpServer({
name: 'shipping-quote-server',
version: '1.0.0'
});
server.registerTool(
'calculate-shipping',
{
title: 'Calculate Shipping Quote',
description: 'Calculate a shipping price and delivery estimate for a package',
inputSchema,
outputSchema
},
async ({ weightKg, zone, service }) => {
const ratePerKg = {
domestic: 2.5,
regional: 5,
international: 9
}[zone];
const standardDays = {
domestic: 4,
regional: 7,
international: 14
}[zone];
const baseCost = Number((5 + weightKg * ratePerKg).toFixed(2));
const serviceSurcharge = service === 'express'
? Number((baseCost * 0.6).toFixed(2))
: 0;
const totalCost = Number((baseCost + serviceSurcharge).toFixed(2));
const estimatedDays = service === 'express'
? Math.max(1, Math.ceil(standardDays / 2))
: standardDays;
const result = {
currency: 'USD' as const,
baseCost,
serviceSurcharge,
totalCost,
estimatedDays
};
return {
content: [
{
type: 'text',
text: `Shipping costs USD ${totalCost.toFixed(2)} and takes approximately ${estimatedDays} days.`
}
],
structuredContent: result
};
}
);
return server;
}
void serveStdio(createServer);
console.error('Shipping MCP server is running on stdio');
工具描述应说明该能力的作用,因为 AI agent 会在决定是否调用它时使用名称、描述和生成的 JSON Schema。该 schema 同时也是一个可执行的边界。在计算处理程序执行之前,负数重量、不支持的区域或未知服务就会被拒绝。
理解结果
处理程序会返回同一结果的两种表示形式。`content` 数组包含一个文本块,模型或用户界面可以直接读取。`structuredContent` 值包含可预测的字段,供需要显示价格、比较选项、保存结果或将结果传递给另一个工具的应用程序使用。由于已注册输出 schema,结构化值必须符合该 schema。
对于重量为 3 千克、目的地区域为 regional、服务级别为 express 的输入,基础费用为 20 美元。express 附加费为 12 美元,因此总费用为 32 美元,预计配送时间为 4 天。成功结果的逻辑结构如下。
json
{
"content": [
{
"type": "text",
"text": "Shipping costs USD 32.00 and takes approximately 4 days."
}
],
"structuredContent": {
"currency": "USD",
"baseCost": 20,
"serviceSurcharge": 12,
"totalCost": 32,
"estimatedDays": 4
}
}
与其将所有信息嵌入一句话中,不如使用结构化输出。客户端不应需要通过正则表达式来提取总价或预计配送时间。文本仍然适合提供模型上下文,而结构化对象则提供稳定的应用程序接口。
运行与测试
直接启动服务器,以确认进程能够加载且没有语法或导入错误。服务器应打印状态消息并继续等待,因为 stdio 服务器需要 MCP 客户端发送请求。确认它保持活动状态后,使用终端中断命令停止服务器。
接下来启动 MCP Inspector。Inspector 会启动你的服务器、执行连接握手,并提供一个用于列出和调用工具的浏览器界面。连接到服务器,打开 Tools 视图,选择 `calculate-shipping`,然后提交有效值,例如 3、regional 和 express。
不要只检查成功路径,而要验证三种行为。首先,确认工具显示时包含预期的描述和输入字段。其次,使用有效数据调用工具,并将结构化结果与计算规则进行比较。第三,尝试使用无效重量,例如负 2,或使用不支持的区域。无效请求应在 schema 验证阶段失败,而不应到达处理程序。

常见故障与后续步骤
最具破坏性的 stdio 错误,是将应用程序日志写入标准输出。标准输出专用于传输协议消息,因此一行普通日志就可能破坏 JSON-RPC 流并导致客户端断开连接。请使用标准错误输出进行诊断,状态消息就是这样做的。此限制既适用于你自己的代码,也适用于可能意外打印输出的库。
如果 Inspector 无法启动该进程,请直接从项目根目录运行启动命令。导入错误通常表示包名称不正确、Node.js 版本过旧,或缺少 ES 模块配置。如果工具可见但调用失败,请检查提交的参数名称和类型。该 schema 要求 weight 为数值,并且必须使用准确的枚举值;数字字符串不会自动等同于数字。
能够成功启动的服务器,并不自动意味着它就是一个有用的 MCP 服务器。其工具需要具备精确的描述、受约束的输入、确定性的行为,以及便于模型和应用程序使用的结果。现在你已经掌握了完整的本地流程:创建服务器、注册结构化工具、通过 stdio 提供服务,并使用真实的 MCP 客户端对其进行调用。下一步可以将同一启动命令连接到 AI 主机,或在保留工具契约的同时,用数据库或外部服务替换本地计算。