MCP 实战 —— 从零写一个 CRC16 服务器(保姆级)

你已读过 \[MCP(模型上下文协议)],知道它是什么、为什么、架构怎样。但没亲手写过一个是吧?

这篇带你从零到跑通 :装环境 → 写一个算 CRC16 的 MCP Server → 接入 Claude Code → 调用成功 → 看懂底层协议每条消息 → 会调试排错。

用 Python 官方 mcp SDK 的 FastMCP 风格(代码最短),实现一个嵌入式风格 CRC16 校验工具。每一步都给完整命令和代码,照抄即可。
本教程结束时你拥有的东西

  1. 一个能跑的 MCP Server(crc_server.py),暴露一个 crc16 工具
  2. Claude Code 能调用它:"帮我算 48656C6C6F 的 CRC16" → 模型自动调你的工具 → 返回校验值
  3. 彻底搞懂 MCP 在底层跑的是什么(JSON-RPC 握手 → 列工具 → 调工具的全流程)
  4. 出错时知道怎么查(Inspector + stderr 日志)

一、先搞清:你写的 Server 在整个流程里是哪一块

复习 \[MCP(模型上下文协议)] 的架构,但聚焦"你写的那块":
#mermaid-svg-IdQgMAKEqIZ6WYqV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IdQgMAKEqIZ6WYqV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IdQgMAKEqIZ6WYqV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IdQgMAKEqIZ6WYqV .error-icon{fill:#552222;}#mermaid-svg-IdQgMAKEqIZ6WYqV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IdQgMAKEqIZ6WYqV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IdQgMAKEqIZ6WYqV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IdQgMAKEqIZ6WYqV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IdQgMAKEqIZ6WYqV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IdQgMAKEqIZ6WYqV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IdQgMAKEqIZ6WYqV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IdQgMAKEqIZ6WYqV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IdQgMAKEqIZ6WYqV .marker.cross{stroke:#333333;}#mermaid-svg-IdQgMAKEqIZ6WYqV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IdQgMAKEqIZ6WYqV p{margin:0;}#mermaid-svg-IdQgMAKEqIZ6WYqV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IdQgMAKEqIZ6WYqV .cluster-label text{fill:#333;}#mermaid-svg-IdQgMAKEqIZ6WYqV .cluster-label span{color:#333;}#mermaid-svg-IdQgMAKEqIZ6WYqV .cluster-label span p{background-color:transparent;}#mermaid-svg-IdQgMAKEqIZ6WYqV .label text,#mermaid-svg-IdQgMAKEqIZ6WYqV span{fill:#333;color:#333;}#mermaid-svg-IdQgMAKEqIZ6WYqV .node rect,#mermaid-svg-IdQgMAKEqIZ6WYqV .node circle,#mermaid-svg-IdQgMAKEqIZ6WYqV .node ellipse,#mermaid-svg-IdQgMAKEqIZ6WYqV .node polygon,#mermaid-svg-IdQgMAKEqIZ6WYqV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IdQgMAKEqIZ6WYqV .rough-node .label text,#mermaid-svg-IdQgMAKEqIZ6WYqV .node .label text,#mermaid-svg-IdQgMAKEqIZ6WYqV .image-shape .label,#mermaid-svg-IdQgMAKEqIZ6WYqV .icon-shape .label{text-anchor:middle;}#mermaid-svg-IdQgMAKEqIZ6WYqV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IdQgMAKEqIZ6WYqV .rough-node .label,#mermaid-svg-IdQgMAKEqIZ6WYqV .node .label,#mermaid-svg-IdQgMAKEqIZ6WYqV .image-shape .label,#mermaid-svg-IdQgMAKEqIZ6WYqV .icon-shape .label{text-align:center;}#mermaid-svg-IdQgMAKEqIZ6WYqV .node.clickable{cursor:pointer;}#mermaid-svg-IdQgMAKEqIZ6WYqV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IdQgMAKEqIZ6WYqV .arrowheadPath{fill:#333333;}#mermaid-svg-IdQgMAKEqIZ6WYqV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IdQgMAKEqIZ6WYqV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IdQgMAKEqIZ6WYqV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IdQgMAKEqIZ6WYqV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IdQgMAKEqIZ6WYqV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IdQgMAKEqIZ6WYqV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IdQgMAKEqIZ6WYqV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IdQgMAKEqIZ6WYqV .cluster text{fill:#333;}#mermaid-svg-IdQgMAKEqIZ6WYqV .cluster span{color:#333;}#mermaid-svg-IdQgMAKEqIZ6WYqV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IdQgMAKEqIZ6WYqV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IdQgMAKEqIZ6WYqV rect.text{fill:none;stroke-width:0;}#mermaid-svg-IdQgMAKEqIZ6WYqV .icon-shape,#mermaid-svg-IdQgMAKEqIZ6WYqV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IdQgMAKEqIZ6WYqV .icon-shape p,#mermaid-svg-IdQgMAKEqIZ6WYqV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IdQgMAKEqIZ6WYqV .icon-shape .label rect,#mermaid-svg-IdQgMAKEqIZ6WYqV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IdQgMAKEqIZ6WYqV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IdQgMAKEqIZ6WYqV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IdQgMAKEqIZ6WYqV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 你写的(本教程产出)
Claude Code(Host,你正在用的)
JSON-RPC over stdio

(stdin/stdout)
决定调工具
tools/call
算 CRC
返回结果
喂回模型
Claude 模型
MCP Client

(Claude Code 内置)
crc_server.py

FastMCP
crc16 工具函数

关键认知:

  • Claude Code 既是 Host 也是 Client(它内置了 MCP Client)
  • 你的 Server 是一个独立进程 ,Claude Code 用 python crc_server.py 把它拉起来,通过进程的 stdin/stdout 收发 JSON-RPC 消息
  • 传输方式是 stdio(标准输入输出),不是网络。本地 MCP Server 默认都走 stdio,简单、无需端口
  • 模型本身不直接调 你的工具;模型说"我要调 crc16",Claude Code 的 Client 代它发 tools/call 给你的 Server

嵌入式视角

这套架构 ≈ 你 MCU 上的"主控 + 协处理器":Claude Code 是主控(决定干啥),你的 Server 是协处理器(执行具体计算),两者靠"串口协议"(JSON-RPC over stdio)通信。你写的 Server 只管"收到命令 → 干活 → 回结果",不用管模型怎么决策。


二、第一步:环境准备(5 分钟)

2.1 确认 Python 版本

MCP SDK 需要 Python 3.10+。打开 PowerShell:

powershell 复制代码
python --version
# 应显示 3.10 或更高,如 Python 3.12.x

2.2 建项目目录 + 虚拟环境

powershell 复制代码
# 找个干净地方建目录(别放 vault 里,vault 是笔记库)
mkdir E:\mcp-crc
cd E:\mcp-crc

# 建虚拟环境(隔离依赖,不污染系统 Python)
python -m venv .venv

