背景
最近 3 个月时间里,AI 领域中最火热的概念毫无疑问就是 MCP
-
各个平台在提供 MCP 支持能力,并作为更新亮点
-
各个应用在提供 MCP 工具,提供给用户接入
-
各个开发都在卷 MCP 接入,立志于集成各种工具
所以 MCP 到底是什么?上手体验是怎么样?
本文尝试用几个简单的案例从零开始复刻一个小型 MCP 全流程
相关代码已上传到 github - github.com/qqqqqcy/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 定义一个通用的调用方法
// config.js
export default {
ark_api_key: "YOUR_API_KEY 需要替换为您在平台创建的 API Key",
model: "deepseek-v3-250324",
url: "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
}
// 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 测试
// 1_llm/test.js
import { llmCall } from "./index.js";
const messages = [
{
role: "user",
content: "天空为什么是蓝色的?",
},
];
const res = await llmCall(messages);
console.log(JSON.stringify(res));
{
"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,但是这种简单的调用存在一些明显的局限性,例如:
-
静态知识:训练数据截止后,无法获取最新信息(如天气、股价)。
-
精确性不足:复杂数学计算、代码执行可能出错。
-
缺乏专业能力:医疗诊断、法律咨询等需依赖权威数据库。
究竟|9.11比9.8大?大模型们为何会在小学数学题上集体翻车 - www.thepaper.cn/newsDetail_…
接下来我们来通过 Function Call 解决这些问题
2. 貌似神奇的 Function Call
2.1 Function Call 是什么?
Function Call(函数调用)是一种让 LLM 与外部工具或 API 交互的能力。当 LLM 遇到无法独立解决的问题(如实时数据查询、精确计算、专业领域知识等)时,可以通过调用预设的函数来获取准确结果,再将信息整合到回答中。
Function Call 可以很好的解决 LLM 的局限性,通过「外包」任务弥补这些短板,例如:
-
调用搜索引擎获取实时新闻;
-
连接计算工具完成方程求解;
-
访问数据库查询专业资料。
2.2 Function Call 的原理
其原理很简单,我们都知道 LLM 可以通过 Prompt 来限定其输出
所以通过提供可调用函数列表、函数功能说明、入参规范,我们就可以得到预期的返回
基于返回,我们再去调用已有的方法就能得到准确的结果
// 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));
{
"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 应用
// 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}`);
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 遵循客户端(Client)- 服务器(Server)架构,其中主机应用(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 的映射关系如下
-
MCP Server - search、add 工具,提供之后可以被任意支持 MCP 协议的 host 调用
-
MCP Client - 一般由 Host 提供,用于连接 MCP Server
-
MCP Host - host 会与 client 交互判断当前应该使用那些工具、抹平不同 LLM 之间的 tools 参数差异
3.3 MCP 实现
3.3.1 MCP Server
假设我的目标是创建一个数学计算相关的 MCP,提供精确的加减法
先创建一个 tools 文件提供工具
// 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 就被成功启动了!
// 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) - stdio 传输通过标准输入和输出流实现通信。这对于本地集成和命令行工具特别有用。
-
服务器发送事件 (SSE) - 服务器发送事件 (SSE)
-
自定义传输 - MCP 使实现自定义传输变得简单。任何传输实现只需要符合传输接口,目前官方的
@modelcontextprotocol/sdk实际提供了多种实现
上一个例子是基于 stdio,我们可以再创建一个基于 SSE 的,以便于后续给 web 端的 client 使用
// 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");
});
使用前先启动服务
node ./4_server/sse.js
- 配置 MCP 服务
- 在对话中执行
3.3.3 Inspector
如果没有现成的 Host,官方也提供了 debug 工具,可以直接设置参数来调用
npx @modelcontextprotocol/inspector node ./4_server/stdio.js
启动之后会生成一个本地服务 Web 页
工具目前支持 STDIO、SSE 两种 MCP Server,通过工具可以非常直观的验证 Tools 的调用结果以便于及时验证
3.3.4 MCP Client
如果你正在使用官方列出的 Host,那么只需要关注 Server 即可。Host 会根据你的配置自动适配对应的 Server 建立 1:1 的 Client
如果你希望自己实现一个 Host,或者想在 Web 端直接调用 MCP Client,过程如下:
// 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();
❯ 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
// 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"
}
}
<!-- 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>
基于之前已经启动的 MCP Server SSE 服务,可以直接看到调用结果
3.3.6 MCP HOST
最后我们基于 vite 简单的实现一个 MCP Host,提供完整的使用链路
// 7_host/package.json
{
"name": "7_host",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"dev": "vite"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.2",
"vite": "^6.3.4"
}
}
<!-- 7_host/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<h1>MCP Host</h1>
<div>
Tools:
<ul id="tools"></ul>
</div>
<label for="input">Query: <input type="text" id="input" /></label>
<button id="button">Send</button>
<div>Result:
<div id="result"></div>
</div>
<script type="module">
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { llmCall } from "../1_llm/index.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();;
const tools = listTools.tools.map((tool) => {
return `<li>${tool.name}</li>`;
});
document.getElementById("tools").innerHTML = tools.join("");
document.getElementById("button").addEventListener("click", async (e) => {
document.getElementById("result").innerHTML = "Loading...";
const query = document.getElementById("input").value;
const llmResult = await llmCall({
messages: [{ role: "user", content: query }],
tools: listTools.tools.map((tool) => {
return {
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.inputSchema,
},
};
}),
});
if (llmResult.choices[0].message.tool_calls) {
const toolCall = llmResult.choices[0].message.tool_calls[0];
const toolName = toolCall.function.name;
const toolArgs = JSON.parse(toolCall.function.arguments);
const callResult = await mcp.callTool({
name: toolName,
arguments: toolArgs,
});
document.getElementById("result").innerHTML = JSON.stringify(callResult);
}
});
</script>
</body>
</html>
4. 总结
4.1 MCP 的优势总结
通过上述对 LLM、Function Call 和 MCP 的详细介绍与实践,我们可以清晰地看到 MCP 在解决当前 AI 应用中 Tools 使用问题上的显著优势。
MCP 作为一个开放协议,为 AI 模型连接不同的数据源和工具提供了标准化方式,就像 USB - C 接口统一了设备连接外设的方式一样。它解决了 Function Call 缺乏统一规范所导致的各种问题,使得 AI 平台、工具提供方和用户都能从中受益。
对于 AI 平台来说,MCP 提供了通用的 tools 入参标准,大大降低了接入工具的成本,无需再为每个工具单独阅读和遵循其不同的规范。对于工具提供方,有了可参照的提供工具标准,能够让工具更容易被不同的 AI 模型调用,有利于工具的推广。而对于用户,在不同 AI 平台的工作流中,MCP 抹平了工具调用、传参和异常处理逻辑的差异,使得创建智能体变得更加简单。
另外,MCP 并不依赖于特定的大模型,它可以被当作一个微服务调用规范,即使不使用大模型,也能实现工具的调用和注册,具有很强的灵活性和通用性。