最近 MCP 的概念很火,还有 A2A。但是发现很多文章都停留在了概念层面,没有深入去说如何构建一个 MCP 的服务,以及我觉得很多文章对 MCP 的理解其实也是有问题的,所以这篇文章算是站在工程师的角度,如何去构建一个 MCP 的服务。
会大概写三个系列,当前为第一系列 主要介绍 Stdio 的搭建。
在正式开始代码之前,先看一下官方给的架构图:

包含了以下部分:
- Host
- MCP Protocol
- MCP Server
- Local ...
- 其中 host 其实就是 Claude Desktop、IDE 等这样的工具,可以理解成主应用
- MCP Protocol 是一个协议,用于在不同的应用之间进行通信
- MCP Server 是一个服务,也是我们开发者最应该关注的地方,host 通过 MCP Protocol 与 MCP Server 进行通信,所以你想让 LLM 完成什么功能,都需要在 MCP Server 中实现
- Local 则是一系列的数据等,用来提供给 MCP Server 进行使用
在很多文章中还会提及到 MCP Client,其实可以简单理解成就是主应用(Host)中的一部分,在一个问题中可能需要调用多个 MCP Server,所以需要多个 MCP Client。它负责发起 MCP 请求和接收 MCP 的响应。
MCP Server 相关概念
在编写一个正式的 MCP Server 之前,我们需要了解一些概念:

上面是官网文档中列举的相关概念,不过重点关注 Resources 和 Tools。
Tools 这个比较好理解就是工具,定义一系列工具等待被调用。而 Resources 就是资源,可以简单理解成就是给 LLM 的上下文填充使用的,让它更好的理解问题。
MCP Server 搭建
目前官网已经支持了 typescript-sdk 这里直接使用即可。
sh
mkdir mcp-server
cd mcp-server
pnpm init
pnpm add @modelcontextprotocol/sdk zod zod-to-json-schema
编写的示例就是读取当前的 package.json 内容,然后返回给 LLM,可以让用户提问当前 package.json 的 name 和 version 是多少的时候正确回答。
先创建一个 server 实例
ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import packageData from "../package.json";
const server = new Server(
{
name: "get-package",
version: packageData.version,
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
上面的 name 应当保持唯一,下面定义两个工具用于返回 name 和 version。
Tools
ts
const emptyInputSchema = z.object({});
const emptyInputJsonSchema = zodToJsonSchema(emptyInputSchema);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get-name",
description: "获取package.json中的name",
inputSchema: emptyInputJsonSchema,
},
{
name: "get-version",
description: "获取package.json中的version",
inputSchema: emptyInputJsonSchema,
},
],
};
});
这里因为我们并不需要 inputSchema 输入,所以直接使用了 zodToJsonSchema 来生成一个空的 schema。但是如果你的工具依赖相关的字段,比如你有一个 add 的工具,计算两个数的相加,可能你就要定义一下 args 的 schema 了。
定义完成之后只是在 server 上声明了工具,但是当 MCP Client 发起请求调用工具的时候,我们还要写相关的逻辑来处理相对应的工具请求。
ts
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name } = request.params;
switch (name) {
case "get-name": {
return {
content: [
{
type: "text",
text: packageData.name,
},
],
};
}
case "get-version": {
return {
content: [
{
type: "text",
text: packageData.version,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
CallToolRequestSchema 表示调用工具的请求,这里我们根据 name 来返回不同的内容。这样对于工具的编写就完成了,但是最初的时候提到除了工具,还有 Resources 资源,我们还没有定义。
Resources
Resources 用于让 MCP client 读取,然后作为 LLM 上下文的内容。因为 LLM 的训练是有一个截止时间的,对于很多不公开或者最新的信息是没有的,现在比较火的 RAG 其实也是为了让 LLM 可以理解信息。
资源的定义和 Tools 类似,需要先声明资源,然后再定义资源的读取逻辑。
不过这里需要注意,因为不同的主应用(Host)实现不同,所以导致资源的读取逻辑并不相通,截取官方的一段话介绍。

ts
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
name: "package devDependencies依赖",
description: "返回 package.json 中的 devDependencies 依赖",
// "返回整个 package.json 文件的内容,包括 name, version, dependencies, devDependencies, scripts 等。",
uri: "package://devDependencies",
},
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
switch (uri) {
case "package://devDependencies":
return {
contents: [
{
uri,
text: JSON.stringify(packageData, null, 2),
},
],
};
default:
throw new Error(`Unknown resource: ${uri}`);
}
});
最后我们使用 Stdio 来完成消息标准输入和输出。
ts
const transport = new StdioServerTransport();
server.connect(transport);
验证服务
客户端的话可以使用 DeepChat,安装好之后点击设置,找到 MCP 设置,新建一个自定义的 MCP 服务。
然后填写下面的内容用于配置解析
ts
mcpServers: {
"read-package": {
"descriptions": "读取package.json文件相关内容并返回",
"icons": "📁",
"autoApprove": [
"read"
],
"type": "stdio",
"command": "node",
"args": [
"/Users/yliu/Desktop/my/mcp-ts/dist/src/index.js"
],
"env": {},
"baseUrl": ""
}
}

提交之后启动新建的 MCP 服务。
之后新建对话框输入对话就可以看到工具被调用的效果了。

因为目前 DeepChat 并不支持 Resources 的读取,导致我们上面定义的 package 的 info 没有被返回,如果支持 package 的信息读取的话,就可以延伸问一下这个包的协议是什么之类的提问了。
完整代码
点击查看完整代码
ts
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import packageData from "../package.json";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const emptyInputSchema = z.object({});
const emptyInputJsonSchema = zodToJsonSchema(emptyInputSchema);
const server = new Server(
{
name: "get-package",
version: packageData.version,
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get-name",
description: "获取package.json中的name",
inputSchema: emptyInputJsonSchema,
},
{
name: "get-version",
description: "获取package.json中的version",
inputSchema: emptyInputJsonSchema,
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name } = request.params;
switch (name) {
case "get-name": {
return {
content: [
{
type: "text",
text: packageData.name,
},
],
};
}
case "get-version": {
return {
content: [
{
type: "text",
text: packageData.version,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
name: "package devDependencies依赖",
description: "返回 package.json 中的 devDependencies 依赖",
// "返回整个 package.json 文件的内容,包括 name, version, dependencies, devDependencies, scripts 等。",
uri: "package://devDependencies",
},
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
switch (uri) {
case "package://devDependencies":
return {
contents: [
{
uri,
text: JSON.stringify(packageData, null, 2),
},
],
};
default:
throw new Error(`Unknown resource: ${uri}`);
}
});
const transport = new StdioServerTransport();
server.connect(transport);
最后
如果文章有什么问题或者建议,欢迎讨论,下面一篇会介绍 SSE 的实现。