# 激活(Windows PowerShell)
.venv\Scripts\Activate.ps1
# 激活后命令行前面会出现 (.venv) 字样

激活失败?

如果 PowerShell 报"无法加载脚本,因为在此系统上禁止运行脚本",执行一次:

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

然后重新激活。

2.3 装 MCP SDK

powershell 复制代码
# 确保已激活 .venv(命令行前有 (.venv))
# ★ 必须装 mcp 1.x,不能装 2.x(见下方说明)
pip install "mcp<2"

!warning 版本坑:1.x 和 2.x 的 API 不同,别装错

pip install mcp 默认装最新的 2.x ,但 2.x 改了类名和导入路径。本教程主体用 1.x 的 FastMCP(代码更直观)。两个版本的差别只有两行,下面说清怎么选、怎么改。

装 1.x(教程原样跑) :pip install "mcp<2"

装 2.x(官方主推,改两行) :pip install mcp,代码改成:

python 复制代码
# 2.x 版:只改这两行,其余(装饰器、函数、run)完全不变
from mcp.server.mcpserver import MCPServer   # 1.x: from mcp.server.fastmcp import FastMCP
mcp = MCPServer("crc-server")                # 1.x: mcp = FastMCP("crc-server")

@mcp.tool()                                   # 1.x 2.x 一样
def crc16(hex_data: str) -> str:
    ...

if __name__ == "__main__":
    mcp.run()                                 # 1.x 2.x 一样
1.x 2.x
导入 from mcp.server.fastmcp import FastMCP from mcp.server.mcpserver import MCPServer
类名 FastMCP MCPServer
装饰器 @mcp.tool() 一样 一样
mcp.run() 一样 一样

怎么选 :跟教程求稳用 1.x;追新、看官方最新文档用 2.x。两者业务代码 99% 兼容。详见迁移指南

验证装上了:

powershell 复制代码
pip show mcp
# 应显示 Name: mcp, Version: 1.x.x

装的是啥

mcp 是 Anthropic 官方 Python SDK。里面包含 FastMCP(高层简化 API,本教程用)和底层 Server 类(\[MCP(模型上下文协议)] §7.2 那个示例用的就是底层 API,代码更长)。本教程用 FastMCP,代码量减半。


三、第二步:写 Server(逐行讲解)

在 E:\mcp-crc\ 下新建 crc_server.py,内容如下:

python 复制代码
# crc_server.py ------ 一个最小 MCP Server,暴露 crc16 工具
# ★ 1.x→2.x 差异只有下面两行,其余代码(装饰器/函数/run)完全不变:
#    1.x: from mcp.server.fastmcp import FastMCP    /  mcp = FastMCP("crc-server")
#    2.x: from mcp.server.mcpserver import MCPServer /  mcp = MCPServer("crc-server")
#    本教程用 1.x;想用 2.x 把这两行换掉即可。
from mcp.server.fastmcp import FastMCP   # ← 2.x 改成: from mcp.server.mcpserver import MCPServer

# ① 创建 Server 实例,名字随便取(会报给 Client)
mcp = FastMCP("crc-server")              # ← 2.x 改成: mcp = MCPServer("crc-server")


# ② 用装饰器把普通函数注册成 MCP 工具
@mcp.tool()
def crc16(hex_data: str) -> str:
    """计算给定十六进制字节序列的 CRC16-CCITT 校验值。

    参数:
        hex_data: 十六进制字符串,如 "48656C6C6F" 表示 "Hello" 的字节

    返回:
        4 位十六进制 CRC 校验值(大写),如 "C86E"
    """
    # 把十六进制字符串转成字节
    data = bytes.fromhex(hex_data)

    # CRC16-CCITT-FALSE:poly=0x1021, init=0xFFFF
    crc = 0xFFFF
    for byte in data:
        crc ^= byte << 8
        for _ in range(8):
            if crc & 0x8000:
                crc = (crc << 1) ^ 0x1021
            else:
                crc <<= 1
            crc &= 0xFFFF
    return f"{crc:04X}"


# ③ 启动 Server,默认走 stdio 传输
if __name__ == "__main__":
    mcp.run()

3.1 逐行讲解

代码 作用
from mcp.server.fastmcp import FastMCP 引入高层 API。FastMCP 帮你处理 JSON-RPC、传输、schema 生成,你只写业务函数
mcp = FastMCP("crc-server") 创建 Server 实例。"crc-server" 是服务名,会出现在 serverInfo 里,给 Client 识别用
@mcp.tool() 核心魔法 :把下面的普通函数注册成 MCP 工具。FastMCP 自动从类型标注 (hex_data: str)生成参数 schema,从docstring 生成工具描述
def crc16(hex_data: str) -> str: 普通函数,该咋写咋写。类型标注必须有------Client 据此知道参数是字符串
"""计算...""" docstring 会成为工具的描述。模型靠这段话判断"该不该调这个工具",写清楚等于给模型下说明书
return f"{crc:04X}" 返回字符串。FastMCP 自动包成 TextContent 返回给 Client
mcp.run() 启动事件循环,在 stdio 上监听 JSON-RPC 消息。调用后会阻塞,等 Client 发消息

3.2 先自测算法(不碰 MCP,确认 CRC 算对)

这一步很重要------先确认业务逻辑对,再接协议层(和 \[gtest详解-嵌入式逻辑PC化测试] 一个思路:逻辑层先在 PC 测通)。

新建 verify.py:

python 复制代码
# verify.py ------ 纯算法自测,不依赖 MCP
def crc16(data: bytes) -> int:
    crc = 0xFFFF
    for b in data:
        crc ^= b << 8
        for _ in range(8):
            crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
    return crc


if __name__ == "__main__":
    # CRC16-CCITT-FALSE 的标准测试向量:"123456789" → 0x29B1
    result = crc16(b"123456789")
    assert result == 0x29B1, f"算法错!得到 {result:#06X},期望 0x29B1"
    print(f"自测通过: crc16('123456789') = {result:04X}")

运行:

powershell 复制代码
python verify.py
# 输出: 自测通过: crc16('123456789') = 29B1

为什么用 "123456789" → 0x29B1

这是 CRC16-CCITT-FALSE(poly=0x1021, init=0xFFFF, 不反转)的国际标准测试向量,所有 CRC16 实现都用它验证正确性。等于一个"标准答案"。你的实现跑出 29B1,说明算法对。

3.3 跑一下 Server 本身(预期:它会"卡住")

powershell 复制代码
python crc_server.py

它会没有任何输出,卡住不动。这是正常的! 因为它在 stdin 上等 Client 发 JSON-RPC 消息。你手动敲没用(没法手敲合法 JSON-RPC)。按 Ctrl+C 退出。

最常见的困惑

新手跑 python crc_server.py 看它卡住,以为写错了。没卡住才坏了------卡住说明它在正常等 Client。要"驱动"它,得用 Claude Code 或 Inspector(后面讲)。


