MCP 到底是什么?从零开始实现一个完整的 MCP 调用链


背景
最近 3 个月时间里,AI 领域中最火热的概念毫无疑问就是 MCP

所以 MCP 到底是什么?上手体验是怎么样?

本文尝试用几个简单的案例从零开始复刻一个小型 MCP 全流程


1. 简单的 LLM
LLM(大型语言模型)是基于深度学习和 Transformer 架构的超大规模人工智能模型,参数规模通常达数百亿至万亿级别,核心能力为文本生成与理解,并逐步扩展至多模态处理(如图像分析)。典型代表包括 OpenAI 的 GPT-4(支持图文输入)、Anthropic 的 Claude 3(长文本与多模态)及深度求索的 DeepSeek-V3(高效开源模型)

1.1 通过 Chat 体验 LLM
LLM 最直观、最常见的使用常见,就是模拟对话

我们通过市面上各种 Chat 客户端,可以与不同的 LLM 来对话


1.2 通过 API 调用 LLM
现在市面上也有各种成熟的大语言模型 api 可以提供给用户任意调用,以火山为例

1.2.1 定义一个通用的调用方法

js

体验AI代码助手

代码解读

复制代码

// config.json { "ark_api_key": "YOUR_API_KEY 需要替换为您在平台创建的 API Key", "model": "deepseek-v3-250324", "url": "" }


js

体验AI代码助手

代码解读

复制代码

// 1_llm/index.js import config from "../config.js"; export const llmCall = async (params) => { const response = await fetch(config.url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.ark_api_key}`, }, body: JSON.stringify({ model: config.model, ...params, }), }); return await response.json(); };

1.2.2 测试

js

体验AI代码助手

代码解读

复制代码

// 1_llm/test.js import { llmCall } from "./index.js"; const messages = [ { role: "user", content: "天空为什么是蓝色的?", }, ]; const res = await llmCall(messages); console.log(JSON.stringify(res));


js

体验AI代码助手

代码解读

复制代码

