MCP(Model Context Protocol)是什么?一句话:帮你把任意 LLM(大型语言模型)和外部系统的对接,统一、简单、高效。
在本篇文章里,你将学会:
- MCP 的概念和使用场景
- MCP 的工作原理与数据流
- 用 Node.js 搭建完整 MCP 服务器
- cURL 与可视化调试全流程实测
- 生产环境注意事项与优化建议
国内开发者们快看!Cursor中文文档已经全面上线!现在,你可以通过母语更轻松地掌握这款强大的AI编码工具的全部功能,关于Cursor的开发技巧和博客都在这里。
更多精彩Cursor开发技巧博客地址:cursor.npmlib.com/blogs/curso…
更多Cursor使用技巧也可关注公众号 AI近距离
MCP 究竟是什么?为什么值得用
MCP,全称 Model Context Protocol,是一套基于 JSON-RPC 2.0 的统一协议,用于让 LLM 和外部系统交互。它主要暴露三类能力:
- 资源(Resources):只读数据访问,相当于 GET
- 工具(Tools):执行业务逻辑,有副作用操作
- 提示(Prompts):可复用的消息模板
为什么需要 MCP?
- 旧方式:函数调用和插件
在 MCP 之前,每个 AI 供应商都有自己的“函数调用”或“插件”系统。 例如:
OpenAI 2023 年的函数调用 API 要求你为每个用例定义自定义的 JSON 模式。
Claude 有一个不同的机制,与 OpenAI 的格式不兼容。
如果你今天为 GPT-4 构建了一个连接器,明天就得为 Claude 重建一个。这导致了连接器数量的 N × M 爆炸式增长。
- MCP 登场:一座通用桥梁
MCP 提供了一个单一的协议(基于 HTTP 或 SSE 的 JSON-RPC 2.0),任何 LLM 客户端都可以使用它来调用你服务器的工具和资源。当一个 LLM 客户端连接时,它首先发送一个
initialize
请求。作为响应,你的服务器会明确通告它支持哪些工具、资源和提示。
完成这个握手后,AI 就可以:
- mcp/readResource → 获取数据,例如从数据库读取个人资料。
- mcp/callTool → 运行代码,例如 BMI 计算器。
- mcp/getPrompt → 检索可重用的消息模板。
因为它是标准化的,所以你无需为每个 AI 模型编写单独的连接器。
MCP 的主要优势
-
跨多个 AI 平台标准化。
-
基于 JSON-RPC 2.0: 一个众所周知的、稳定的协议。
-
支持流式 HTTP: 通过 HTTP + SSE,服务器可以持续下发事件给客户端。
-
能力通告: 服务器明确告知客户端可用的工具、资源和提示。
-
可扩展: 你可以动态添加或删除工具/资源,并自动通知任何监听的客户端。
⚡ 小提示:MCP 设计初衷就是“可复用、可发现、可流式”,方便快速构建复杂 LLM 应用
核心 MCP 概念
在编写代码之前,让我们用简单的语言回顾一下 MCP 的主要组成部分。
- 服务器 (Server)
一个 McpServer
是核心对象,负责管理:
-
能力 (Capabilities): 声明是否支持工具、资源和提示。
-
注册表 (Registry): 保存已注册的工具、资源和提示。
-
协议合规性: 处理传入的 JSON-RPC 消息 (initialize, callTool, readResource 等)。
在 Node.js (TypeScript/JavaScript) 中,你可以这样创建它:
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
capabilities: {
tools: { listChanged: true },
resources: { listChanged: true },
prompts: { listChanged: true }
}
});
- 工具 (Tools)
工具是 AI 可以调用来执行某些操作(计算或产生副作用)的函数。 注册工具时,你需要提供:
-
名称 (字符串), 例如:"calculate-bmi".
-
其参数的 Zod 模式(以便 MCP 可以自动验证)。
-
一个异步处理函数,接收解析后的参数并返回结果或错误。 示例:
server.tool(
'calculate-bmi',
{ weightKg: z.number(), heightM: z.number() },
async ({ weightKg, heightM }) => {
const bmi = weightKg / (heightM * heightM);
return {
content: [{ type: 'text', text: String(bmi) }]
};
}
);
注册后,AI 客户端可以进行 JSON-RPC 调用:
{
"jsonrpc": "2.0",
"id": 2,
"method": "mcp/callTool",
"params": {
"name": "calculate-bmi",
"arguments": { "weightKg": 70, "heightM": 1.75 }
}
}
- 资源 (Resources) 资源向 AI 暴露数据。它们类似于 "GET" 端点:
server.resource(
'user-profile',
new ResourceTemplate('users://{userId}/profile', { list: undefined }),
async (uri, { userId }) => {
// 例如,从你的数据库获取
return {
contents: [{
uri: uri.href,
text: `Profile data for user ${userId}`
}]
};
}
);
客户端随后可以发送:
{
"jsonrpc": "2.0",
"id": 4,
"method": "mcp/readResource",
"params": { "uri": "users://123/profile" }
}
- 提示 (Prompts) 提示是可重用的消息模板。它们帮助为 LLM 格式化请求:
server.prompt(
'review-code',
{ code: z.string() },
({ code }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: `Please review this code:\n\n${code}`
}
}]
})
);
然后,AI 可以执行:
{
"jsonrpc": "2.0",
"id": 5,
"method": "mcp/getPrompt",
"params": { "name": "review-code", "arguments": { "code": "const a = 1;" } }
}
Node.js 搭建完整 MCP 服务器
现在我们将构建一个完整的 Express 服务器,通过可流式 HTTP 暴露我们的 MCP 端点。
先决条件:
- Node.js v18+
- 依赖安装:
npm install express @modelcontextprotocol/sdk zod
我们将使用可流式 HTTP 传输 (Streamable HTTP transport) 来实现实时通知 (SSE)。
完整 server.js:
```js
/**
* server.js
*
* Express MCP Server (Streamable HTTP, Stateful)
*
* - 声明 MCP 能力(tools/resources/prompts)
* - 资源:config://app(静态)、users://{userId}/profile(动态)
* - 工具:calculate-bmi
* - 提示:review-code
* - 会话内持久化 McpServer 实例
*/
const express = require('express');
const { randomUUID } = require('crypto');
const { McpServer, ResourceTemplate } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
const { isInitializeRequest } = require('@modelcontextprotocol/sdk/types.js');
const { z } = require('zod');
const app = express();
app.use(express.json());
// 简单的内存会话表
const sessions = {};
function createMcpServer() {
const server = new McpServer({
name: 'example-server',
version: '1.0.0',
capabilities: {
tools: { listChanged: true },
resources: { listChanged: true },
prompts: { listChanged: true }
}
});
// 静态资源 config://app
server.resource(
'config',
'config://app',
async (uri) => {
return {
contents: [
{ uri: uri.href, text: 'App configuration here' }
]
};
}
);
// 动态资源 users://{userId}/profile
server.resource(
'user-profile',
new ResourceTemplate('users://{userId}/profile', { list: undefined }),
async (uri, { userId }) => {
return {
contents: [
{
uri: uri.href,
text: `Profile data for user ${userId}`
}
]
};
}
);
// 工具 calculate-bmi
server.tool(
'calculate-bmi',
{ weightKg: z.number(), heightM: z.number() },
async ({ weightKg, heightM }) => {
const bmi = weightKg / (heightM * heightM);
return {
content: [
{ type: 'text', text: String(bmi) }
]
};
}
);
// 提示 review-code
server.prompt(
'review-code',
{ code: z.string() },
({ code }) => {
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Please review this code:\n\n${code}`
}
}
]
};
}
);
return server;
}
// POST /mcp:初始化或复用会话
app.post('/mcp', async (req, res) => {
const sessionIdHeader = req.headers['mcp-session-id'];
let sessionEntry = null;
// 情况1:复用已存在的会话
if (sessionIdHeader && sessions[sessionIdHeader]) {
sessionEntry = sessions[sessionIdHeader];
// 情况2:无会话但这是初始化请求 → 建立新会话
} else if (!sessionIdHeader && isInitializeRequest(req.body)) {
const newSessionId = randomUUID();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => newSessionId,
onsessioninitialized: (sid) => {
sessions[sid] = { server, transport };
}
});
transport.onclose = () => {
if (transport.sessionId && sessions[transport.sessionId]) {
delete sessions[transport.sessionId];
}
};
const server = createMcpServer();
await server.connect(transport);
sessions[newSessionId] = { server, transport };
sessionEntry = sessions[newSessionId];
} else {
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
id: null
});
return;
}
// 将请求转交给本会话的 transport
await sessionEntry.transport.handleRequest(req, res, req.body);
});
// GET/DELETE /mcp:SSE 下行与关闭会话
async function handleSessionRequest(req, res) {
const sessionIdHeader = req.headers['mcp-session-id'];
if (!sessionIdHeader || !sessions[sessionIdHeader]) {
res.status(400).send('Invalid or missing session ID');
return;
}
const { transport } = sessions[sessionIdHeader];
await transport.handleRequest(req, res);
}
app.get('/mcp', handleSessionRequest);
app.delete('/mcp', handleSessionRequest);
// 启动
const PORT = 7171;
app.listen(PORT, () => {
console.log(`MCP Server listening on port ${PORT}`);
});
关键点说明
-
Express:我们使用
app.use(express.json())
,以便 Express 解析所有 JSON 请求体。 -
会话存储:我们维护一个内存中的对象
sessions
,以sessionId
为键,存储{ server, transport }
。这确保了每个客户端保持相同的McpServer
实例,因此工具在多次调用之间保持注册状态。 -
初始化流程
createMcpServer()
:-
我们传递
capabilities: { tools, resources, prompts }
并将listChanged
设为true
,以便在初始化握手期间,服务器明确通告可用的工具/资源/提示。 -
注册两个资源 (
config://app
和users://{userId}/profile)
、一个工具 (calculate-bmi
) 和一个提示 (review-code
)。
-
-
POST /mcp 处理程序:
-
如果
mcp-session-id
存在且匹配sessions
中的条目,我们将请求转发给该会话的transport.handleRequest(...)
。 -
如果没有
sessionId
且请求体是 MCP 初始化请求 (isInitializeRequest(req.body)
),我们创建一个新会话:-
生成 newSessionId = randomUUID()。
-
实例化 StreamableHTTPServerTransport,提供 sessionIdGenerator 和 onsessioninitialized 回调。
-
调用
createMcpServer()
来注册工具/资源/提示。 -
在发送任何响应之前调用
await server.connect(transport)
。 -
将
{ server, transport }
存储在sessions[newSessionId]
中。
-
-
最后,将 JSON-RPC 请求体转发给
transport.handleRequest(req, res, req.body)
,以便 MCP 服务器处理它。
-
-
GET/DELETE /mcp 处理程序:
- 客户端使用这些端点接收 SSE 通知 (GET) 或关闭会话 (DELETE)。我们再次在请求头中查找
sessionId
,找到对应的transport
并调用transport.handleRequest(req, res)
。
- 客户端使用这些端点接收 SSE 通知 (GET) 或关闭会话 (DELETE)。我们再次在请求头中查找
测试 MCP 服务器
有两种主要方法测试你的 MCP 服务器:
- 使用 cURL(手动发送 JSON-RPC 调用)。
- 使用 MCP Inspector(一个通过 npx 运行的交互式 CLI/UI 工具)。
启动服务器
在你的项目文件夹中打开终端并运行:
node server.js
你应该看到:
MCP Server listening on port 7171
这表示 Express 已启动并运行,准备在 http://localhost:7171/mcp 接受 MCP JSON-RPC 调用。
使用 cURL 测试
1. 初始化会话:
发送一个 initialize
请求。如果你想让你工具看到它,请确保包含 Authorization 头:
curl -i -X POST http://localhost:7171/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer my-secret-token" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": { "interactive": true },
"clientInfo": { "name": "example-client", "version": "1.0.0" }
}
}'
预期响应(示例)
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
mcp-session-id: 550e8400-e29b-41d4-a716-446655440000
Date: Sat, 05 Jun 2025 18:00:00 GMT
event: message
data: {
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"resources": { "listChanged": true },
"tools": { "listChanged": true },
"prompts": { "listChanged": true }
},
"serverInfo": {
"name": "example-server",
"version": "1.0.0",
"capabilities": {
"resources": { "listChanged": true },
"tools": { "listChanged": true },
"prompts": { "listChanged": true }
}
}
}
}
注意头信息 mcp-session-id: 550e8400-e29b-41d4-a716-446655440000
。你必须精确复制这个值(区分大小写)用于后续调用。
在 "result.capabilities" 中,你看到服务器通告支持工具、资源和提示。
2. 调用 calculate-bmi 工具:
使用上面获得的会话 ID:
curl -X POST http://localhost:7171/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "mcp/callTool",
"params": {
"name": "calculate-bmi",
"arguments": { "weightKg": 70, "heightM": 1.75 }
}
}'
预期响应:
event: message
data: {
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "22.857142857142858"
}
]
}
}
常见问题故障排除
以下是你可能遇到的最常见问题及其解决方法。
1. 调用工具时出现 "Method not found"
现象: 当你调用 mcp/callTool 时,得到:
{ "jsonrpc":"2.0", "id":2, "error": { "code": -32601, "message": "Method not found" } }
可能原因和解决方法:
- 未声明能力 (Capabilities): 如果你在
new McpServer(...)
中省略了capabilities
,客户端将不知道任何工具存在。务必包含:
const server = new McpServer({
name: 'example-server',
version: '1.0.0',
capabilities: {
tools: { listChanged: true },
resources: { listChanged: true },
prompts: { listChanged: true }
}
});
- 在
server.connect(...)
之后注册工具: 如果你在注册工具之前调用server.connect(transport)
,初始化握手永远不会通告这些工具。确保顺序是:
createMcpServer(); // 注册所有内容
await server.connect(transport);
- 每个请求都创建新的 McpServer 实例: 如果你在每次 POST 时重新创建
new McpServer(...)
(而不是每个会话重用同一个实例),后续调用将没有你的工具。使用会话映射来存储{ server, transport }
。
2. 缺少或不匹配的 mcp-session-id 现象: 400 Bad Request: No valid session ID provided。
解决: 始终包含初始化响应头中返回的精确会话 ID:
-H "mcp-session-id: 550e8400-e29b-41d4-a716-446655440000"
高级技巧:用于完全 HTTP 访问的低级处理程序
如果你绝对需要在工具内部访问原始的 HTTP 请求(用于 cookie、查询参数等),你可以绕过 server.tool(...)
,并为 CallToolRequestSchema
注册一个低级处理程序:
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const {
CallToolRequestSchema
} = require('@modelcontextprotocol/sdk/types.js');
const server = new Server(
{ name: 'low-level-server', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(CallToolRequestSchema, async (request) => {
// `request` 将包含:
// • request.params.name, request.params.arguments
// • request.transportContext (在某些传输实现中可能包含原始 HTTP)
// 你可以手动读取 `request.transportContext.headers`(如果传输层支持它)。
});
const transport = new StdioServerTransport();
await server.connect(transport);
然而,截至 2025 年中,MCP 的高级 SDK 不会将原始 HTTP 注入到高级的server.tool(...)
回调中。如果你需要完全访问 HTTP,请使用这种低级模式。
更多内容请查看 Cursor中文文档
更多Cursor使用技巧也可关注公众号 AI近距离
好了, 到这里文章就接近尾声了, 本文详细的介绍了:
- 什么是 MCP
- 为什需要 MCP
- 如何实现 Express MCP 服务器(含完整代码)
- 如何测试验证 MCP
认真按照步骤操作,你可以快速在 Node.js 中构建一个健壮的 MCP 服务器。