从零开发MCP Server:原理、用法与手写实战全解析

在AI生态高速发展的当下,大语言模型(LLM)与外部工具、数据源的集成需求日益迫切。MCP(Model Context Protocol,模型上下文协议)作为Anthropic推出的标准化通信协议,彻底解决了"多模型×多工具"的集成爆炸难题,成为AI与外部世界交互的"通用连接器"。

很多开发者在接触MCP时,会依赖官方提供的mcpfastmcp等库,但其实掌握MCP的底层原理后,完全可以纯手写一个无依赖、全兼容的MCP Server。本文将从MCP核心原理、实际用法出发,详细讲解如何手写MCP Server,并重点强调开发过程中的关键注意事项,帮助开发者快速上手。

一、MCP协议核心原理:读懂底层逻辑

MCP的本质是一套标准化的"客户端-服务器(C/S)"通信协议,核心目标是让LLM(通过MCP Client)与外部工具(通过MCP Server)实现"即插即用"的交互。其底层逻辑可拆解为3个核心部分,也是手写MCP Server的基础。

1.1 核心定位与价值

MCP类比为AI世界的"USB-C"或"HTTP",核心解决4大问题:

  • 集成爆炸:避免M个模型与N个工具需开发M×N个定制接口的繁琐;
  • 厂商锁定:打破各平台私有接口的壁垒,实现跨平台迁移;
  • 安全风险:内置权限、审批机制,避免敏感数据泄露;
  • 上下文割裂:让模型高效、结构化获取外部工具/资源的信息。

其核心价值在于"解耦"与"标准化":模型推理与外部能力分离,一套协议适配所有兼容模型与服务,大幅降低开发与维护成本。

1.2 通信架构与协议规范

MCP采用C/S架构,核心角色分为3类,但对于手写Server的开发者而言,只需关注"MCP Server"的实现(MCP Client已由Claude、Cursor等主流AI客户端内置):

  • MCP Host:用户交互入口(如Claude Desktop、Cursor);
  • MCP Client:协议处理层(客户端内置,负责与Server通信、路由消息);
  • MCP Server:能力提供层(开发者需要实现的部分,封装自定义工具/资源)。

通信的核心规则的是:

  1. 通信方式:通过标准输入(stdin)/标准输出(stdout)交换数据(最常用,适合本地工具;也支持HTTP/SSE用于远程服务);
  2. 协议格式:严格遵循JSON-RPC 2.0,所有消息均为JSON格式,包含请求、响应、通知3种类型;
  3. 数据流转:Client向Server发送JSON请求,Server处理后返回JSON响应,全程通过stdin读、stdout写完成。

1.3 手写Server的核心前提

无论是否依赖第三方库,MCP Server要被客户端识别并正常工作,必须满足两个核心前提:

  • 实现客户端强制调用的3个核心方法(缺一不可);
  • 严格遵循通信规范,确保消息格式与传输无误。

二、MCP的实际用法:无需自建Client,专注Server开发

很多开发者会有疑问:"开发MCP需要同时写Client和Server吗?"答案是:绝大多数场景下,完全不用自建MCP Client

2.1 为什么不用自建MCP Client?

主流AI客户端、IDE都已内置完整的MCP Client实现,例如:

  • Anthropic Claude Desktop / Claude API 封装层;
  • Cursor、VS Code等IDE(未来会深度内置);
  • 国内主流LLM客户端/SDK(逐步适配支持)。

开发者的核心工作,只是编写MCP Server,暴露自定义工具/资源,让内置Client主动连接即可------类比为"你写HTTP接口(Server),浏览器(Client)来调用,无需自己写浏览器"。

2.2 常见使用场景:多工具聚合Server

最推荐的用法是"一个MCP Server聚合多个工具"(Server聚合模式),优势在于:

  • 极简配置:只需在客户端配置一次,即可加载所有工具;
  • 资源共享:多个工具共享进程内存、数据库连接等,避免重复消耗;
  • 统一管理:便于权限控制、日志排查和版本迭代。

2.3 客户端配置方法(通用版)

无论你是用官方库还是手写Server,客户端配置都通过mcp.json文件实现,核心是告诉客户端"如何启动/连接你的MCP Server"。

2.3.1 配置文件结构(以stdio类型为例,最常用)

json 复制代码
{
  "mcpServers": {
    "自定义服务器名称": {
      "type": "stdio",  // 传输类型:stdio(本地)/http(远程)/sse
      "command": "python",  // 启动Server的命令
      "args": ["/绝对路径/到/你的/server脚本.py"],  // 脚本路径
      "env": {  // 环境变量(可选,用于传递密钥等)
        "API_KEY": "你的密钥"
      },
      "timeout": 30000  // 超时时间(毫秒)
    }
  }
}