四、第三步:底层协议长啥样(全流程)

"了解 MCP 全部流程"=看懂这几条 JSON-RPC 消息。Claude Code 拉起你的 Server 后,在 stdio 上跑这个序列:
crc_server.py(Server) Claude Code(Client) crc_server.py(Server) Claude Code(Client) #mermaid-svg-mUgKMfZNB5pleZAE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mUgKMfZNB5pleZAE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mUgKMfZNB5pleZAE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mUgKMfZNB5pleZAE .error-icon{fill:#552222;}#mermaid-svg-mUgKMfZNB5pleZAE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mUgKMfZNB5pleZAE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mUgKMfZNB5pleZAE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mUgKMfZNB5pleZAE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mUgKMfZNB5pleZAE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mUgKMfZNB5pleZAE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mUgKMfZNB5pleZAE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mUgKMfZNB5pleZAE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mUgKMfZNB5pleZAE .marker.cross{stroke:#333333;}#mermaid-svg-mUgKMfZNB5pleZAE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mUgKMfZNB5pleZAE p{margin:0;}#mermaid-svg-mUgKMfZNB5pleZAE .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mUgKMfZNB5pleZAE text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-mUgKMfZNB5pleZAE .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-mUgKMfZNB5pleZAE .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-mUgKMfZNB5pleZAE .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-mUgKMfZNB5pleZAE .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-mUgKMfZNB5pleZAE #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-mUgKMfZNB5pleZAE .sequenceNumber{fill:white;}#mermaid-svg-mUgKMfZNB5pleZAE #sequencenumber{fill:#333;}#mermaid-svg-mUgKMfZNB5pleZAE #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-mUgKMfZNB5pleZAE .messageText{fill:#333;stroke:none;}#mermaid-svg-mUgKMfZNB5pleZAE .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mUgKMfZNB5pleZAE .labelText,#mermaid-svg-mUgKMfZNB5pleZAE .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-mUgKMfZNB5pleZAE .loopText,#mermaid-svg-mUgKMfZNB5pleZAE .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-mUgKMfZNB5pleZAE .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-mUgKMfZNB5pleZAE .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-mUgKMfZNB5pleZAE .noteText,#mermaid-svg-mUgKMfZNB5pleZAE .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-mUgKMfZNB5pleZAE .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mUgKMfZNB5pleZAE .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mUgKMfZNB5pleZAE .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mUgKMfZNB5pleZAE .actorPopupMenu{position:absolute;}#mermaid-svg-mUgKMfZNB5pleZAE .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-mUgKMfZNB5pleZAE .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mUgKMfZNB5pleZAE .actor-man circle,#mermaid-svg-mUgKMfZNB5pleZAE line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-mUgKMfZNB5pleZAE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户提问,模型决定调 crc16 模型拿到结果,组织回答给用户 ① initialize(协议版本, 能力) ① 返回能力(tools), serverInfo ② notifications/initialized(通知:握手完成) ③ tools/list(列工具) ③ 返回 crc16 工具声明 ④ tools/call(name=crc16, arguments={hex_data:"..."}) ④ 返回 content: {text:"29B1"}

4.1 ① 握手(initialize)

Client → Server(写到 Server 的 stdin):

json 复制代码
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
  "protocolVersion":"2024-11-05",
  "capabilities":{},
  "clientInfo":{"name":"claude-code","version":"1.x.x"}
}}

Server → Client(写到 stdout):

json 复制代码
{"jsonrpc":"2.0","id":1,"result":{
  "protocolVersion":"2024-11-05",
  "capabilities":{"tools":{}},
  "serverInfo":{"name":"crc-server","version":"0.1.0"}
}}

这一步在协商:双方报各自协议版本和支持的能力(tools/resources/prompts)。版本对不上握手会失败。

4.2 ② 握手完成通知

Client → Server(通知,没有 id,不需要回复):

json 复制代码
{"jsonrpc":"2.0","method":"notifications/initialized"}

4.3 ③ 列工具(tools/list)

Client → Server:

json 复制代码
{"jsonrpc":"2.0","id":2,"method":"tools/list"}

Server → Client(FastMCP 自动生成,来自你的类型标注和 docstring):

json 复制代码
{"jsonrpc":"2.0","id":2,"result":{"tools":[{
  "name":"crc16",
  "description":"计算给定十六进制字节序列的 CRC16-CCITT 校验值。",
  "inputSchema":{
    "type":"object",
    "properties":{
      "hex_data":{"type":"string","description":"十六进制字符串,如 \"48656C6C6F\" 表示 \"Hello\" 的字节"}
    },
    "required":["hex_data"]
  }
}]}}

看清了? 你写的 def crc16(hex_data: str) + docstring,被 FastMCP 自动转成了这个 inputSchema。模型看到的就是这个声明,据此判断怎么调。

4.4 ④ 调工具(tools/call)

当模型决定"我要算个 CRC",Client → Server:

json 复制代码
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{
  "name":"crc16",
  "arguments":{"hex_data":"313233343536373839"}
}}

Server → Client(FastMCP 调你的函数,把返回值包成 content):

json 复制代码
{"jsonrpc":"2.0","id":3,"result":{
  "content":[{"type":"text","text":"29B1"}],
  "isError":false
}}

模型拿到 "29B1",组织成自然语言回答你。

这就是 MCP 的全部

四步:握手 → 列工具 → 调工具。所有 MCP Server 都跑这个流程,差别只在第三步列出的工具不同、第四步执行的逻辑不同。搞懂这个,你就懂了 MCP 的"全部流程"。


五、第四步:接入 Claude Code

5.1 方法 A:命令行添加(最快)

在任意目录 的 PowerShell 里执行(注意用 .venv 里 Python 的完整路径 ,确保能找到 mcp 包):

powershell 复制代码
claude mcp add crc-server -- E:/mcp-crc/.venv/Scripts/python.exe E:/mcp-crc/crc_server.py

!important 为啥要用 .venv 的 python 完整路径

Claude Code 启动 Server 时,是用你给的 command 拉起一个新进程。新进程不会自动激活你的虚拟环境,如果只用 python,它可能用的是系统 Python(没装 mcp)→ 启动即崩。用 .venv/Scripts/python.exe 的完整路径,保证用的是装了 mcp 的那个 Python。

5.2 方法 B:写配置文件(更直观,推荐首次用)

Claude Code 的项目级配置文件是项目根目录的 .mcp.json。在你当前工作的项目目录 (或 vault 根)建 .mcp.json:

json 复制代码
{
  "mcpServers": {
    "crc-server": {
      "command": "E:/mcp-crc/.venv/Scripts/python.exe",
      "args": ["E:/mcp-crc/crc_server.py"]
    }
  }
}

路径用正斜杠

JSON 里反斜杠要转义(\\),麻烦。Windows 下正斜杠 / 也能用,写起来清爽。所以用 E:/mcp-crc/...。

