你已读过 \[MCP(模型上下文协议)],知道它是什么、为什么、架构怎样。但没亲手写过一个是吧?
这篇带你从零到跑通 :装环境 → 写一个算 CRC16 的 MCP Server → 接入 Claude Code → 调用成功 → 看懂底层协议每条消息 → 会调试排错。
用 Python 官方
mcpSDK 的 FastMCP 风格(代码最短),实现一个嵌入式风格 CRC16 校验工具。每一步都给完整命令和代码,照抄即可。
本教程结束时你拥有的东西
- 一个能跑的 MCP Server(
crc_server.py),暴露一个crc16工具- Claude Code 能调用它:"帮我算
48656C6C6F的 CRC16" → 模型自动调你的工具 → 返回校验值- 彻底搞懂 MCP 在底层跑的是什么(JSON-RPC 握手 → 列工具 → 调工具的全流程)
- 出错时知道怎么查(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 FastMCPfrom mcp.server.mcpserver import MCPServer类名 FastMCPMCPServer装饰器 @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 预期发生的事
- Claude 模型看到你的问题,识别"算 CRC"这个意图
- 模型发现可用工具里有
crc16(来自tools/list),决定调用 - Claude Code 弹出工具调用确认(或自动执行,看你的权限设置),显示要调
crc16(hex_data="48656C6C6F") - 你的 Server 被调用,返回结果
- 模型把结果组织成自然语言回你:
"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 的 pythonUser=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()vsmcp.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 行 |
| 适合 | 简单工具、快速上手 | 需要精细控制协议细节 |
💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。
如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。