2.3.2 配置文件存放位置

  • Claude Desktop:全局配置(~/.claude/mcp.json)、项目配置(项目根目录/.mcp.json);
  • Cursor:全局配置(~/.cursor/mcp.json)、项目配置(项目目录/.cursor/mcp.json);
  • VS Code:全局配置(用户配置目录/mcp.json)、项目配置(.vscode/mcp.json)。

优先级:项目配置 > 全局配置,配置完成后重启客户端即可生效。

三、从零手写MCP Server:无依赖实战(核心重点)

掌握以上原理和用法后,我们开始手写一个无依赖、全兼容的MCP Server(Python原生实现)。重点关注开发过程中的5个关键要点(后文会反复强调,也是新手最容易踩坑的地方)。

3.1 手写Server的核心要求(必记)

这5个要点直接决定Server能否正常工作,缺一不可:

  1. 必须用 stdin/stdout:通信只能通过标准输入读、标准输出写,不能用print打日志(会破坏协议格式,导致客户端解析失败);
  2. 必须flush输出:每次通过stdout返回响应后,必须调用sys.stdout.flush()(强制刷新缓冲区,否则客户端收不到消息);
  3. 必须实现3个核心方法:initialize(初始化握手)、tools/list(返回工具列表)、tools/call(执行工具调用);
  4. 工具参数必须用JSON Schema:每个工具的输入参数需用JSON Schema描述,客户端会据此校验参数合法性;
  5. 无依赖=全平台兼容:采用Python原生库开发,不引入任何第三方包,可在Windows/Mac/Linux全平台运行。

3.2 完整手写代码(无依赖,可直接运行)