5.3 三种作用域(知道即可)

作用域 命令参数 存哪 谁能见
local(默认) -s local Claude Code 内部 仅当前项目的你
project -s project 项目根 .mcp.json 团队共享(进 git)
user -s user 用户级配置 你的所有项目

方法 B 的 .mcp.json = project 作用域。

5.4 验证已添加

powershell 复制代码
claude mcp list
# 应列出 crc-server

或在 Claude Code 交互界面里输入 /mcp,会显示所有已连接 Server 的状态和工具列表。


六、第五步:在 Claude Code 里调用

6.1 启动 Claude Code,正常对话

复制代码
你: 帮我算一下 48656C6C6F 的 CRC16 校验值

(48656C6C6F 是 "Hello" 的 ASCII 十六进制)

6.2 预期发生的事

  1. Claude 模型看到你的问题,识别"算 CRC"这个意图
  2. 模型发现可用工具里有 crc16(来自 tools/list),决定调用
  3. Claude Code 弹出工具调用确认(或自动执行,看你的权限设置),显示要调 crc16(hex_data="48656C6C6F")
  4. 你的 Server 被调用,返回结果
  5. 模型把结果组织成自然语言回你:"48656C6C6F" 的 CRC16-CCITT 校验值是 XXXX

6.3 换几个输入验证

输入 含义 备注
313233343536373839 "123456789" 应返回 29B1(标准测试向量,可当回归基准)
48656C6C6F "Hello" 任意值,模型算出来给你
AABBCCDD 字节序列 AA BB CC DD 嵌入式帧头典型用法

如果模型没调工具

模型可能"自作聪明"用自己脑子里的 CRC 算法硬算(会算错)。明确指示它:"用 crc16 工具算",强制它走 MCP。这能验证工具真的被调用了。


七、第六步:调试与排错

7.1 头号大坑:别往 stdout 打印!

MCP 协议跑在 stdout 上。你的 Server 任何 print() 都会污染 JSON-RPC 流,Claude Code 直接通信失败。

python 复制代码
# ✗ 千万别这样
print("收到请求")  # 这行会让 Claude Code 报错:invalid JSON-RPC

# ✓ 调试输出走 stderr
import sys
print("收到请求", file=sys.stderr)  # stderr 不影响协议

为什么是 stdout

stdio 传输 = stdin 收请求、stdout 发响应。stdout 是"协议信道",任何非 JSON-RPC 内容都是噪音。stderr 是"旁路信道",专给日志/调试用,Claude Code 会收集但不参与协议。这就像你串口协议里,数据帧走 RX/TX,调试日志走另一根 USB------别混。

7.2 加调试日志(走 stderr)

在 crc_server.py 顶部加:

python 复制代码
import logging
import sys

