适用场景
你手上有一批只在公司内网能访问的数据,或者一段只有你自己清楚的业务逻辑——查工单状态、算报价、读本地那堆 CSV。通用大模型不知道这些,也碰不到这些。MCP(Model Context Protocol)的作用,就是把这层"外部能力"用统一协议接给支持它的 AI 客户端。这套方案适合想给自己常用 AI 客户端接一个内部小工具的开发者,也适合要把团队脚本开放给 AI 调用的运维,前提是你能读写基本的 Python。
环境与前置条件
- 操作系统:macOS / Linux / Windows 都可以,stdio 传输在本机跑,不挑系统
- Python 3.10 及以上(MCP Python SDK 的要求,具体以下官方文档当前版本为准)
- 包管理:pip 或 uv,二选一,建议 uv
- 一个支持 MCP 的客户端:Claude Desktop、Cursor、VS Code 的 AI 插件等,任选其一,各自的配置字段与路径以官方文档为准
- 磁盘:项目本身几百 KB,留 200MB 给虚拟环境和依赖足够
- 内存:本地 stdio 服务通常占用几十 MB;如果工具里要加载模型或大文件,按实际需求预留
- 网络:stdio 模式全程离线也能跑;用 HTTP 传输时,需要本机或服务器能被客户端访问
如果想用 mcp dev 打开自带的调试面板(Inspector),本机还需要 Node.js 与 npx,具体以官方文档为准。
分步骤部署
第 1 步:先把 MCP 的角色关系理清
MCP 里只有三个角色:
- Host:你用的 AI 客户端本体(Claude Desktop、IDE 插件等)
- Client:Host 内部为每个 Server 建立的一条连接
- Server:你写的程序,对外暴露能力
Server 能暴露三类东西:
1. Tools(工具):可被模型调用的函数,有参数、有返回值,这是本次的重点
2. Resources(资源):可被读取的数据,形态上接近"文件"
3. Prompts(提示模板):预置的提示词片段
底层通信是 JSON-RPC 2.0。一次典型流程是这样的:Client 发 initialize 协商协议版本与双方能力 → 发 tools/list 拿到工具清单(名字、描述、参数 JSON Schema)→ 模型决定调用哪个 → Client 发 tools/call → Server 执行并把结果返回。
传输层有两种常见形态:
- stdio:Client 把 Server 当子进程启动,通过标准输入输出收发消息。本机自用更合适,不用管端口和鉴权。
- Streamable HTTP(早期是 HTTP + SSE):Server 独立跑成一个服务,通过 URL 访问。适合团队共用、远程部署。
记住 stdio 模式的一条铁律:标准输出只能走协议消息。任何多余的调试打印都会污染协议流,后文报错章节里出现频率较高的一类问题就是它。
第 2 步:建目录、装依赖
```bash
mkdir -p ~/mcp-demo && cd ~/mcp-demo
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install "mcp[cli]"
```
用 uv 的话:
```bash
uv init mcp-demo && cd mcp-demo
uv add "mcp[cli]"
```
这步在做什么:创建一个隔离的 Python 环境,并装上官方 MCP Python SDK(带 CLI 附加组件,会提供 mcp 命令行工具)。
成功标志:pip show mcp 能打印出包信息,或者 mcp --help 能列出可用命令。具体版本号以官方文档当前版本为准。
第 3 步:写最小可用的 Server
新建 server.py:
```python
import sys
from datetime import date
from mcp.server.fastmcp import FastMCP
服务名,会在客户端的工具列表里显示
mcp = FastMCP("demo-tools")
@mcp.tool()
def word_count(text: str) -> int:
"""统计文本的字符数(含空格)。
Args:
text: 需要统计的原始文本。
"""
return len(text)
@mcp.tool()
def days_between(start: str, end: str) -> int:
"""计算两个日期之间相差的天数。
Args:
start: 起始日期,格式 YYYY-MM-DD。
end: 结束日期,格式 YYYY-MM-DD。
"""
return (date.fromisoformat(end) - date.fromisoformat(start)).days
@mcp.resource("config://demo")
def demo_config() -> str:
"""返回一段演示用的配置文本。"""
return "env=dev\nretry=3\n"
if __name__ == "__main__":
不传 transport 时默认走 stdio
print("demo-tools 已启动", file=sys.stderr)
mcp.run()
```
几个要点:
- 函数名就是工具名,docstring 就是给模型看的说明书。docstring 写得越具体,模型选错工具的概率越低。
- 参数的类型注解会被 SDK 转成 JSON Schema,客户端据此做校验。缺注解容易导致参数被识别成空,或者直接报错。
- 返回值保持简单(字符串、数字、简单结构)就好;需要返回复杂对象时,SDK 会自动序列化。
- 上面那行
print(..., file=sys.stderr)是刻意示范:日志走 stderr,不碰协议流。 - 如果你装的版本里导入路径已经调整,以官方文档当前版本为准。
第 4 步:本地冒烟测试
先直接跑一遍,确认进程能起来、不崩:
```bash
python server.py
```
成功标志:终端出现 demo-tools 已启动,随后程序停住等待输入(stdio 服务就是在等 Client 说话)。按 Ctrl+C 退出即可,这一步没有别的输出是正常的。
想直接看到工具清单,可以用手写 JSON-RPC 喂给它:
```bash
printf '%s\n%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"<按官方文档当前版本填写>","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
| python server.py
```
成功标志:输出里能看到两个带 result 的响应,第二个的 result.tools 数组里有 word_count 和 days_between,每个都带着 inputSchema。
如果本机装了 Node.js,也可以用官方 CLI 拉起调试面板:
```bash
mcp dev server.py
```
面板里能直接点按钮调用工具、查看请求与响应,排查问题比较省事。命令名和参数以官方文档当前版本为准。
第 5 步:把 Server 接进客户端
以 Claude Desktop 为例,其他客户端思路一致,路径与字段以各自官方文档为准。
先找配置文件:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
文件不存在就新建,写入:
```json
{
"mcpServers": {
"demo-tools": {
"command": "/Users/you/mcp-demo/.venv/bin/python",
"args": ["/Users/you/mcp-demo/server.py"]
}
}
}
```
三个容易踩的点,这里一次说清楚:
1. command 要写虚拟环境里 Python 的绝对路径,不要写裸 python。图形界面客户端启动子进程时继承的 PATH 和你终端里的不一样,写裸命令大概率找不到。
2. args 里的脚本路径也写绝对路径。
3. Windows 上路径用双反斜杠或正斜杠,例如 C:/Users/you/mcp-demo/server.py。
用 uv 管理的话,可以这样写:
```json
{
"mcpServers": {
"demo-tools": {
"command": "uv",
"args": ["--directory", "/Users/you/mcp-demo", "run", "server.py"]
}
}
}
```
保存后完全退出客户端再重新打开(是退出进程,不是关窗口)。启动时客户端会拉起你的 Server,并调用 tools/list 取工具清单。
第 6 步(可选):改成 HTTP 传输给团队共用
如果希望多个同事共用同一个 Server,把 stdio 换成 Streamable HTTP:
```python
if __name__ == "__main__":
mcp.run(transport="streamable-http")
```
启动后监听本机某个端口(端口与访问路径以官方文档当前版本为准):
```bash
python server.py
```
客户端侧改成填 URL,而不是 command / args:
```json
{
"mcpServers": {
"demo-tools": {
"url": "http://127.0.0.1:8000/mcp"
}
}
}
```
两点提醒:一是对外暴露时要在反向代理层加 TLS 和鉴权;二是这种模式下,Server 里的工具等于开放给了所有能连上的人,写操作类工具要加确认或白名单。
验证部署是否成功
按顺序做三层验证:
1. 进程层:python server.py 能起来且不报错,Ctrl+C 能干净退出。
2. 协议层:上面那段 printf | python server.py 能拿到 tools/list 的返回,两个工具都在列表里。
3. 客户端层:在客户端里发一句
> 帮我用 word_count 统计一下这句话有多少个字符:AI 词典:模型上下文协议">模型上下文协议很好用
预期结果:对话里出现一次工具调用(通常有可展开的调用详情),工具返回一个整数,模型据此给出回答。
再试一句:
> 用 days_between 算一下 2024-01-01 到 2024-03-01 差几天
预期返回 60。
如果客户端里看不到工具,先查客户端日志(Claude Desktop 的日志在 ~/Library/Logs/Claude/ 这类目录,具体位置以官方文档为准)。
常见报错与解决
1. 客户端提示 Unexpected token ... is not valid JSON,或 Server 侧抛 JSONDecodeError
原因:Server 往标准输出打了非协议内容。print()、某个库的启动 banner、进度条都会造成这个问题——stdio 的 stdout 是协议专线。
解决:把所有调试输出改到 stderr。
```python
import logging, sys
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
print("调试信息", file=sys.stderr)
```
第三方库如果往 stdout 打印,用 contextlib.redirect_stdout(sys.stderr) 包住启动代码。
2. ModuleNotFoundError: No module named 'mcp'
原因:客户端启动时用的 Python,不是装过依赖的那个环境。
解决:把 command 换成绝对路径,并确认该解释器里确实装了包。
```bash
/path/to/.venv/bin/python -c "import mcp, sys; print(sys.executable)"
```
有输出说明这个解释器可用,把打印出的路径填进配置。
3. spawn python ENOENT 或 Permission denied
原因:command 不在客户端进程的 PATH 里;或者把脚本直接当 command 用时,脚本没有执行权限。
解决:改用绝对路径,必要时补执行权限。
```bash
chmod +x /path/to/server.py
```
4. 客户端里工具列表是空的
原因:改完配置没重启客户端;或者函数缺类型注解、缺 docstring,生成的 schema 不合法被跳过。
解决:完全退出客户端再启动;给每个工具补上参数注解和说明。
```bash
改完配置后,先确认服务本身没问题
python /path/to/server.py
```
5. 工具调用超时
原因:工具里做了耗时操作,比如读大文件、慢查询、调外部接口。
解决:把长任务拆小,或改成异步函数 async def,并在阻塞点加超时与重试。
```python
import asyncio
@mcp.tool()
async def slow_query(sql: str) -> str:
"""执行一条查询,超时后返回错误信息。"""
try:
return await asyncio.wait_for(_run(sql), timeout=10)
except asyncio.TimeoutError:
return "查询超时,请缩小范围后重试"
```
具体超时阈值由客户端决定,以官方文档为准。
后续维护
- 备份:把
server.py、依赖清单(requirements.txt或uv.lock)、客户端配置文件一起放进版本控制。配置里如果写了密钥,用环境变量引用,不要直接提交。 - 升级:升级 SDK 前先读官方 CHANGELOG。MCP 协议本身在演进,版本协商、字段命名都可能调整,升级后按"验证部署"那一节的三层验证重跑一遍。依赖锁定到具体版本,避免某次自动升级把在用的服务打挂。
- 日志:stdio 模式下日志只能走 stderr,建议重定向到文件:
```bash
python server.py 2>> ~/mcp-demo/server.log
```
HTTP 模式下 Server 是常驻进程,交给 systemd、supervisor 或容器编排管理,并配好日志轮转。
- 监控:至少盯三件事——进程是否存活、
tools/call的失败率、单次调用耗时。可以在工具函数外层套一层计时与计数,把指标打到 stderr 或监控系统。 - 权限:工具就是递给 AI 的手。凡是会写数据、发消息、删文件、动线上配置的工具,要么做成只读,要么执行前做二次确认,要么加参数白名单。别让一次提示注入把生产环境改坏。
- 演进:需要给多个客户端复用时,先把工具拆细、命名清晰(
query_order比do_stuff好),再考虑把 stdio 版迁到 HTTP 版。工具数量多起来以后,描述文字的措辞会直接影响模型的选择准确率,值得定期回看。
写到这里,一个最小可用的 MCP Server 就跑起来了。下一步建议从自己每天重复做的一件小事开始——查一次状态、转一次格式、拉一份报表——把它包成工具接进去,比先设计一个大而全的服务更划算。