以下代码实现了一个聚合3个工具(加法、获取时间、回声)的MCP Server,注释详细,可直接复制运行,后续新增工具只需简单修改。

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
纯手写MCP Server(无任何第三方依赖)
核心:stdin/stdout通信 + JSON-RPC 2.0 协议
满足所有MCP客户端兼容要求,支持多工具聚合
"""
import sys
import json
import traceback
from datetime import datetime


# ===================== 1. 自定义工具实现(可无限扩展) =====================
# 工具1:加法计算
def add(a: int, b: int) -> int:
    """计算两个整数的和"""
    return a + b

# 工具2:获取当前系统时间
def get_time() -> str:
    """返回当前格式化时间(YYYY-MM-DD HH:MM:SS)"""
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

# 工具3:回声功能(原样返回输入文本)
def echo(text: str) -> str:
    """原样返回输入的文本,用于测试通信"""
    return f"你输入的内容:{text}"

# 工具注册表:key=工具名(客户端调用时使用),value=工具函数对象
TOOLS = {
    "add": add,
    "get_time": get_time,
    "echo": echo
}


# ===================== 2. MCP协议核心方法实现(3个必写方法) =====================
def get_tool_definitions():
    """
    生成MCP标准格式的工具描述(客户端用于识别工具)
    每个工具必须包含:name(工具名)、description(描述)、inputSchema(参数校验)
    inputSchema 遵循JSON Schema规范,客户端据此校验参数
    """
    return [
        {
            "name": "add",
            "description": "计算两个整数的和",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "a": {"type": "integer", "description": "第一个整数"},
                    "b": {"type": "integer", "description": "第二个整数"}
                },
                "required": ["a", "b"]  # 必传参数
            }
        },
        {
            "name": "get_time",
            "description": "获取当前系统的格式化时间",
            "inputSchema": {
                "type": "object",
                "properties": {},  # 无参数
                "required": []     # 无必传参数
            }
        },
        {
            "name": "echo",
            "description": "原样返回输入的文本,用于测试MCP通信是否正常",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "text": {"type": "string", "description": "需要返回的文本"}
                },
                "required": ["text"]
            }
        }
    ]


def handle_json_rpc(request: dict) -> dict:
    """
    处理客户端发送的所有JSON-RPC请求
    核心逻辑:解析请求方法,调用对应处理逻辑,返回标准JSON响应
    """
    # 提取请求核心信息(JSON-RPC 2.0 标准字段)
    request_id = request.get("id")  # 请求唯一标识,响应需对应
    method = request.get("method")  # 调用的方法名
    params = request.get("params", {})  # 方法参数

    # 方法1:initialize(必写)------ 初始化握手,客户端启动后首先调用
    if method == "initialize":
        return {
            "jsonrpc": "2.0",  # 固定为2.0(JSON-RPC版本)
            "id": request_id,
            "result": {
                "protocolVersion": "2025-06-18",  # MCP协议版本(固定值)
                     "capabilities": {
                    "tools": {}  # 声明支持 tools 能力
                },
                "serverInfo": {
                    "name": "My-Custom-MCP-Server",
                    "version": "1.0.0"
                }
            }
    # 方法2:tools/list(必写)------ 客户端调用此方法获取当前Server提供的所有工具列表
    elif method == "tools/list":
        try:
            # 调用我们之前定义的 get_tool_definitions 函数获取工具描述列表
            tool_list = get_tool_definitions()
            return {
                "jsonrpc": "2.0",
                "id": request_id,
                "result": {
                    "tools": tool_list
                }
            }
        except Exception as e:
            # 异常处理:如果工具列表生成失败,返回错误信息
            return {
                "jsonrpc": "2.0",
                "id": request_id,
                "error": {
                    "code": -32603, # Internal error
                    "message": f"Failed to list tools: {str(e)}"
                }
            }

    # 方法3:tools/call(必写)------ 客户端调用此方法执行具体的工具函数
    elif method == "tools/call":
        try:
            # 1. 解析参数:获取要调用的工具名 (name) 和输入参数 (arguments)
            tool_name = params.get("name")
            arguments = params.get("arguments", {})

            # 2. 校验工具是否存在
            if tool_name not in TOOLS:
                return {
                    "jsonrpc": "2.0",
                    "id": request_id,
                    "error": {
                        "code": -32601, # Method not found
                        "message": f"Tool '{tool_name}' not found"
                    }
                }

            # 3. 执行工具函数
            tool_function = TOOLS[tool_name]
            # 注意:这里直接解包 arguments 字典作为函数参数
            # 实际生产中建议增加 try-except 包裹函数执行,避免单个工具崩溃导致 Server 退出
            result = tool_function(**arguments)

            # 4. 返回执行结果
            return {
                "jsonrpc": "2.0",
                "id": request_id,
                "result": {
                    "content": [ # MCP 标准要求 content 是一个数组
                        {
                            "type": "text",
                            "text": str(result) # 所有结果统一转为字符串返回
                        }
                    ]
                }
            }
        except Exception as e:
            # 捕获工具执行过程中的异常,返回给客户端展示
            return {
                "jsonrpc": "2.0",
                "id": request_id,
                "error": {
                    "code": -32500, # Application error (自定义)
                    "message": f"Tool execution failed: {str(e)}",
                    "data": traceback.format_exc() # 携带堆栈信息便于调试
                }
            }

    # 处理未知方法(可选,标准 JSON-RPC 规范)
    else:
        return {
            "jsonrpc": "2.0",
            "id": request_id,
            "error": {
                "code": -32601,
                "message": f"Method {method} not supported"
            }
        }
3.2.2 实现主循环(Main Loop)

完成了请求处理函数,最后我们需要一个死循环来持续监听 stdin,读取客户端发来的数据。这是 MCP Server 的入口。

特别注意

  1. 读取方式 :必须使用 sys.stdin.buffer.readline() 读取二进制行,因为 MCP 协议头是二进制格式(Content-Length)。
  2. 输出刷新 :每次 print 后必须紧跟 sys.stdout.flush(),否则客户端会一直等待。
python 复制代码
def main():
    """
    MCP Server 主循环
    持续从 stdin 读取数据,处理请求,写入 stdout
    """
    # 设置标准输出为无缓冲模式,确保消息立即发送
    sys.stdout.reconfigure(line_buffering=True) if hasattr(sys.stdout, 'reconfigure') else None
    
    # 启动日志(注意:这里不能用普通 print,或者确保不影响协议)
    # 实际开发中,建议将调试日志写入文件,避免污染 stdout
    print("MCP Server Started", file=sys.stderr) 

    # 主循环
    while True:
        try:
            # 1. 读取 HTTP-like 头部(获取 Content-Length)
            # MCP 基于 stdio 时,每条消息前都有一个头部,格式如:Content-Length: 123\r\n\r\n
            header = ""
            while True:
                line = sys.stdin.buffer.readline()
                if not line:
                    # 客户端断开连接
                    return
                header += line.decode("utf-8")
                # 头部以 \r\n\r\n 结尾
                if header.strip() == "":
                    break

            # 2. 解析 Content-Length
            # 从 header 字符串中提取数字
            content_length = 0
            for line in header.split("\r\n"):
                if line.lower().startswith("content-length:"):
                    content_length = int(line.split(":", 1)[1].strip())
                    break

            if content_length == 0:
                continue

            # 3. 读取 JSON Body
            # 根据 Content-Length 读取指定字节数
            raw_body = sys.stdin.buffer.read(content_length).decode("utf-8")
            
            # 4. 解析 JSON 并处理请求
            request = json.loads(raw_body)
            response = handle_json_rpc(request)

            # 5. 构造响应并发送
            # MCP 响应也需要带上 Content-Length 头
            response_body = json.dumps(response, ensure_ascii=False)
            response_header = f"Content-Length: {len(response_body.encode('utf-8'))}\r\n\r\n"
            
            # 关键点:必须先写 Header,再写 Body
            sys.stdout.write(response_header)
            sys.stdout.write(response_body)
            
            # 关键点:必须强制刷新缓冲区!
            sys.stdout.flush()

        except KeyboardInterrupt:
            # 允许用户手动中断
            break
        except Exception as e:
            # 防御性编程:主循环不能因为一次错误而崩溃
            # 这里可以写入 debug 日志文件,或者发送一个通用错误给客户端
            error_response = {
                "jsonrpc": "2.0",
                "id": None,
                "error": {
                    "code": -32000,
                    "message": f"Server internal error: {str(e)}"
                }
            }
            # 即使出错也要尝试发送错误信息并刷新
            try:
                sys.stdout.write(f"Content-Length: {len(json.dumps(error_response))}\r\n\r\n")
                sys.stdout.write(json.dumps(error_response))
                sys.stdout.flush()
            except:
                pass # 如果连错误都发不出去,只能放弃了

# 程序入口
if __name__ == "__main__":
    main()

四、开发过程中的关键注意事项(避坑指南)

虽然代码看起来只有百余行,但在实际调试中,开发者常因以下细节导致"明明代码没错,但客户端就是连不上"的问题。

4.1 日志输出的陷阱
  • 问题 :在 main 循环中使用 print("Debug...") 打印调试信息。
  • 后果print 会直接写入 stdout,客户端会误认为这是协议数据,导致解析 JSON 失败,报 Invalid response 错误。
  • 解决方案
    • 方案 A(推荐):将日志写入文件。
    • 方案 B :使用 print("Debug", file=sys.stderr)。标准错误流(stderr)不会干扰标准输出流(stdout)的协议通信。
4.2 缓冲区刷新(Flush)的必要性
  • 原理 :操作系统为了提高效率,会将写入 stdout 的数据暂存在缓冲区中,直到缓冲区满或程序结束才真正发送给客户端。
  • 现象:客户端一直转圈等待,直到超时,而 Server 端其实早就处理完了。
  • 解决方案 :正如我们在核心要求中强调的,**每一次 sys.stdout.write 之后,必须紧跟 **sys.stdout.flush()。这是 MCP Server 能够实时响应的关键。
4.3 JSON 序列化的兼容性
  • 编码问题:MCP 协议要求使用 UTF-8 编码。
  • 非 ASCII 字符 :如果工具返回的内容包含中文或特殊字符,json.dumps 时请务必设置 ensure_ascii=False,否则客户端收到的将是 Unicode 转义字符(如 \u4f60\u597d),导致显示乱码。
4.4 异常处理的健壮性
  • 单点故障 :如果 tools/call 中的某个工具(比如 add)抛出了未捕获的异常(例如除零错误),且没有在 handle_json_rpc 中进行 try-except 包裹,整个 Python 进程会崩溃退出。
  • 后果:客户端会报"连接被重置",需要重启客户端才能重新连接。
  • 最佳实践 :在 tools/call 的逻辑分支中,必须用 try-except 包裹具体的函数执行代码,将错误转化为 MCP 协议规定的 Error 对象返回,保证 Server 进程永不退出。

五、总结与展望

通过本文的讲解,我们不仅了解了 MCP 协议作为"AI 通用连接器"的核心价值,更重要的是,我们亲手剥离了所有第三方库的封装,使用 Python 原生能力实现了一个最精简的 MCP Server。

重点

  1. 通信基石stdin/stdout 是连接的唯一通道。
  2. 协议规范Content-Length 头部 + JSON-RPC 2.0 格式。
  3. 三大方法initialize 握手,tools/list 暴露能力,tools/call 执行业务。
相关推荐
313YPHU311 小时前
【MCP】第二章 MCP 实战
mcp
zhangshuang-peta11 小时前
MCP 在企业架构中的位置:它该放在哪一层?
人工智能·架构·ai agent·mcp·peta
zhangshuang-peta12 小时前
MCP 与 AI Agent:为什么 Agent 离不开协议?
人工智能·ai agent·mcp·peta
zhangshuang-peta13 小时前
MCP 的执行与回执:如何让每一步可追踪、可验证、可审计?
人工智能·ai agent·mcp·peta
313YPHU314 小时前
【MCP】第一章 初识 MCP
mcp
婷婷_17216 小时前
【PCIe验证每日学习·Day21】PCIe复位机制与功能级复位(FLR)全解析
学习·程序人生·芯片·pcie·芯片验证·链路恢复·pcie 复位
郝学胜-神的一滴17 小时前
图形学基础:OpenGL、图形引擎与IG的核心认知及核心模式解析
开发语言·c++·qt·程序人生·图形渲染
本旺17 小时前
[AIAgent-MCP]MCP Client 全景:Elicitation、Roots 与 Sampling 一次讲透
人工智能·ai·aiagent·mcp