随着人工智能技术的快速发展,大型语言模型(LLM)如 GPT-4、Claude 等已经展现出强大的能力。然而,这些模型仍然存在一些固有的局限性,如无法访问实时信息、无法执行代码、无法与外部系统交互等。为了解决这些问题,Model Context Protocol(MCP)被开发出来。
MCP 是一种开放标准协议,旨在扩展 AI 模型的能力,使其能够与外部工具和资源进行安全、标准化的交互。官方文档已经写得非常详细,建议读者先阅读官方文档了解基础概念。本文将从技术实现角度深入分析 MCP 的工作原理,并使用TypeScript SDK的一些示例,为读者提供更深入的技术洞察。
整体架构
从上图可以看出,MCP 分为客户端和服务端,客户端负责与 MCP 服务端通信并与 LLMs 进行交互,服务端负责处理客户端请求。
Hosts
Hosts 是集成了 MCP 能力同时具备 LLM 能力的应用程序,常见的有:
| 名称 | 工具 | 资源 | 提示词 | 采样 | roots |
|---|---|---|---|---|---|
| Inspector | 支持 | 支持 | 支持 | 不支持 | 不支持 |
| HyperChat | 支持 | 支持 | 支持 | 不支持 | 不支持 |
| Claude Desktop | 支持 | 支持 | 支持 | 不支持 | 不支持 |
| Cherry Studio | 支持 | 简单支持 | 支持 | 不支持 | 不支持 |
| Cursor | 支持 | 不支持 | 不支持 | 不支持 | 不支持 |
| Cline | 支持 | 不支持 | 不支持 | 不支持 | 不支持 |
这里重点推荐一下 HyperChat,虽然界面没有 Cherry Studio 优秀,但有很多高级的功能,比较更适合程序员。
Clients(客户端)
Client 是指在 Host 中用于连接 MCP Server 并提供与 MCP Server 交互的客户端,一般它由 Host 内部实现。
对于 MCP 的能力,部分能力主要需要 Client 来实现的,如下:
- Sampling:提供一个 MCP Server 调用 LLMs 的通道,当 MCP Server 在实现工具、资源等时能够调用 LLMs 来生成结果
- Roots:客户端提供的一个 MCP Server 的一个可参考的根路径或者 URI,规定 MCP Server 的活动范围
Server(服务端)
我们常说的 MCP Server 就是 MCP 的服务端,它负责处理客户端请求,并提供资源、提示词和工具给客户端调用。服务端包含了以下部分:
- Tools 工具:MCP 最常用的能力,通过描述、参数、返回值等信息,使 LLMs 能够与外部资源进行交互
- Resources 资源:提供文件资源、数据库记录、图像等资源供 LLMs 使用
- Prompts 提示词:提供记忆、上下文等提示词供 LLMs 使用
以上工具主要在服务端中实现,供客户端调用。
通信方式
MCP 通信主要通过 JSON-RPC 2.0 协议进行通信,MCP 的通信主要分为以下几种模式:
- Stdio:客户端与 MCP Server 通过标准输入输出进行通信
- SSE(已不再推荐):客户端与 MCP Server 通过 Server-Sent Events 进行通信
- Streamable HTTP:客户端与 MCP Server 通过 HTTP 进行通信,并使用 Streaming 方式返回结果
这些通信模式都是官方实现,如果你有兴趣,完全可以自己实现自己的通信模式比如 WebSocket,当然这得需要你的客户端和服务端两边都需要自己实现。
MCP功能解析
下面我们逐一分析 MCP 的能力以及使用一些示例代码来理解这些功能。
有些地方便于大家理解,使用了最原始的 JSON-RPC 2.0 数据格式,一般我们可以使用各种语言的 SDK,它封装了几乎所有功能,能更便利地使用 MCP。后面部分功能我会用 TypeScript SDK 来做示例演示。
连接
下面是一个简单的 MCP 连接示例:
sequenceDiagram
participant Client as 客户端
participant MCP as MCP服务器
Note over Client, MCP: 连接初始化流程
Client->>+MCP: initialize请求 {protocol_version, client_info}
Note over MCP: 验证协议版本和客户端信息,缓存连接
MCP-->>-Client: initialize响应 {session_id, server_info, capabilities}
Note over Client: 存储session_id,解析capabilities
Note over Client, MCP: 工具列表获取流程
Client->>+MCP: tools/list请求 {session_id}
Note over MCP: 验证session_id
MCP-->>-Client: tools/list响应 {tools: [...]}
Note over Client: 解析可用工具列表
首先 MCP 客户端会向连接的服务端发送一个初始化请求,可能像这样:
{
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"sampling": {},
"roots": {
"listChanged": true
}
},
"clientInfo": {
"name": "simple-mcp-client",
"version": "1.0.0"
}
},
"jsonrpc": "2.0",
"id": 0
}
包含了客户端协议版本、客户端信息、客户端能力,服务端收到该消息后会根据 capabilities 来理解客户端的能力,然后返回初始化响应,可能像这样:
{
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": {
"listChanged": true
},
"prompts": {
"listChanged": true
},
"resources": {
"listChanged": true
},
"completions": {}
},
"serverInfo": {
"name": "stock-agent",
"version": "1.0.0"
}
},
"jsonrpc": "2.0",
"id": 0
}
客户端收到该消息后根据 capabilities 也知道了服务端有哪些能力,如 tools 不为空,说明服务端支持工具使用,那么客户端就可以通过 tools/list 方法获取工具列表。
服务端TypeScript SDK开启服务代码如下
const server = new Server(
{
name: "stock-agent",
version: "1.0.0",
},
{
capabilities: {
tools: {
listChanged: true,
},
prompts: {
listChanged: true,
},
resources: {
listChanged: true,
},
},
}
);
// 使用Stdio连接
await server.connect(new StdioServerTransport());
使用Server的时候需要在 capabilities 中传入支持的能力如tools,或者使用封装更好的 McpServer,可以不用写 capabilities,如工具能力在注册时,会自动注册 tools 能力
const mcpServer = new McpServer({
name: "stock-agent",
version: "1.0.0",
});
// 使用Stdio连接
await server.connect(new StdioServerTransport());
Tools(工具)
工具注册
一般对于单个工具,我们可以直接使用 McpServer,它提供了 tool 函数来注册工具:
mcpServer.tool(
"get-stock-info",
"获取股票信息",
{
stockName: z.string().describe("股票名称或股票代码"),
},
async (parameters, extra) => {
const { stockName } = parameters;
// 调用外部接口获取实时数据
let result = await api.getStockInfo(stockName);
return {
content: [
{
type: "text",
text: result.data,
},
],
isError: false,
};
}
);
获取工具
上面讲了 tools/list 能获取到工具列表,它返回的数据格式长这样:
{
"result": {
"tools": [
{
"name": "get-stock-info",
"description": "获取股票信息",
"inputSchema": {
"type": "object",
"properties": {
"stockName": {
"type": "string",
"description": "股票名称或股票代码"
}
},
"required": [
"stockName"
],
"additionalProperties": false
}
},
{
"name": "get-suggestion-rate",
"description": "投资建议率",
"inputSchema": {
"type": "object",
"properties": {
"stockCode": {
"type": "string",
"description": "股票代码"
}
},
"required": [
"stockCode"
],
"additionalProperties": false
}
}
]
},
"jsonrpc": "2.0",
"id": 1
}
tools 列表下即为支持的工具列表,其中包含以下字段:
- name:工具名称
- description:工具描述
- inputSchema:入参名称及描述,可以指定入参类型,还可以通过
required指定必传参数
调用工具
当我们想调用工具,我们需要构造工具名称以及参数,它类似这样:
{
"method": "tools/call",
"params": {
"name": "get-stock-info",
"arguments": {
"stockName": "阿里巴巴"
}
},
"jsonrpc": "2.0",
"id": 2
}
对应 TypeScript SDK 写法是:
// 客户端调用工具示例
const result = await client.callTool({
name: "get-stock-info",
arguments: {
stockName: "阿里巴巴"
}
});
console.log(result.content);
服务端会收到该请求并执行对应的工具函数,返回结果可能像这样:
{
"result": {
"content": [
{
"type": "text",
"text": "阿里巴巴(BABA)当前股价:$85.32,涨跌:+2.1%,成交量:15.2M"
}
],
"isError": false
},
"jsonrpc": "2.0",
"id": 2
}
listChanged
capabilities下的listChanged可以告诉客户端,当工具发生变化后比如中间新增了新的工具,MCP Server会通过调用notifications/tools/list_changed消息来告知客户端,所以客户端可以通过判断listChanged字段,来监听工具变化并重新对之前获取的工具进行赋值
// ...初始化Client
tool = await client.listTools();
if (client.getServerCapabilities()?.tools?.listChanged) {
client.setNotificationHandler(
ToolListChangedNotificationSchema,
async (args: ToolListChangedNotification) => {
console.log("Tool list changed:", args);
tool = await client.listTools();
}
);
}
Resources(资源)
顾名思义,它能够提供资源给模型,可用于实现类似知识库(RAG)的功能。它可以是任何类型的资源,如文本、图片、音频、视频等。一个简单的资源开发如下:
// 省略初始化
mcpServer.resource("股票历史", "file://documents/stock_info", () => {
const documentsFolder = 'path/stockHistory';
const files = fs.readdirSync(documentsFolder);
return {
contents: files.map(file => ({
uri: `${documentsFolder}/${file}`,
name: file,
mimeType: "text/plain",
text: fs.readFileSync(`${documentsFolder}/${file}`, 'utf-8')
}))
};
});
客户端可以通过listResources来获取资源列表,并通过readResource来读取资源内容
const resources = await client.listResource();
console.log("Resources:", resources);
// {
// resources: [
// { name: '股票历史', uri: 'file://documents/stock_info' },
// ]
// }
const resource = await client.readResource({ uri: "file://documents/stock_info" });
// {
// contents: [
// {
// uri: 'file1.txt',
// mimeType: 'text/plain',
// text: '...'
// },
// {
// uri: 'file2.txt',
// mimeType: 'text/plain',
// text: '...'
// }
// ]
// }
不同的resource下可以按业务类型、资源类型或者功能类型进行分类,contents中可以再进行细分。resource提供的是静态的资源的读取,对于动态资源可以使用Resource Template来实现。
Resource Template
SDK提供了ResourceTemplate用于注册动态资源,其用法如下:
mcpServer.resource(
"user-info",
new ResourceTemplate("data://user/{id}", {
list: undefined,
}),
async (uri, { id }) => {
if (id) {
const info = await getUserInfo(id);
return {
contents: [
{
uri: `data://user/${id}`,
text: JSON.stringify(info),
},
],
};
}
return {
contents: [],
};
}
);
上面我使用ResourceTemplate注册了一个data://user/{id}的动态资源,可以通过传入id来获取用户信息提供给LLMs使用,客户端可以通过readResource来调用
const temps = await client.listResourceTemplates();
console.log("Resource templates:", temps);
// {
// resourceTemplates: [
// { name: 'user-info', uriTemplate: 'data://user/{id}' }
// ]
// }
const userInfo = await client.readResource({ uri: "data://user/123" });
// { contents: [ { uri: 'data://user/123', text: '{\"name\":\"John Doe\"}' } ] }
需要注意的是,Resource Template是通过listResourceTemplates来获取MCP服务器支持哪些资源的,如果想被listResources获取到,可以在ResourceTemplate中传入list函数,比如上面的user-info例子
mcpServer.resource(
"user-info",
new ResourceTemplate("data://user/{id}", {
list: (extra) => {
return {
resources: [
{
uri: "data://user/1",
name: "用户1",
},
{
uri: "data://user/2",
name: "用户2",
},
],
};
},
}),
async (uri, { id }) => {
if (id) {
const info = await getUserInfo(id);
return {
contents: [
{
uri: `data://user/${id}`,
text: JSON.stringify(info),
},
],
};
}
return {
contents: [],
};
}
);
如果在客户端listResources就可以获取到data://user/1和data://user/2两个资源了
const resources = await client.listResource();
console.log("Resources:", resources);
// {
// resources: [
// { name: '股票历史', uri: 'file://documents/stock_info' },
// { name: '用户1', uri: 'data://user/1' },
// { name: '用户2', uri: 'data://user/2' },
// ]
// }
Resources也有listChanged功能。
Prompts(提示词)
提示词是MCP的一个重要概念,它可以提供引导信息、记忆等供LLMs使用。MCP中的提示词支持user和assistant两个角色。
如果我有一个读代码的MCP Server,我可以再其中定义一个提示词让LLMs严格按照我指定的步骤来分析代码,以达到更好的效果。
mcpServer.prompt(
"code-analysis-steps",
"代码分析步骤",
async () => {
return {
description: "代码分析步骤",
messages: [
{
role: "user",
content: {
type: "text",
text: `
请严格按照以下步骤完成代码分析:
第一步:通过'get-environment'工具获取当前项目环境
第二步:通过'get-code-wiki'工具获取本项目的工程简介
第三步:使用'get-code-analysis'工具对代码进行分析
`,
},
}
],
};
}
);
如果我们的MCP客户端支持Prompts,那么我们可以载入上面提示词后再进行后面问答。有了Prompts,就能给模型指引,更好完成任务。就像目前大部分Agent一样,都是以提示词为主,有了提示词,MCP也可以被当作Agent使用。
Prompts也有listChanged功能。
Sampling(采样)
Sampling是MCP的一个高级功能,它允许MCP Server能够直接通过该能力直接调用LLMs来扩展MCP的能力。比如我有一个客服MCP服务,我可以使用Sampling能力来将客户的问题进行分类
mcpServer.tool(
"question-asking-assistant",
"提问小助手",
{
input: z.string().describe("用户提问"),
},
async (parameters, extra) => {
const { input } = parameters;
// 将用户提问分类
// 先判断客户端是否支持sampling能力,不支持则不记录
if (
mcpServer.isConnected() &&
mcpServer.server.getClientCapabilities()?.sampling != null
) {
mcpServer.server.createMessage({
messages: [
{
role: "user",
content: {
type: "text",
text: "你是一个分类任务大师,请根据用户的提问,将问题进行分类,有以下类别:1. 需求 2. bug 3. 优化 4. 咨询 5. 其他。 用户提问是: " + input,
},
},
],
maxTokens: 10,
}).then((result) => {
// 获取分类结果
const text = result.content.text;
saveClassification(text);
})
}
const info = await getSuggestion(input);
return {
content: [{ type: "text", text: info }],
isError: false,
};
}
);
很遗憾,目前几乎没有MCP Hosts实现了Sampling的能力。
Roots
Roots是MCP客户端给服务端提供的一个根路径或者URI,用于限制MCP Server的活动范围。它可以是一个目录、文件或者其他资源。
// 注意这里是MCP客户端代码
client.setRequestHandler(
ListRootsRequestSchema,
(): ListRootsResult => ({
roots: [
{
uri: "file:///path/to/workspace",
name: "workspace",
},
],
})
);
服务端通过listRoots来获取客户端设置的根路径
const roots = await mcpServer.server.listRoots()
// { roots: [ { uri: 'file:///path/to/workspace', name: 'workspace' } ] }
Notification (通知)
Notification是MCP客户端与服务端互相通知的一个机制,MCP内置了一些服务端向客户端通知的能力,比如工具变化通知
// 服务端代码,当工具变化后通知客户端
server.sendToolListChanged();
// 客户端代码
if (client.getServerCapabilities()?.tools?.listChanged) {
client.setNotificationHandler(
ToolListChangedNotificationSchema,
async (args: ToolListChangedNotification) => {
console.log("Tool list changed:", args);
tool = await client.listTools();
}
);
}
除此之外,服务端还提供了如下通知:
notifications/cancelled:此请求可以被客户端和服务端双方发起,表示客户端或者服务端此请求被取消notifications/progress:用于双方对请求进行进度更新的通知notifications/message:用于服务端打印必要日志通知客户端notifications/resources/updated:资源变化后通知客户端notifications/resources/list_changed:资源列表变化后通知客户端notifications/prompts/list_changed:提示词列表变化后通知客户端notifications/tools/list_changed:工具变化后通知客户端,上面例子背后实际调用的此消息
客户除了上面cancelled和progress,还有如下消息可用于通知服务端
notifications/initialized:当客户端初始化完成并与服务端完成建连后通知服务端notifications/roots/list_changed:Roots列表变化后通知服务端
除此之外,我们也可以使用通知机制来实现自定义通知
// 服务端代码
mcpServer.server.notification({
method: "notifications/refresh",
params: { refresh: true },
});
// 客户端监听
client.setNotificationHandler(
NotificationSchema.extend({
method: z.literal("notifications/refresh"),
}),
(args: any) => {
console.log("refresh changed:", args);
}
);
通信
Stdio
Stdio基于标准的流输入输出实现,客户端可以通过stdin和stdout来与MCP Server进行通信,在node中通过使用process.stdin和process.stdout来实现JSON的输入输出。使用此方式连接MCP Server,
Stdio一般采用环境变量来传递身份认证信息。
Stdio一般使用文件来记录日志,(因为使用console.log等打印的日志会被MCP客户端获取,从导致一些异常日志)
Streamable HTTP
Streamable Http采用了POST请求进行双向通信,同时也兼容了SSE双向通信方式。如果要开启POST通信,初始化StreamableHTTPServerTransport时需要传入enableJsonResponse为true
transport = new StreamableHTTPServerTransport({
enableJsonResponse: true,
//...
});
采用了POST通信后,Streamable HTTP请求流程:
sequenceDiagram
participant Client as MCP客户端
participant Server as MCP服务器
participant SSE as SSE流
Note over Client,Server: 1. 初始化连接
Client->>Server: POST /mcp (initialize请求)
Note right of Client: 包含协议版本、客户端能力
Server-->>Client: 初始化响应
Note left of Server: 返回sessionId和服务器能力
Note over Client,Server: 2. 建立SSE连接(默认都会建连)
Client->>Server: GET /mcp
Note right of Client: Accept: text/event-stream<br/>mcp-session-id: {sessionId}
alt 支持SSE
Server-->>SSE: 创建SSE连接
Server-->>Client: 200 OK + SSE头
SSE-->>Client: 保持连接连接
end
Note over Client,Server: 3. 正常客户端->服务端请求响应
loop 请求响应循环
Client->>Server: POST /mcp (JSON-RPC请求)
Note right of Client: tools/call, resources/read等
Server-->>Client: 直接JSON响应
end
Note over Client,Server: 4. 发送通知
Server->>SSE: 发送SSE事件
alt 支持SSE
Note left of Server: data: {JSON-RPC响应}
SSE-->>Client: 流式接收响应
end
Note over Client,Server: 5. 连接关闭
Client->>Server: DELETE /mcp (可选)
Server-->>Client: 200 OK
SSE--xClient: 关闭SSE连接
如果客户端通过POST请求获取MCP Server响应比如调用initialize,MCP Server会直接使用这次的POST请求返回对应数据,当然SSE同样还是会保留用于服务端往客户端发送通知(Notification)的,那如果SSE断了服务端如何向客户端发送通知呢,那没办法了,因为没有SSE通道,服务端就无法向客端主动发送通知了。由于服务端往客户端发送通知并不是MCP的核心链路,所以即使没有建立SSE通道,也不会影响MCP核心功能。
一般Streamable Http身份验证
- 在url参数中携带身份认证信息,这种兼容性最高,几乎所有MCP Hosts都能支持
{
"mcpServers": {
"url": "https://host.com/mcp?AIPKEY={YOUR_API_KEY}",
}
}
- 在请求头中携带身份认证信息
- 使用OAuth2认证
Streamable HTTP可以使用console.log来记录日志,但是由于一般一个服务器会对应多个MCP服务端(一个MCP服务端和一个MCP客户端是一对一连接),所以日志中一般需要至少标注好sessionId,这在分析问题很有用。同时也推荐重写StreamableHTTPServerTransport的handleRequest和send方法,来记录原始日志信息(Stdio同理)
class MyTransport extends StreamableHTTPServerTransport {
constructor(options: StreamableHTTPServerTransportOptions) {
super(options);
}
async handleRequest(req: any, res: any, parsedBody?: any): Promise<void> {
if (parsedBody) {
console.log(`接受到参数:${JSON.stringify(parsedBody)}`);
}
super.handleRequest(req, res, parsedBody);
}
async send(
message: JSONRPCMessage,
options?: { relatedRequestId?: RequestId }
): Promise<void> {
if (message) {
console.log(`返回数据:${JSON.stringify(message)}`);
}
super.send(message, options);
}
}
错误处理
在开发MCP Server时,合适的错误处理非常重要:
mcpServer.tool(
"risky-operation",
"可能失败的操作",
{
input: z.string()
},
async (parameters) => {
try {
const result = await someRiskyOperation(parameters.input);
return {
content: [{ type: "text", text: result }],
isError: false
};
} catch (error) {
return {
content: [{
type: "text",
text: `操作失败: ${error.message}`
}],
isError: true
};
}
}
);
可以通过设置isError为true,来让MCP客户端知道操作失败了。
MCP客户端简单实现
拿到tools、resources、prompts列表之后,我们可以把它告诉LLMs,然后LLMs就可以使用它了。下面通过实现工具的调用来看如何简单实现一个MCP客户端。
目前主流有两种实现工具调用的方式,一是使用Tool Call实现,一个是利用模型返回XML、JSON等格式的文本来调用工具,如Cline。下面我使用TypeScript SDK来演示一下两种方式的实现
- Tool Call
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import OpenAI from "openai";
import {
ChatCompletionMessageParam,
ChatCompletionTool,
} from "openai/resources";
async function main() {
// 这里改为你自己的MCP服务
const transport = new StreamableHTTPClientTransport(
new URL("http://127.0.0.1:3000/mcp")
);
const client = new Client({
name: "simple-mcp-client",
version: "1.0.0",
});
await client.connect(transport);
const tool = await client.listTools();
const openAI = new OpenAI({
baseURL: process.env.OPENAI_API_BASE,
apiKey: process.env.OPENAI_API_KEY,
});
const messages: ChatCompletionMessageParam[] = [
{
role: "user",
content: "帮我分析阿里巴巴股票",
},
];
const tools: Array<ChatCompletionTool> = tool.tools.map((tool) => {
return {
type: "function",
function: {
name: tool.name,
parameters: tool.inputSchema,
description: tool.description,
},
};
});
let response = await openAI.chat.completions.create({
model: "gpt-4o",
messages: messages,
// 传入工具
tools: tools,
});
let message = response.choices[0].message;
// 如果模型调用了工具,通过代码进行真正工具调用
while (message.tool_calls) {
// 将工具调用消息缓存成记忆用于后续再次调用模型时使用
messages.push(message);
const toolCalls = message.tool_calls;
if (toolCalls && toolCalls.length > 0) {
for (const toolCall of toolCalls) {
console.log("工具调用:" + toolCall.function.name);
const name = toolCall.function.name;
// 解析参数
const args = JSON.parse(toolCall.function.arguments);
console.log("参数:" + toolCall.function.arguments);
// 调用工具
const result = await client.callTool({ name, arguments: args });
// 将结果再次给模型
messages.push({
tool_call_id: toolCall.id,
role: "tool",
content: JSON.stringify(result.content),
});
}
}
response = await openAI.chat.completions.create({
model: "gpt-4o",
messages: messages,
});
message = response.choices[0].message;
}
console.log("LLM返回:" + response.choices[0].message.content);
}
main();
以上代码配置好OPENAI_API_BASE和OPENAI_API_KEY后,将MCP服务改为本地已有服务即可直接运行。这就是一个最简单的MCP客户端了。
以上是利用大模型的Tool call能力来实现的MCP工具调用,但部分模型是不支持Tool Call的,如DeepSeek R1,那么还有一种让大模型返回模版的方式来实现类似Tool Call能力
- 模板解析
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import OpenAI from "openai";
import {
ChatCompletionMessageParam,
} from "openai/resources";
async function main() {
const transport = new StreamableHTTPClientTransport(
new URL("http://127.0.0.1:3000/mcp")
);
const client = new Client({
name: "simple-mcp-client",
version: "1.0.0",
});
await client.connect(transport);
const tool = await client.listTools();
const openAI = new OpenAI({
baseURL: process.env.OPENAI_API_BASE,
apiKey: process.env.OPENAI_API_KEY,
});
const messages: ChatCompletionMessageParam[] = [
{
role: "system",
content: `你是一个任务大师,你可以通过使用工具来完成用户的任务。
## 可用工具
${tool.tools
?.map((tool) => {
const schemaStr = tool.inputSchema
? `${JSON.stringify(tool.inputSchema, null, 2).split("\n").join("\n ")}`
: "";
return `- tool_name: ${tool.name}\n- description: ${tool.description}\n- Input Schema: ${schemaStr}\n`;
})
.join("\n\n")}
## 工具使用方法
你可以使用XML格式的方式来调用工具,首先你会根据工具的\`description\`来选择合适的工具,然后通过XML组合工具名称(tool_name)和参数(arguments)来调用工具。
参数:
- tool_name: (必填)工具名称
- arguments: (必填) JSON格式的参数组合,需要参考工具的\`Input Schema\`来填写。
使用方法:
<use_mcp_tool>
<tool_name>tool name here</tool_name>
<arguments>
{
"param1": "value1",
"param2": "value2"
}
</arguments>
</use_mcp_tool>
示例:
我要调用get_weather工具来获取San Francisco的天气情况,然后再提供度假路线,
<use_mcp_tool>
<tool_name>get_weather</tool_name>
<arguments>
{
"city": "San Francisco"
}
</arguments>
</use_mcp_tool>
`,
},
{
role: "user",
content: "帮我分析阿里巴巴股票",
},
];
let response = await openAI.chat.completions.create({
model: "gpt-4o",
messages: messages,
});
const toolResults = [];
let message = response.choices[0].message.content || "";
messages.push(response.choices[0].message)
console.log("response: " + message);
// 解析responseText中的use_mcp_tool标签
const useMcpToolRegex = /<use_mcp_tool>([\s\S]*?)<\/use_mcp_tool>/g;
let match;
let callTools = false;
while ((match = useMcpToolRegex.exec(message)) !== null) {
const toolContent = match[1];
// 解析tool_name
const toolNameMatch = /<tool_name>(.*?)<\/tool_name>/s.exec(toolContent);
const toolName = toolNameMatch ? toolNameMatch[1].trim() : "";
// 解析arguments
const argumentsMatch = /<arguments>([\s\S]*?)<\/arguments>/s.exec(
toolContent
);
let toolArgs: Record<string, any> = {};
if (argumentsMatch) {
try {
toolArgs = JSON.parse(argumentsMatch[1].trim());
} catch (e) {
console.error("Failed to parse tool arguments:", e);
}
}
console.log(`Calling tool: ${toolName} with args:`, toolArgs);
if (toolName) {
try {
// 创建符合callTool参数要求的对象
const result = await client.callTool({
name: toolName,
arguments: toolArgs,
});
toolResults.push(result);
console.log(`Tool result:`, result);
// 将工具结果添加到消息中
messages.push({
role: "user",
content: `Tool result: ${JSON.stringify(result)}`,
});
callTools = true;
} catch (e) {
console.error(`Error calling tool ${toolName}:`, e);
}
}
}
if (callTools) {
const response = await openAI.chat.completions.create({
model: "gpt-4o",
messages,
});
message = response.choices[0].message.content || "";
}
console.log("LLM返回:" + message);
}
main();
一般建议使用XML格式来调用工具,相比JSON,XML有以下好处
- 更容易被正则解析
- 更易读,能让模型和用户都能更好理解字符串内容
当然使用这种方式调用工具肯定没有Tool Call稳定,但是在某些场景使用起来兼容性更好。
功能示例
- 代码生成。如莫高设计提供的magic-mcp,配合使用可以做到设计稿到本地代码的代码生成。更进一步还可以做到与本地UI库的代码生成。
- 本地工作流自动化。例如使用filesystem+git根据本地文件及仓库日志自动生成周报,推荐使用
HyperChat,可设置定时任务生成。 - 使用高德地图MCP Server做旅游路线和行程规划
总结
从上面功能已经看出来了,MCP是作为AI Agent来设计的,为什么很少有人叫MCP为Agent,为什么大部分MCP Server只有Tools,我认为有以下原因:
Tools太强大,Resources和Prompts都能用工具实现- 支持
Resources和Prompts的Host太少了,目前支持比较好仅有Claude Desktop和HyperChat,像Cursor、Cline都仅支持Tools - 模型太强大,只有工具也能完成大部分任务,仅用简单提示词就创造出很好用的智能体,而且还更灵活。
- 高级智能体都还是代码驱动的,使用MCP还是不够稳定
不过虽然现在看起来MCP只是被当成工具使用,但不得不承认它还是有发展成高阶智能体的潜质。不管怎么样,MCP目前都在AI市场中占据着比较重要的地位,掌握MCP的能力也能帮助我们更好地掌握AI和使用AI。