MCP 服务器开发指南
本文面向 FancyHelper 开发者,详细说明了 MCP 协议规范。MCP 基于 JSON-RPC 2.0,支持 HTTP 和 SSE 两种传输方式。服务器需按顺序实现 initialize、notifications/initialized、tools/list 和 tools/call 等方法,HTTP 模式下还需处理 ping。文档还规定了消息格式、错误码及约束,并提供了 Node.js 和 Python 的实现示例。
概述
本文档面向 MCP 服务器的开发者,说明 FancyHelper 使用的 MCP 协议规范和服务器需要实现的方法。
MCP 基于 JSON-RPC 2.0 协议。FancyHelper 作为客户端,连接到开发者编写的 MCP 服务器,调用服务器暴露的工具。
传输方式
FancyHelper 支持两种传输方式,MCP 服务器可以选择实现其中一种或同时支持两种:
HTTP 传输
客户端直接向配置的 URL 发送 HTTP POST 请求,请求头包含:
Content-Type: application/json
Accept: application/json, text/event-stream
Authorization: Bearer <api_key> (如果配置了 api_key)服务器只需实现一个 POST 端点,接收 JSON-RPC 请求并返回 JSON 响应。这是最简单的实现方式。
客户端配置:transport: "auto" 或 transport: "http"
SSE 传输
客户端分两步连接:
向配置的 URL 发起 GET 请求,
Accept: text/event-stream,建立 SSE 连接服务器通过 SSE 事件发送消息端点 URL
客户端后续 JSON-RPC 请求全部发送到消息端点
SSE 事件格式:
event: endpoint
data: http://<host>:<port>/messages其中 data 为字符串格式的消息端点 URL。如果 data 为 JSON 对象,则读取 uri 字段。
客户端配置:transport: "sse"
协议流程
连接建立后,客户端按以下顺序依次调用:
initialize → 协议握手,协商版本和能力
notifications/initialized → 通知握手完成(无响应)
tools/list → 获取可用工具列表
tools/call → 调用具体工具(可多次调用)HTTP 模式下,客户端还会每 30 秒发送一次 ping 保持连接。
JSON-RPC 消息格式
请求
{
"jsonrpc": "2.0",
"id": "<string>",
"method": "<string>",
"params": { ... }
}字段 | 类型 | 说明 |
|---|---|---|
|
| 固定为 |
|
| 请求 ID,响应中需原样返回 |
|
| 方法名 |
|
| 方法参数 |
成功响应
{
"jsonrpc": "2.0",
"id": "<原请求 ID>",
"result": { ... }
}错误响应
{
"jsonrpc": "2.0",
"id": "<原请求 ID 或 null>",
"error": {
"code": -32601,
"message": "<错误描述>"
}
}通知(Notification)
通知的请求中没有 id 字段,服务器无需返回任何数据,HTTP 状态码返回 202 Accepted 即可。
{
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
}方法参考
initialize
握手请求,客户端连接后首先发送此方法。服务器需返回协议版本和能力声明。
请求参数:
字段 | 类型 | 说明 |
|---|---|---|
|
| 客户端支持的协议版本 |
|
| 客户端信息,包含 |
|
| 客户端能力声明 |
响应字段:
字段 | 类型 | 说明 |
|---|---|---|
|
| 服务器使用的协议版本 |
|
| 服务器能力声明 |
|
| 服务器信息,包含 |
请求示例:
{
"jsonrpc": "2.0",
"id": "1",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"clientInfo": { "name": "FancyHelper", "version": "1.0.0" },
"capabilities": {}
}
}响应示例:
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": { "listChanged": true }
},
"serverInfo": {
"name": "MyMCPServer",
"version": "1.0.0"
}
}
}notifications/initialized
客户端在 initialize 成功后发送此通知。服务器收到后即可进入正常工作状态。
此方法为 Notification,无需返回数据。
{
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
}tools/list
客户端请求服务器返回可用的工具列表。
请求参数: 无
响应字段:
字段 | 类型 | 说明 |
|---|---|---|
|
| 工具列表 |
|
| 工具名称,同一服务器内唯一 |
|
| 工具描述,帮助 AI 理解工具用途 |
|
| 参数 Schema,JSON Schema 格式 |
请求示例:
{
"jsonrpc": "2.0",
"id": "2",
"method": "tools/list",
"params": {}
}响应示例:
{
"jsonrpc": "2.0",
"id": "2",
"result": {
"tools": [
{
"name": "get_weather",
"description": "获取指定城市的天气信息",
"inputSchema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称"
}
},
"required": ["city"]
}
}
]
}
}tools/call
客户端请求执行指定工具。
请求参数:
字段 | 类型 | 说明 |
|---|---|---|
|
| 工具名称 |
|
| 工具参数,对应 |
响应字段:
字段 | 类型 | 说明 |
|---|---|---|
|
| 执行结果内容列表 |
|
| 内容类型,目前仅处理 |
|
| 文本内容 |
|
| 是否执行出错 |
请求示例:
{
"jsonrpc": "2.0",
"id": "3",
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "city": "Beijing" }
}
}成功响应示例:
{
"jsonrpc": "2.0",
"id": "3",
"result": {
"content": [
{ "type": "text", "text": "北京当前天气:晴,28°C" }
],
"isError": false
}
}执行失败响应示例:
{
"jsonrpc": "2.0",
"id": "3",
"result": {
"content": [
{ "type": "text", "text": "查询失败:API 服务不可用" }
],
"isError": true
}
}ping(仅 HTTP 模式)
客户端在 HTTP 模式下每 30 秒发送一次心跳检测。
{
"jsonrpc": "2.0",
"id": "4",
"method": "ping",
"params": {}
}响应:
{
"jsonrpc": "2.0",
"id": "4",
"result": {}
}错误码
JSON-RPC 标准错误码:
错误码 | 含义 |
|---|---|
| JSON 解析错误 |
| 无效请求 |
| 方法不存在 |
| 无效参数 |
| 内部错误 |
约束与注意事项
initialize必须在任何其他请求之前处理通知类消息(方法名以
notifications/开头)的请求缺少id字段,不应返回响应体content数组中type为"text"以外的内容项,FancyHelper 当前不会处理,会跳过content[].text为null时,FancyHelper 会显示为(空结果)工具名和服务器的
name字段只能包含字母、数字、下划线和连字符HTTP 模式的服务器需要在一处 URL 同时处理
initialize、tools/list、tools/call和ping所有方法
实现示例
Node.js
const http = require('http');
const TOOLS = [
{
name: 'greet',
description: '向指定用户打招呼',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: '用户名' }
},
required: ['name']
}
}
];
function handleRequest(body) {
const req = JSON.parse(body);
const { id, method, params } = req;
switch (method) {
case 'initialize':
return JSON.stringify({
jsonrpc: '2.0', id,
result: {
protocolVersion: '2024-11-05',
capabilities: { tools: { listChanged: true } },
serverInfo: { name: 'MyServer', version: '1.0.0' }
}
});
case 'notifications/initialized':
return null;
case 'tools/list':
return JSON.stringify({
jsonrpc: '2.0', id,
result: { tools: TOOLS }
});
case 'tools/call':
if (params.name === 'greet') {
return JSON.stringify({
jsonrpc: '2.0', id,
result: {
content: [{ type: 'text', text: `Hello, ${params.arguments.name}!` }],
isError: false
}
});
}
return JSON.stringify({
jsonrpc: '2.0', id,
error: { code: -32601, message: 'Unknown tool' }
});
case 'ping':
return JSON.stringify({ jsonrpc: '2.0', id, result: {} });
default:
return JSON.stringify({
jsonrpc: '2.0', id,
error: { code: -32601, message: 'Unknown method' }
});
}
}
const server = http.createServer((req, res) => {
if (req.method === 'POST') {
let body = '';
req.on('data', c => body += c);
req.on('end', () => {
const response = handleRequest(body);
if (response === null) {
res.writeHead(202);
res.end();
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(response);
}
});
}
});
server.listen(3000);运行后配置 FancyHelper:
mcp:
client:
enabled: true
servers:
- name: "my-server"
url: "http://localhost:3000/mcp"Python
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
TOOLS = [{
"name": "get_time",
"description": "获取当前服务器时间",
"inputSchema": {"type": "object", "properties": {}}
}]
class MCPHandler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers['Content-Length'])
body = self.rfile.read(length).decode()
req = json.loads(body)
id = req.get('id')
method = req.get('method')
params = req.get('params', {})
if method == 'initialize':
response = {
"jsonrpc": "2.0", "id": id,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {"listChanged": True}},
"serverInfo": {"name": "PyServer", "version": "1.0.0"}
}
}
elif method == 'notifications/initialized':
self.send_response(202)
self.end_headers()
return
elif method == 'tools/list':
response = {"jsonrpc": "2.0", "id": id, "result": {"tools": TOOLS}}
elif method == 'tools/call':
response = {
"jsonrpc": "2.0", "id": id,
"result": {
"content": [{"type": "text", "text": "Hello from Python!"}],
"isError": False
}
}
elif method == 'ping':
response = {"jsonrpc": "2.0", "id": id, "result": {}}
else:
response = {"jsonrpc": "2.0", "id": id, "error": {"code": -32601, "message": "unknown method"}}
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(response).encode())
HTTPServer(('', 3000), MCPHandler).serve_forever()调试
使用 curl 可直接测试 MCP 服务器的响应:
# 测试 initialize
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"test","version":"1.0"},"capabilities":{}}}'
# 测试 tools/list
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"2","method":"tools/list","params":{}}'
# 测试 tools/call
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"greet","arguments":{"name":"test"}}}'