随着人工智能技术的快速发展,大型语言模型(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
字段,来监听工具变化并重新对之前获取的工具进行赋值
// ...初始化Clienttool = 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。