{ "choices": [ { "finish_reason": "stop", "index": 0, "logprobs": null, "message": { "content": "天空呈现蓝色是由于**瑞利散射(Rayleigh Scattering)**现象,这是阳光与地球大气层相互作用的结果。以下是详细的解释:\n\n---\n\n### 1. **阳光的组成**\n太阳发出的白光由不同波长的光组成(红、橙、黄、绿、蓝、靛、紫等)。其中,蓝光(波长较短,约450-495纳米)和紫光(波长更短)比红光(波长较长,约620-750纳米)具有更高的频率和能量。\n\n---\n\n### 2. **大气层的作用**\n当阳光穿过地球大气层时,会与空气中的气体分子(如氮气、氧气)以及微小颗粒发生碰撞。这些分子和颗粒的尺寸远小于光的波长,导致**瑞利散射**发生: \n- **短波光(蓝/紫光)散射更强**:根据瑞利散射定律,散射强度与波长的四次方成反比(\( I \propto 1/\lambda^4 \)),因此蓝光比红光更容易被散射到四面八方。 \n- **长波光(红光)散射较弱**:红光波长较长,散射程度小,更容易直接穿过大气层。\n\n---\n\n### 3. **人眼感知**\n- 虽然紫光的波长比蓝光更短、散射更强,但人眼对紫光的敏感度较低,且太阳光谱中紫光的能量较少。因此,综合散射强度和视觉感知后,我们主要看到的是**蓝色**。 \n- 散射的蓝光从各个方向进入人眼,使得整个天空呈现蓝色,而太阳本身看起来偏黄(因为部分蓝光已被散射掉)。\n\n---\n\n### 4. **其他现象**\n- **日出/日落时的红色**:当太阳靠近地平线时,阳光需穿过更厚的大气层,蓝光被大量散射,剩余的红光直达人眼,导致天空呈现红橙色。 \n- **高空或太空的颜色**:在太空或极高海拔处,因缺乏大气散射,天空看起来是黑色的。\n\n---\n\n### 总结\n天空的蓝色本质上是阳光中的短波蓝光被大气分子强烈散射的结果,而人眼和太阳光谱的特性进一步强化了这一视觉效果。这一现象由19世纪的物理学家**瑞利勋爵**(Lord Rayleigh)首次科学解释。", "role": "assistant" } } ], "created": 1745845778, "id": "0217458457588550802b9ec61ff2a96f40f4288f2c076e8ef7881", "model": "deepseek-v3-250324", "service_tier": "default", "object": "chat.completion", "usage": { "completion_tokens": 477, "prompt_tokens": 8, "total_tokens": 485, "prompt_tokens_details": { "cached_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0 } } }

1.3 总结

我们用 1min 不到就学会了使用 LLM,但是这种简单的调用存在一些明显的局限性,例如:

接下来我们来通过 Function Call 解决这些问题

2. 貌似神奇的 Function Call
2.1 Function Call 是什么?
Function Call(函数调用)是一种让 LLM 与外部工具或 API 交互的能力。当 LLM 遇到无法独立解决的问题(如实时数据查询、精确计算、专业领域知识等)时,可以通过调用预设的函数来获取准确结果,再将信息整合到回答中。


Function Call 可以很好的解决 LLM 的局限性,通过「外包」任务弥补这些短板,例如:


2.2 Function Call 的原理
其原理很简单,我们都知道 LLM 可以通过 Prompt 来限定其输出

所以通过提供可调用函数列表、函数功能说明、入参规范,我们就可以得到预期的返回

基于返回,我们再去调用已有的方法就能得到准确的结果


js

体验AI代码助手

代码解读

复制代码

// 2_function_call/test.js const messages = [ { role: "user", content: "今天的天气", }, ]; const res = await llmCall({ messages: [ { role: "system", content: ` 你是一个智能函数助手。请根据用户输入完成两个任务: 1. 从可用函数列表中选择最合适的函数 2. 根据选择的函数的参数模式提取参数 可用函数列表: ${JSON.stringify([ { name: "add", description: "计算两个数字的和", parameters: { type: "object", properties: { a: { type: "number" }, b: { type: "number" }, }, }, }, { name: "search", description: "百度搜索", parameters: { type: "object", properties: { query: { type: "string" }, }, }, }, ])} 请以JSON格式返回结果,格式为: { "functionName": "选择的函数名", "parameters": {根据选择函数的schema提取的参数} } `, }, ...messages, ], }); console.log(JSON.stringify(res));


js

体验AI代码助手

代码解读

复制代码

{ "choices": [ { "finish_reason": "stop", "index": 0, "logprobs": null, "message": { "content": "```json\n{\n "functionName": "search",\n "parameters": {\n "query": "今天的天气"\n }\n}\n```", "role": "assistant" } } ], "created": 1745914413, "id": "0217459144113995c9442e2ce6b139b5364a55668e364791cae17", "model": "deepseek-v3-250324", "service_tier": "default", "object": "chat.completion", "usage": { "completion_tokens": 29, "prompt_tokens": 143, "total_tokens": 172, "prompt_tokens_details": { "cached_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0 } } } --- content --- { "functionName": "search", "parameters": { "query": "今天的天气" } }


2.3 tools
实际上,当前市面上主流的 LLM API,都已经通过 tools(不同 API 入参会存在一些差异)内置了 Function Call 能力,不需要大家自己去实现 promot


基于此,我们可以快速实现一个带「插件」功能的小型 LLM 应用


js

体验AI代码助手

代码解读

复制代码

// 3_tools/test.js import { z } from "zod"; import { llmCall } from "../1_llm/index.js"; // 工具定义集合 const TOOLS = { // 加法 add: { name: "add", description: "计算两个数字的和", parameters: z.object({ a: z.number().describe("第一个数字"), b: z.number().describe("第二个数字"), }), execute: ({ a, b }) => { const result = a + b; return result; }, }, // 搜索 search: { name: "search", description: "百度搜索", parameters: z.object({ query: z.string().describe("搜索关键词"), }), execute: async ({ query }) => { const response = await fetch( `https://www.baidu.com/s?wd=${encodeURIComponent(query)}`, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", }, } ); const html = await response.text(); const regex = /<h3 class="t">.*?<a.*?>(.*?)</a>.*?<a.*?href="(.*?)".*?>/; const match = regex.exec(html); if (!match) { return "没有找到相关结果"; } const title = match[1].replace(/<[^>]+>/g, "").trim(); const url = match[2]; return `\n${title}\n${url}`; }, }, }; const messages = [ { role: "user", content: "北京今天的天气", }, ]; const res = await llmCall({ messages, // 工具注册 tools: Object.values(TOOLS).map(( tool ) => ({ type: "function", function: { name: tool .name, description: tool .description, parameters: tool .parameters.shape, }, })), }); // 执行工具 async function executeTool(response) { const message = response.choices[0].message; // 可能不适配任何工具 if (!message.tool_calls) { return message.content; } const toolCall = message.tool_calls[0].function; console.log("fn 调用:", toolCall); return await TOOLS[toolCall.name].execute(JSON.parse(toolCall.arguments)); } const result = await executeTool(res); console.log(`结果: ${result}`);


js

体验AI代码助手

代码解读

复制代码

fn 调用: { arguments: '{"query":"北京今天的天气"}', name: 'search' } 结果: 北京今天的天气的最新相关信息 http://www.baidu.com/link?url=sJKlZP_6IqY6kM0WYcn2tvrpP01_Pucj8tbdI9Whw34S0N9iFFuUVUUpQTKYJiBc_JKGonrkSkZNxnj1ZWpRfB53j8_IQAHOrLBO92JSEXi

2.4 Function Call 的混乱
Function Call 很棒,可以极大扩充 LLM 的能力范围,但是它有一个最大的缺陷:缺乏统一规范

标题    OpenAI    Claude
输入    


输出    


这个缺陷导致各种 tools 之间难以复用、生态割裂、哪怕就是想互相兼容都找不到一个公认标准...

3. 爆火的 MCP

在没有 MCP 之前,如果涉及到 Tools 部分:

身份    需求    痛点
AI 平台    接入工具    不同 LLM 的 tools 入参并不是一致的,市面上没有通用 tools 的入参标准,每接入一个工具,都需要阅读其文档并遵循其规范
工具提供方    提供、推广工具    没有可参照的提供工具标准,只能任选市面上一种 LLM 的规范来遵循,可能其他模型不能很好的调用,也不利于推广
用户    创建智能体    哪怕是同一个工具,在不同 AI 平台的工作流中由于接入方式不同,可能存在不同的调用、传参、异常处理逻辑
因此,MCP协议诞生了!

MCP 是一个开放协议,它规范了应用程序如何向 LLM 提供上下文。可以将 MCP 想象成 AI 应用程序的 USB-C 接口。就像 USB-C 为设备连接各种外设和配件提供了标准化方式一样,MCP 为 AI 模型连接不同的数据源和工具提供了标准化方式

3.1 MCP 总体架构
我们可以先大致了解一下官网所介绍的「总体架构」

MCP 遵循客户端-服务器架构,其中主机应用(Host)程序可以连接到多个服务器:


MCP 主机(Host):想要通过 MCP 访问数据的程序,如 Trae、Claude Desktop、IDE 或 AI 工具
MCP 客户端(Client):与服务器保持1:1连接的协议客户端
MCP 服务器(Server):通过标准化的模型上下文协议暴露特定功能的轻量级程序
本地数据源:MCP 服务器可以安全访问的计算机文件、数据库和服务
远程服务:MCP 服务器可以连接的互联网上的外部系统(例如,通过API)
3.2 MCP 和 Function Call 的映射关系
我们先把 MCP 简单理解为一个目前被公认的 FunctionCall 规范

那么 MCP 和 Function Call 的映射关系如下

3.3 MCP 实现

3.3.1 MCP Server
假设我的目标是创建一个数学计算相关的 MCP,提供精确的加减法

先创建一个 tools 文件提供工具


js

体验AI代码助手

代码解读

复制代码

// 4_server/tools.js import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; const server = new McpServer({ name: "math", version: "1.0.0", }); server.tool( "add", "计算两个数字的和", { a: z.number().describe("First number"), b: z.number().describe("Second number"), }, async ({ a, b }) => { return { content: [ { type: "text", text: `${a} + ${b} = ${a + b}`, }, ], }; } ); server.tool( "minus", "计算两个数字的差", { a: z.number().describe("First number"), b: z.number().describe("Second number"), }, async ({ a, b }) => { return { content: [ { type: "text", text: `${a} - ${b} = ${a - b}`, }, ], }; } ); export default server;

有了工具之后,我们新建一个 transport 并与 Server 绑定

一个简单的 加减法 MCP 就被成功启动了!


js

体验AI代码助手

代码解读

复制代码

// 4_server/stdio.js import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import server from "./tools.js"; const transport = new StdioServerTransport(); await server.connect(transport);

可以通过 Trae 来验证

配置 MCP 服务

在对话中执行


MCP Server SSE

MCP 定义了标准的 Server、Client 之间的消息格式,基于这个格式有不同的实现


上一个例子是基于 stdio,我们可以再创建一个基于 SSE 的,以便于后续给 web 端的 client 使用


js

体验AI代码助手

代码解读

复制代码

// 4_server/sse.js import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import server from "./tools.js"; import express from "express"; let transport = null; const app = express(); // 添加 CORS 中间件 app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); res.header( "Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept" ); res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); next(); }); app.get("/sse", (req, res) => { transport = new SSEServerTransport("/messages", res); server.connect(transport); }); app.post("/messages", (req, res) => { if (transport) { transport.handlePostMessage(req, res); } }); app.listen(8083, () => { console.log("Server is running on port 8083"); });

使用前先启动服务


bash

体验AI代码助手

代码解读

复制代码

node ./4_server/sse.js

配置 MCP 服务

在对话中执行


3.3.3 Inspector
如果没有现成的 Host,官方也提供了 debug 工具,可以直接设置参数来调用


bash

体验AI代码助手

代码解读

复制代码

启动之后会生成一个本地服务 Web 页

工具目前支持 STDIO、SSE 两种 MCP Server,通过工具可以非常直观的验证 Tools 的调用结果以便于及时验证

3.3.4 MCP Client
如果你正在使用官方列出的 Host,那么只需要关注 Server 即可。Host 会根据你的配置自动适配对应的 Server 建立 1:1 的 Client

如果你希望自己实现一个 Host,或者想在 Web 端直接调用 MCP Client,过程如下:


js

体验AI代码助手

代码解读

复制代码

// 5_client/stdio.js import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const mcp = new Client({ name: "mcp-client", version: "1.0.0" }); const transport = new StdioClientTransport({ command: process.execPath, args: ["/Users/bytedance/my-project/trae/mcp_demo/4_server/stdio.js"], }); await mcp.connect(transport); // 展示 Server 提供的 tools const listTools = await mcp.listTools(); console.log(listTools); // 执行指定的 tool const result = await mcp.callTool({ name: "add", arguments: { a: 1, b: 2 }, }); console.log(result); mcp.close();


js

体验AI代码助手

代码解读

复制代码

❯ node ./5_client/stdio.js { tools: [ { name: 'add', description: '计算两个数字的和', inputSchema: [Object] }, { name: 'minus', description: '计算两个数字的差', inputSchema: [Object] } ] } { content: [ { type: 'text', text: '1 + 2 = 3' } ] }

可以清楚的看到,基于官方 SDK 来创建一个 Client 并不难。而且很容易发现一个有意思的点:

整个 MCP 里貌似没有 LLM 的存在

MCP 只是一个调用工具、注册工具的规范,哪怕不用大模型,我把市面上的 MCP Server 当做一个微服务来调用也是完全 OK 的

3.3.5 MCP Client SSE
同 Server 一样,Client 也有不同的实现,接下来我们基于 SSE 实现一个 web 版的 Client


js

体验AI代码助手

代码解读

复制代码

// 6_client_web/package.json { "name": "6_client_web", "version": "1.0.0", "main": "main.js", "scripts": { "dev": "vite" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.10.2", "vite": "^6.3.4" } }


html

体验AI代码助手

代码解读

复制代码

<!-- 6_client_web/index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> </head> <body> <h1>MCP Client</h1> <div id="app"></div> <script type="module"> import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; const mcp = new Client({ name: "mcp-client-web", version: "1.0.0", }); const transport = new SSEClientTransport( new URL("http://127.0.0.1:8083/sse") ); await mcp.connect(transport); // 展示 Server 提供的 tools const listTools = await mcp.listTools(); console.log(listTools); // 执行指定的 tool const result = await mcp.callTool({ name: "add", arguments: { a: 1, b: 2 }, }); console.log(result); mcp.close(); </script> </body> </html>

作者:imtoken
链接:https://www.chinaqicheng.com/post/75011532818019943462146
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值