🛠️ MCP Server 怎么搭?从 15 行代码到接入宿主
别先啃协议——搭一个 MCP Server 核心就是一个 Python 文件加几个装饰器。先跑起来,再一步步接到 Claude/Cursor、部署到远程。
📚 本文目录
先跑起来:15 行一个能用的 Server
⚡ 别被协议吓到
搭 MCP Server 不需要自己实现 JSON-RPC、握手、传输——官方 Python SDK 全帮你干了。
你要做的核心只有一件事:用装饰器把函数注册成能力。
下面 15 行就是一个完整可跑的 Server。
📦 先装 SDK
pip install "mcp[cli]"
mcp 是核心库,[cli] 额外给你 mcp dev / mcp run / mcp install 命令行工具。
没有它你连写都不用写。
from mcp.server import MCPServer
mcp = MCPServer("我的工具箱")
@mcp.tool()
def add(a: int, b: int) -> int:
"""两个数相加。"""
return a + b
@mcp.resource("config://{name}")
def get_config(name: str) -> str:
"""按名字读一条配置。"""
return f"{name} 的配置"保存成 server.py,终端里跑:
mcp dev server.py
会自动打开 MCP Inspector(一个网页调试台),你在里面点 add(a=1, b=2) 返回 3——第一个 MCP Server 就这样跑通了。
MCPServer("名字")
创建 Server 实例。这个名字会出现在宿主(Claude / Cursor)的工具列表里。
@mcp.tool()
把下面的普通函数注册成一个工具。函数名 = 工具名,返回给模型。
docstring = 描述
函数说明文字就是模型的「工具描述」,决定模型什么时候调它。写清楚使用场景。
类型注解 = Schema
a: int, b: int 会自动变成 JSON Schema——你不用手写参数结构定义。
写一个真工具:docstring 是描述,注解是 Schema
最小版够跑,但真实工具要带参数、写清 docstring。看一个查天气的工具:
from mcp.server import MCPServer
import json
mcp = MCPServer("天气助手")
@mcp.tool()
def get_weather(city: str, unit: str = "celsius") -> str:
"""查询指定城市的当前天气。city 是城市名,unit 可选 celsius 或 fahrenheit。"""
# 这里通常去调真实天气 API,为演示直接返回
data = {"city": city, "temp": 25, "unit": unit, "desc": "晴"}
return json.dumps(data, ensure_ascii=False)| 写工具的黄金习惯 | 为什么 |
|---|---|
| docstring 写清「何时用 + 参数含义」 | 模型靠描述决定调不调、怎么填参 |
| 参数类型 / 默认值标清楚 | 自动生成正确的 Schema,模型少填错 |
| 返回 JSON 而非裸字符串 | 结构化的结果更好被模型消化 |
Tool / Resource / Prompt:三种能力怎么选
🛠️ Tool:有副作用
会改变世界 → 用 Tool。
创建文件、发消息、改数据库。
注册:@mcp.tool()
📄 Resource:只读数据
只是读取上下文 → 用 Resource。
读 README、查 schema、看日志。
注册:@mcp.resource("uri")
📝 Prompt:提示模板
标准化一段指令 → 用 Prompt。
代码审查、日报模板等最佳实践。
注册:@mcp.prompt()
@mcp.resource("note://{id}")
def get_note(id: str) -> str:
"""按 id 读一条笔记(只读)。"""
return f"笔记 {id} 的内容"
@mcp.prompt()
def code_review(pr: int) -> str:
"""生成一段 code review 指令模板。"""
return f"请 review PR #{pr},重点看:正确性、可读性、安全性。"接进宿主:Claude Desktop / Cursor
本地部署走 stdio——宿主把你的 Server 当子进程拉起来,通过标准输入输出通信,不用写 HTTP 服务。
以 Claude Desktop 为例,在它的配置文件 claude_desktop_config.json 里加一段:
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["/绝对路径/weather_server.py"]
}
}
}远程部署:Streamable HTTP
本地 stdio 只能给「同一台机器」的宿主用。要多端 / 远程 用,就用 Streamable HTTP——把 Server 挂成一个 HTTP 端点,宿主填 URL 连接。
from mcp.server import MCPServer
from fastapi import FastAPI
mcp = MCPServer("天气助手")
# ... 上面那些 @mcp.tool() / @mcp.resource() 保持不变 ...
app = FastAPI()
app.mount("/mcp", mcp.streamable_http_app())
# 启动:uvicorn main:app --host 0.0.0.0 --port 8000本地
宿主当子进程拉起
无需网络、无需鉴权
适合桌面客户端
配置写 command + args
远程
挂成 URL 端点
跨设备、可鉴权
适合多端 / 服务端
配置写 url
远程配置就一行:
{"mcpServers":{"weather":{"url":"http://服务器:8000/mcp"}}}
踩坑 & 工具多了的 Token 优化
docstring 别啰嗦
描述既是「模型怎么用它」的说明书,也是要占 token 的。写清使用场景即可,别堆形容词——太长既费 token 又让模型乱调。
按需注入 top-K
工具几十上百个时,别全量塞给模型。根据用户意图先做 embedding 召回,只注入命中的 top-K 个工具,其余不占上下文。
按模块分组路由
把工具按功能分组(订单/用户/物流),用户意图先路由到某组,再只加载该组的工具定义。
懒加载 + 复用连接
不常用的工具不预注册,用到再 list_tools 拉取;HTTP 部署复用连接、避免重复握手。
| 手段 | 效果 |
|---|---|
| 精简 docstring | 减少每个工具占的字数 |
| 按需注入 top-K(embedding 召回) | 全量注入 → 只注入命中的工具 |
| 按模块分组路由 | 只加载相关那组的工具 |
| 懒加载 + 复用连接 | 不常用的不占上下文,省握手 |
搭 MCP Server 核心三步:装 SDK(pip install "mcp[cli]")→ 用 @mcp.tool()/@mcp.resource()/@mcp.prompt() 把函数注册成能力(docstring 是描述、类型注解自动生成 Schema)→ 用 mcp dev 测试 / 配 config 接入宿主(本地 stdio)或挂 FastAPI 用 Streamable HTTP 远程部署。工具多了会占 token,靠精简描述、按需注入、按模块分组、懒加载来降开销。
我一般装官方 Python SDK(pip install "mcp[cli]"),然后写一个 Python 文件:创建一个 MCPServer 实例,用 @mcp.tool() 装饰器把函数注册成工具。函数签名就是参数定义、docstring 就是工具描述,SDK 会自动生成 JSON Schema,完全不用手写协议。写完用 mcp dev server.py 打开 Inspector 测试;本地给宿主配 stdio 的 command+args,远程就挂 FastAPI 用 Streamable HTTP 填 URL。
分四步。启动时工具被登记进能力清单;宿主连上后调 tools/list,SDK 用函数签名自动生成 JSON Schema 返回;模型决策要调用时输出工具名和参数,宿主调 tools/call 带着参数来;SDK 帮我把参数反序列化、调我的函数,返回值再回传塞给模型。核心是模型只出意图,真正执行的是我的函数。
核心是按需加载,别全量塞。我主要用:精简 docstring(描述既是说明书也占 token,写清场景别啰嗦);按用户意图 embedding 召回 top-K 个工具只注入命中的;按模块分组路由,先路由到某组只加载那组的工具;懒加载不常用的工具、用到再拉取。远程 HTTP 复用连接避免重复握手。