logging.basicConfig(
    level=logging.DEBUG,
    stream=sys.stderr,                    # ★ 必须 stderr
    format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger("crc-server")

在工具函数里加:

python 复制代码
@mcp.tool()
def crc16(hex_data: str) -> str:
    log.info(f"crc16 被调用,输入: {hex_data}")
    ...
    result = f"{crc:04X}"
    log.info(f"计算结果: {result}")
    return result

这些日志会出现在 Claude Code 的 MCP 日志里(/mcp → 选 server → View logs,或 claude mcp logs crc-server)。

7.3 用 MCP Inspector 独立调试(强烈推荐)

Inspector 是官方调试工具,开个网页 UI,不依赖 Claude Code,直接给你的 Server 发 JSON-RPC,让你看到每条消息。需要 Node.js:

powershell 复制代码
npx @modelcontextprotocol/inspector E:/mcp-crc/.venv/Scripts/python.exe E:/mcp-crc/crc_server.py

浏览器自动打开 http://localhost:6274,你能:

  • 点 List Tools → 看到 crc16 的 schema(验证 tools/list 通了)
  • 填参数 hex_data = 313233343536373839 → 点 Run Tool → 看返回 29B1(验证 tools/call 通了)
  • 看到完整的 JSON-RPC 请求和响应原文(对着 §四 的消息对照看,全流程一目了然)

Inspector 是学 MCP 的最佳工具

它把抽象的 JSON-RPC 可视化了。出问题时,先用 Inspector 隔离"是 Server 本身的问题,还是 Claude Code 配置的问题"------Inspector 能通,Server 就没问题,去查 Claude Code 配置。

7.4 常见错误对照表

现象 原因 解决
/mcp 显示 crc-server 启动失败 command 路径错,或 venv 里没装 mcp 用 .venv/Scripts/python.exe 完整路径;pip show mcp 确认装了
启动失败,报 -32000 多半是 Server 进程启动即崩(import 错、代码语法错等) 手动跑 python crc_server.py 看 stderr 报错;最常见是装了 mcp 2.x(见下)
启动失败,日志有 ModuleNotFoundError: No module named 'mcp.server.fastmcp' 装的是 mcp 2.x ,API 改了(FastMCP→MCPServer) 降级:pip install "mcp<2";或改代码适配 2.x(见 §2.3 说明)
启动失败,日志有 ModuleNotFoundError: No module named 'mcp' 用的 Python 没装 mcp 用 venv 的 python 完整路径 .venv/Scripts/python.exe
Server 启动但调用没反应 Server 里 print() 污染了 stdout 改成 print(..., file=sys.stderr)
模型自己硬算不用工具 模型没识别到工具,或工具描述不清 /mcp 确认工具出现;对话里明确说"用 crc16 工具";把 docstring 写清楚
tools/list 返回空 @mcp.tool() 没装饰上,或函数写在 if __name__ 里 装饰器写在模块顶层,函数在 mcp.run() 之前定义
路径含中文/空格报错 Windows 路径转义 用正斜杠 /,或确保路径无空格中文
assert result == 0x29B1 失败 CRC 算法实现错 对照 verify.py 的实现,检查 poly/init/移位方向

八、第七步:扩展------加第二个工具 + 一个资源

跑通后,加一点东西,覆盖 MCP 的另一类能力(Resource)。

8.1 加一个工具:批量算 CRC

python 复制代码
@mcp.tool()
def crc16_batch(hex_lines: str) -> str:
    """对多行十六进制数据批量计算 CRC16,每行一条。

    参数:
        hex_lines: 多行数据,换行分隔,如 "AABB\nCCDD\nEEFF"

    返回:
        每行原数据 + CRC,换行分隔
    """
    results = []
    for line in hex_lines.strip().splitlines():
        line = line.strip()
        if not line:
            continue
        crc = 0xFFFF
        for byte in bytes.fromhex(line):
            crc ^= byte << 8
            for _ in range(8):
                crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
        results.append(f"{line} → {crc:04X}")
    return "\n".join(results)

重启 Claude Code,/mcp 里就能看到两个工具了。

8.2 加一个资源(Resource):暴露版本信息

Resource 是"只读数据源",模型可以主动读取(不像工具要被调用)。FastMCP 用 @mcp.resource():

python 复制代码
@mcp.resource("crc://version")
def version_info() -> str:
    """本 Server 的版本和支持的算法"""
    return "crc-server v0.1.0, 支持 CRC16-CCITT-FALSE (poly=0x1021, init=0xFFFF)"

Resource 用 URI(crc://version)标识。Client 能 resources/list 看到它、resources/read 读内容。适合暴露"配置""版本""说明文档"这类静态信息。

Tool vs Resource 的区别

  • Tool :模型调用 它执行操作、拿结果(有副作用或计算)。如 crc16。
  • Resource :模型读取它获取上下文(只读数据)。如版本信息、配置文件内容。
  • 类比嵌入式:Tool ≈ 你给协处理器下命令"算这个";Resource ≈ 协处理器的只读寄存器,你随时能读状态。

九、进阶:远程部署(HTTP 传输,本地 Claude 调用)

前面都是 stdio 本地传输------Server 跑在你本机,Claude Code 拉起它的进程。但实际场景里,你可能想:

  • 把 Server 放到云服务器/内网 Linux 机器上,团队多人共用
  • Server 需要访问那台机器上的资源(数据库、内部 API、特定硬件)
  • 不想在每台电脑上各装一遍 Python 环境

这时就要换 HTTP 传输 。MCP 2.x 支持 streamable-http(官方当前主推)和 sse(旧版,兼容用)。

!important stdio vs HTTP 的本质区别

  • stdio :Claude Code 自己拉起 Server 进程,靠它的 stdin/stdout 通信。Server 的生命周期归 Client 管,Client 关了 Server 也没了。只在本机。
  • HTTP :Server 是个独立常驻服务 ,自己监听端口。Client 通过 HTTP 请求调用。Server 的生命周期独立于 Client,任何能联网的 Client 都能连。和访问一个 Web API 没区别。

9.1 架构对比

#mermaid-svg-qFQWOhEj0XB1nmaX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qFQWOhEj0XB1nmaX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qFQWOhEj0XB1nmaX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qFQWOhEj0XB1nmaX .error-icon{fill:#552222;}#mermaid-svg-qFQWOhEj0XB1nmaX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qFQWOhEj0XB1nmaX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qFQWOhEj0XB1nmaX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qFQWOhEj0XB1nmaX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qFQWOhEj0XB1nmaX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qFQWOhEj0XB1nmaX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qFQWOhEj0XB1nmaX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qFQWOhEj0XB1nmaX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qFQWOhEj0XB1nmaX .marker.cross{stroke:#333333;}#mermaid-svg-qFQWOhEj0XB1nmaX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qFQWOhEj0XB1nmaX p{margin:0;}#mermaid-svg-qFQWOhEj0XB1nmaX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qFQWOhEj0XB1nmaX .cluster-label text{fill:#333;}#mermaid-svg-qFQWOhEj0XB1nmaX .cluster-label span{color:#333;}#mermaid-svg-qFQWOhEj0XB1nmaX .cluster-label span p{background-color:transparent;}#mermaid-svg-qFQWOhEj0XB1nmaX .label text,#mermaid-svg-qFQWOhEj0XB1nmaX span{fill:#333;color:#333;}#mermaid-svg-qFQWOhEj0XB1nmaX .node rect,#mermaid-svg-qFQWOhEj0XB1nmaX .node circle,#mermaid-svg-qFQWOhEj0XB1nmaX .node ellipse,#mermaid-svg-qFQWOhEj0XB1nmaX .node polygon,#mermaid-svg-qFQWOhEj0XB1nmaX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qFQWOhEj0XB1nmaX .rough-node .label text,#mermaid-svg-qFQWOhEj0XB1nmaX .node .label text,#mermaid-svg-qFQWOhEj0XB1nmaX .image-shape .label,#mermaid-svg-qFQWOhEj0XB1nmaX .icon-shape .label{text-anchor:middle;}#mermaid-svg-qFQWOhEj0XB1nmaX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qFQWOhEj0XB1nmaX .rough-node .label,#mermaid-svg-qFQWOhEj0XB1nmaX .node .label,#mermaid-svg-qFQWOhEj0XB1nmaX .image-shape .label,#mermaid-svg-qFQWOhEj0XB1nmaX .icon-shape .label{text-align:center;}#mermaid-svg-qFQWOhEj0XB1nmaX .node.clickable{cursor:pointer;}#mermaid-svg-qFQWOhEj0XB1nmaX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qFQWOhEj0XB1nmaX .arrowheadPath{fill:#333333;}#mermaid-svg-qFQWOhEj0XB1nmaX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qFQWOhEj0XB1nmaX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qFQWOhEj0XB1nmaX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qFQWOhEj0XB1nmaX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qFQWOhEj0XB1nmaX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qFQWOhEj0XB1nmaX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qFQWOhEj0XB1nmaX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qFQWOhEj0XB1nmaX .cluster text{fill:#333;}#mermaid-svg-qFQWOhEj0XB1nmaX .cluster span{color:#333;}#mermaid-svg-qFQWOhEj0XB1nmaX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qFQWOhEj0XB1nmaX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qFQWOhEj0XB1nmaX rect.text{fill:none;stroke-width:0;}#mermaid-svg-qFQWOhEj0XB1nmaX .icon-shape,#mermaid-svg-qFQWOhEj0XB1nmaX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qFQWOhEj0XB1nmaX .icon-shape p,#mermaid-svg-qFQWOhEj0XB1nmaX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qFQWOhEj0XB1nmaX .icon-shape .label rect,#mermaid-svg-qFQWOhEj0XB1nmaX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qFQWOhEj0XB1nmaX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qFQWOhEj0XB1nmaX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qFQWOhEj0XB1nmaX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP 模式(远程,本节)
HTTP POST /mcp

JSON-RPC over HTTP
HTTP 响应

(SSE 事件流)
Claude Code

(你的电脑)
防火墙/端口
Server 进程

(远程服务器)
stdio 模式(本地,前面几节用的)
拉起进程

python crc_server.py
stdin/stdout

JSON-RPC
Claude Code
Server 进程

嵌入式视角

stdio ≈ 你的 MCU 直接用串口线连协处理器,点对点、专属;HTTP ≈ 协处理器接上网络,任何设备都能通过 IP 地址访问它。一旦上了网,就要考虑"谁能连、会不会被攻击"------所以本节后半段讲安全。

9.2 第一步:改 Server,从 stdio 换成 HTTP

改 mcp.run() 一行即可,业务代码(crc16 函数)一字不动:

python 复制代码
# crc_server_http.py ------ 远程 HTTP 版 (mcp 2.x)
# ★ 和 stdio 版的唯一区别在最后的 mcp.run() 那一行
from mcp.server.mcpserver import MCPServer

mcp = MCPServer("crc-server")

@mcp.tool()
def crc16(hex_data: str) -> str:
    """计算给定十六进制字节序列的 CRC16-CCITT 校验值。

    参数:
        hex_data: 十六进制字符串,如 "48656C6C6F" 表示 "Hello" 的字节

    返回:
        4 位十六进制 CRC 校验值(大写),如 "C86E"
    """
    data = bytes.fromhex(hex_data)
    crc = 0xFFFF
    for byte in data:
        crc ^= byte << 8
        for _ in range(8):
            if crc & 0x8000:
                crc = (crc << 1) ^ 0x1021
            else:
                crc <<= 1
            crc &= 0xFFFF
    return f"{crc:04X}"


if __name__ == "__main__":
    # ★ stdio 版:  mcp.run()
    # ★ HTTP 版:    mcp.run("streamable-http", host="0.0.0.0", port=8000)
    #   host="0.0.0.0" = 监听所有网卡(远程可访问);用 127.0.0.1 则只有本机能连
    #   port=8000      = 监听端口
    #   默认路径 /mcp,完整端点 URL = http://<服务器IP>:8000/mcp
    mcp.run("streamable-http", host="0.0.0.0", port=8000)

run() 的参数(从源码确认,mcp 2.2.0):

参数 默认值 说明
transport "stdio" 传 "streamable-http" 走 HTTP
host "127.0.0.1" 监听地址。远程访问必须改 "0.0.0.0"(监听所有网卡),否则只有本机能连
port 8000 监听端口
streamable_http_path "/mcp" 端点路径,完整 URL = http://host:port/mcp
json_response False True=纯 JSON 响应;False=SSE 事件流(默认,兼容性好)

!warning host 一定要 0.0.0.0

默认 127.0.0.1 只接受本机连接,远程机器连不上。这是远程部署最常踩的坑:Server 起来了、本地能访问、别的机器死活连不上------99% 是没改 host。改成 0.0.0.0 表示"监听所有网卡的连接"。

9.3 第二步:在远程服务器上跑起来

SSH 到你的 Linux 服务器,把代码传上去、装环境、后台运行。按顺序来,每步都有 Linux 特有的坑。

9.3.1 先查 Python 版本(最常见的坑)

mcp 2.x 要求 Python 3.10+ ,但很多 Linux 服务器自带的 python3 是 3.8 或 3.9(CentOS 7、Ubuntu 18.04、Debian 10),直接装会失败:

bash 复制代码
# SSH 登录服务器后,先查版本
python3 --version
# 如果显示 3.10+ → 没问题,跳到 9.3.3
# 如果显示 3.8 / 3.9  → 需要装新版,见 9.3.2

9.3.2 Python 版本太低怎么办

Ubuntu/Debian(用 deadsnakes PPA 装新版,不影响系统自带 python3):

bash 复制代码
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.12 python3.12-venv
# 之后用 python3.12 代替 python3

CentOS/RHEL(用 SCL 或直接编译,较麻烦;推荐用 conda):

bash 复制代码
# 装 miniconda(推荐,CentOS 上最省心)
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh
# 重新登录 shell 后,conda 自带 Python 3.12

别覆盖系统 python3

Linux 系统工具(yum、apt、firewall-config 等)依赖自带的 python3。如果你强行 ln -sf /usr/bin/python3.12 /usr/bin/python3 覆盖掉,yum 会崩。装新版后用完整名 python3.12 调用,或用 venv/conda 隔离,别动系统默认的 python3。

9.3.3 把代码从 Windows 传到服务器

在 Windows 本机 的 PowerShell 里执行(把 user@serverIP 换成你的):

powershell 复制代码
# 方法 A:scp(最简单,传单个文件)
scp E:\mcp-crc\crc_server_http.py user@serverIP:~/mcp-crc/

# 方法 B:rsync(增量同步,改了代码反复传更高效)
rsync -avz E:\mcp-crc\crc_server_http.py user@serverIP:~/mcp-crc/

# 方法 C:git(推荐,代码进版本库,服务器 clone)
# 在 Windows 上:cd E:\mcp-crc; git init; git add .; git commit -m "init"; git push
# 在服务器上:  git clone <你的仓库地址> ~/mcp-crc

9.3.4 建虚拟环境 + 装 mcp

回到 服务器上(SSH 会话里):

bash 复制代码
mkdir -p ~/mcp-crc && cd ~/mcp-crc

# 建虚拟环境(用 9.3.1/9.3.2 确认的 python 版本)
python3.12 -m venv .venv          # 或 conda create -n mcp python=3.12
source .venv/bin/activate         # 激活:命令行前出现 (.venv)

# 装 mcp 2.x
pip install "mcp>=2"

# 验证
python -c "from mcp.server.mcpserver import MCPServer; print('OK')"

9.3.5 后台常驻运行

方式 A:nohup(最通用,关 SSH 也不停)

bash 复制代码
nohup python crc_server_http.py > server.log 2>&1 &
echo $! > server.pid              # 记下进程号,方便后面杀
cat server.log                    # 看日志确认起来了
# 应有类似: Uvicorn running on http://0.0.0.0:8000

方式 B:screen/tmux(更友好,能随时回到前台看输出)

bash 复制代码
# 用 screen(几乎所有 Linux 都自带)
screen -S mcp                     # 开一个叫 mcp 的会话
python crc_server_http.py         # 前台跑,能直接看输出
# 按 Ctrl+A 然后按 D → 脱离会话(进程继续跑)
screen -r mcp                     # 随时回到会话看输出
# screen -ls                      # 列出所有会话

# 或用 tmux(更现代,操作类似)
tmux new -s mcp
python crc_server_http.py
# 按 Ctrl+B 然后按 D → 脱离
tmux attach -t mcp                # 回到会话

!tip nohup vs screen/tmux

  • nohup :简单一行命令,适合"启动就不管了"。看日志得 cat server.log,不能交互
  • screen/tmux:能随时回到前台看实时输出、按 Ctrl+C 停,调试阶段更方便。正式生产用 §9.6 的 systemd

9.3.6 端口占用排查(Server 起不来时查)

bash 复制代码
# 看 8000 端口有没有被占
ss -tlnp | grep 8000              # 现代Linux(推荐)
# 或
lsof -i:8000                      # 需要装 lsof
# 或
netstat -tlnp | grep 8000         # 老系统

# 如果被占了,看是谁占的(上面命令会显示进程名/PID),要么杀掉它,要么换端口
# 换端口:改 crc_server_http.py 里的 port=8001

# 确认你的 server 在监听
ss -tlnp | grep 8000
# 应显示: LISTEN 0.0.0.0:8000  ...  python

9.3.7 在服务器本机验证

bash 复制代码
# 用 curl 发 initialize 请求
curl -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}'
# 应返回: event: message \n data: {"jsonrpc":"2.0","id":1,"result":{...}}

!tip 我已在你本机实测过

我在你电脑上用 crc_server_http.py 启动了 HTTP Server(mcp 2.2.0,streamable-http 传输),用 Invoke-WebRequest 发 initialize 请求,得到状态码 200 + 合法的 SSE 事件流响应(event: message\ndata: {"jsonrpc":"2.0",...})。代码和流程都跑通了,你照搬到远程 Linux 服务器即可。

9.4 第三步:开放防火墙端口

服务器端口不开放,外部照样连不上:

环境 怎么开 8000 端口
云服务器(阿里云/腾讯云/AWS) 在云控制台的安全组里加一条入方向规则:TCP 8000 允许。光改系统防火墙不够,云平台还有一层
Linux 系统防火墙(ufw) sudo ufw allow 8000
Linux 系统防火墙(firewalld) sudo firewall-cmd --permanent --add-port=8000/tcp && sudo firewall-cmd --reload
Windows 防火墙 New-NetFirewallRule -DisplayName "MCP" -Direction Inbound -Protocol TCP -LocalPort 8000 -Action Allow

验证端口从外部可达(在你本地电脑上测):

powershell 复制代码
# 把 <服务器IP> 换成实际 IP,如 47.100.x.x 或 192.168.1.100
Test-NetConnection -ComputerName <服务器IP> -Port 8000
# TcpTestSucceeded : True  ← 这才是真的通了

两层防火墙都要开

云服务器有两层防火墙:① 云平台安全组(外层)② 系统防火墙(内层)。两层都放行 8000 端口,外部才能连上。很多人只开了系统防火墙,死活连不通,就是安全组没开。
替代方案:SSH 隧道(不开公网端口,更安全)

如果你的 Linux 服务器是内网机器 (没公网 IP),或不想把 8000 端口暴露到公网,用 SSH 隧道。在 Windows 本机开一个 PowerShell 窗口,保持运行:

powershell 复制代码
# 把服务器的 8000 端口,通过 SSH 转发到你本机的 8000
ssh -N -L 8000:localhost:8000 user@serverIP
# -N 不执行远程命令,只做端口转发
# -L 本地8000 → 服务器localhost:8000

然后本机 Claude Code 连 http://localhost:8000/mcp(就像 Server 跑在本机一样)。服务器不用开任何公网端口,流量全走 SSH 加密通道。SSH 窗口别关,关了隧道就断。 这对内网开发服务器是最省心的方案------既不用碰防火墙,又自带加密。

9.5 第四步:本地 Claude Code 连接远程 Server

用 --transport http 添加,后面跟完整 URL(不是 command/args 了):

powershell 复制代码
# 添加远程 HTTP Server(把 IP 换成你服务器的)
claude mcp add crc-server-http --transport http http://<服务器IP>:8000/mcp

或写进 .mcp.json(项目级,团队共享):

json 复制代码
{
  "mcpServers": {
    "crc-server-http": {
      "type": "http",
      "url": "http://<服务器IP>:8000/mcp"
    }
  }
}

配置对比:

stdio 模式 HTTP 模式
claude mcp add -- <command> <args> --transport http <url>
配置字段 command + args type: "http" + url
本地需要装 Python/代码? 要(Claude Code 拉起本地进程) 不要(Server 在远程,Claude Code 只发 HTTP)
Server 谁启动? Claude Code 启动 你手动在服务器上启动
Server 生命周期 随 Client 启停 独立常驻,Client 关了也在

重启 Claude Code,/mcp 里应出现 crc-server-http,状态正常。调用方式和 stdio 版完全一样:"用 crc16 工具算 313233343536373839"。

9.6 第五步:生产环境加固(必读)

直接裸跑 python crc_server_http.py 只适合内网测试。公网部署必须加固:

① 用 HTTPS(加密传输)

HTTP 明文传输,你的工具调用内容(可能含敏感数据)会被中间人看到。用 Nginx 反向代理加 HTTPS:

nginx 复制代码
# /etc/nginx/sites-available/mcp
server {
    listen 443 ssl;
    server_name mcp.yourdomain.com;

    ssl_certificate     /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location /mcp {
        proxy_pass http://127.0.0.1:8000;   # 转发到本机 MCP Server
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header Connection "";
        proxy_buffering off;                 # SSE 流必须关缓冲
        proxy_read_timeout 86400;            # 长连接超时调长
    }
}

然后 Claude Code 连 https://mcp.yourdomain.com/mcp。Server 的 host 可改回 127.0.0.1(只让本机 Nginx 访问,不直接暴露)。

② 加认证(谁允许调)

裸跑的 Server 谁都能连、谁都能调你的工具。两种认证方式:

方式 怎么做 适合
HTTP Header Token Nginx 层校验 Authorization header,Claude Code 配置里带 header 简单场景
OAuth 2.0 MCP 2.x 内置 OAuth 支持(--client-id 参数) 正式生产、多用户

Nginx 加 Token 校验示例:

nginx 复制代码
location /mcp {
    if ($http_authorization != "Bearer your-secret-token") {
        return 401;
    }
    proxy_pass http://127.0.0.1:8000;
    # ...
}

Claude Code 配置带 header:

json 复制代码
{
  "mcpServers": {
    "crc-server-http": {
      "type": "http",
      "url": "https://mcp.yourdomain.com/mcp",
      "headers": {
        "Authorization": "Bearer your-secret-token"
      }
    }
  }
}

③ 进程守护(别让它挂了)

裸跑 nohup 重启会丢。用 systemd 守护:

ini 复制代码
# /etc/systemd/system/mcp-crc.service
[Unit]
Description=MCP CRC Server
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/mcp-crc
ExecStart=/home/ubuntu/mcp-crc/.venv/bin/python crc_server_http.py
Restart=always                  # 挂了自动重启
RestartSec=3

# ★ 环境变量(按需取消注释)
# Environment=PYTHONUNBUFFERED=1          # Python 日志不缓冲,立刻写日志
# Environment=HTTPS_PROXY=http://127.0.0.1:7897   # 服务器也要走代理时装
# EnvironmentFile=/home/ubuntu/mcp-crc/.env       # 或从 .env 文件读

[Install]
WantedBy=multi-user.target
bash 复制代码
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-crc   # 开机自启 + 立即启动
sudo systemctl status mcp-crc         # 看状态(绿色 active = 正常)
journalctl -u mcp-crc -f              # 看实时日志(Ctrl+C 退出)
sudo systemctl restart mcp-crc        # 改了代码后重启

!tip systemd 的关键细节

  • ExecStart 用 venv 里的 python 完整路径 (/home/ubuntu/mcp-crc/.venv/bin/python),和 stdio 模式配置 Claude Code 时同理------systemd 不会激活你的 venv,得直接指向 venv 的 python
  • User=ubuntu 用非 root 用户跑,别用 root(安全)
  • PYTHONUNBUFFERED=1 很重要:Python 默认缓冲 stdout,systemd 的日志(journalctl)可能看不到实时输出。加这个变量让日志立刻写出
  • 改了 crc_server_http.py 后,sudo systemctl restart mcp-crc 重启生效

9.7 远程部署排错

现象 原因 解决
pip install mcp 报 Python 版本不够 Linux 自带 python3 是 3.8/3.9 装 3.10+(§9.3.2),用 python3.12 -m venv
本机能访问,别的机器连不上 host 还是 127.0.0.1,或防火墙没开 改 host="0.0.0.0";开安全组 + 系统防火墙 8000 端口
Test-NetConnection 端口不通 云安全组没开 去云控制台安全组加 TCP 8000 入方向规则
内网服务器没公网 IP,外部连不上 没法开公网端口 用 SSH 隧道(§9.4 tip)
Address already in use Server 起不来 8000 端口被占 `ss -tlnp
systemctl status 显示 failed venv 路径错,或 User 不对 确认 ExecStart 用 venv python 完整路径;User 用有权限的用户
journalctl 看不到日志 Python 缓冲了 stdout systemd 加 Environment=PYTHONUNBUFFERED=1
连上了但 /mcp 返回 404 URL 路径写错 完整 URL 是 http://IP:8000/mcp,别漏 /mcp
Claude Code 报"无法连接" Server 没启动,或端口被占 systemctl status mcp-crc;`ss -tlnp
超时/响应慢 Nginx 缓冲了 SSE 流 Nginx 配 proxy_buffering off; proxy_read_timeout 86400;
公网裸跑被扫描攻击 没 HTTPS + 没认证 至少加 Nginx HTTPS + Token 校验(§9.6)

9.8 什么时候用 stdio,什么时候用 HTTP

场景 选 理由
个人本地开发,Server 只自己用 stdio 零配置,Claude Code 自动拉起,最简单
Server 要访问本机文件/硬件 stdio 直接本机访问,无需网络
团队多人共用一个 Server HTTP 部署一次,大家连同一个 URL
Server 要访问服务器上的资源(数据库/内网 API) HTTP Server 留在服务器上,就近访问资源
Server 计算重,想放强机器上 HTTP 卸载到服务器,本地只发请求
想跨设备/跨地点用同一个 Server HTTP 任何能联网的 Claude Code 都能连

决策口诀

"只自己用 + 在本机" → stdio;"别人也要用 + 在别的机器" → HTTP。 本教程 §一~§八 的 stdio 版是基础,跑通后再按本节升级到 HTTP,两者业务代码只差 mcp.run() 一行。


十、速查表

10.1 从零到跑通的 7 步

步 做什么 关键命令/文件
1 建环境 python -m venv .venv && .venv\Scripts\activate && pip install mcp
2 写 Server crc_server.py(FastMCP + @mcp.tool())
3 自测算法 verify.py,确认 "123456789"→0x29B1
4 理解协议 握手→列工具→调工具(§四)
5 接入 Claude Code claude mcp add 或 .mcp.json(用 venv python 完整路径)
6 验证 对话里说"用 crc16 工具算 ..."
7 调试 Inspector 独立测 + stderr 日志

10.2 关键认知速记

  • 传输 :本地 MCP 走 stdio (stdin 收、stdout 发);远程走 streamable-http(HTTP POST,响应是 SSE 流)
  • stdio vs HTTP 差一行 :mcp.run() vs mcp.run("streamable-http", host="0.0.0.0", port=8000)
  • stdout 是协议信道 :stdio 模式下任何 print() 到 stdout 都会破坏协议,调试走 stderr
  • FastMCP 自动生成 schema:类型标注 → 参数 schema,docstring → 工具描述
  • 协议四步:initialize → notifications/initialized → tools/list → tools/call(两种传输都一样)
  • 完整路径 :stdio 配置里用 .venv/Scripts/python.exe 完整路径;HTTP 配置用 url
  • 远程三必查:host=0.0.0.0、防火墙/安全组开端口、公网加 HTTPS+认证
  • Inspector 隔离法:出问题时先用 Inspector 单测 Server,排除 Claude Code 配置问题

10.3 Linux 远程部署速查

要点 命令/做法
Python 版本 python3 --version,需 3.10+;不够用 deadsnakes PPA 装 python3.12 或用 conda
传代码 scp crc_server_http.py user@IP:~/mcp-crc/(Windows→Linux)
后台跑 nohup python crc_server_http.py > server.log 2>&1 & 或 screen -S mcp
端口排查 `ss -tlnp
本机验证 curl -X POST http://localhost:8000/mcp -H "Accept: application/json, text/event-stream" -d '{...initialize...}'
防火墙 云安全组 + sudo ufw allow 8000(两层都开)
内网免开端口 SSH 隧道:ssh -N -L 8000:localhost:8000 user@IP,本机连 localhost:8000
进程守护 systemd,ExecStart 用 venv python 完整路径,加 PYTHONUNBUFFERED=1
别覆盖系统 python3 用 python3.12 或 venv/conda,别 ln -sf 覆盖 /usr/bin/python3(yum 会崩)

10.4 你写的 vs 底层 API 对比

FastMCP(本教程) 底层 Server(\[MCP(模型上下文协议)] §7.2)
注册工具 @mcp.tool() 装饰普通函数 @app.list_tools() + @app.call_tool() 两个回调
schema 生成 自动从类型标注生成 手写 inputSchema 字典
返回值 直接返回 str/int,FastMCP 包装 手动构造 TextContent 列表
代码量 ~20 行 ~30 行
适合 简单工具、快速上手 需要精细控制协议细节

💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。

如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。


相关推荐
AI备忘录1 小时前
(二十三)华为华三锐捷迈普思科 MLAG 跨设备配置命令(双活网关五厂商对照)
服务器·网络·华为
打工仔折腾 AI1 小时前
把模型切换交给平台:用蓝耘智能路由搭建商品评论分析工具
java·服务器·前端·后端·python·性能优化·ai agent 实战
今年下半年1 小时前
【运维】windows虚拟机安装arm(aarch64)服务器
运维·windows·arm·虚拟机·aarch64
杨云龙UP1 小时前
TDengine Community 超级表建表实战:统一21个TAG与DOUBLE/字符串数据模板
运维·服务器·数据库·时序数据库·tdengine·涛思数据·stable建表
高山有多高2 小时前
【Linux笔记】冯诺依曼体系结构
linux·运维·笔记
海宇AI2 小时前
AI驱动的保险科技:基于海宇车辆出险记录核验构建自动化理赔审计引擎
人工智能·ai·工具分享
katasea2 小时前
第05章:信创技术栈选型:服务器、操作系统、数据库、中间件适配对比
服务器·数据库·中间件
筑梦之路2 小时前
Alibaba Cloud Linux 4 IPXE全自动安装(直连官方源)——筑梦之路
linux·运维·服务器·国产化·阿里云操作系统
Pointer Pursuit2 小时前
make / Makefile
linux·运维·服务器