简介:这是一份面向大模型应用开发者与Agent方向学习者的MCP入门实战资料,围绕Model Context Protocol这一由Anthropic提出的智能体工具调用协议展开,帮助读者跨越Function calling门槛过高、外部函数重复开发的痛点,从零搭建可运行的MCP客户端与服务器。内容覆盖MCP技术体系与Agent开发脉络回顾、uv依赖管理工具使用、极简客户端搭建、接入OpenAI与DeepSeek在线模型及本地ollama、vLLM模型,以及天气查询服务器的完整创建与Inspector调试,并延伸至客户端与服务器的进阶功能。资源为1个PDF文件,压缩包约47.66MB,适合按章节顺序阅读并同步动手实践。目前已有1124人学习下载,可作为理解MCP通讯机制、掌握客户端与服务器协作流程的参考材料。
1. MCP 快速入门:从一条 stdio 命令到能被 Agent 调用的工具
很多人第一次接触 MCP,是在某个 AI 编辑器里看到「添加 MCP Server」的按钮,点进去填了个命令,然后就没有然后了。真正卡住的地方从来不是概念,而是:我写的这个 Server,怎么让 Agent 稳定地发现、调用、拿到结构化结果?MCP(Model Context Protocol)解决的正是这件事——它把「模型能调用的能力」抽象成一套标准协议,Server 暴露 tools、resources、prompts,Client(也就是 Agent 宿主)负责握手、列能力、转发调用。你不需要改模型,只需要按协议把工具挂上去。
这篇面向想动手的人:会用 Python 写函数、装过 uv、知道 Agent 大概是什么。目标是从零跑通一个本地 stdio Server,再用一个最小 Client 调通它,最后讲清楚参数、传输方式和排错。全程不依赖任何云端账号,本地就能复现。
2. MCP 协议核心概念与 uv 环境准备
2.1 MCP 的三种原语:tool、resource、prompt 到底怎么选
MCP 把 Server 能提供的东西分成三类,选错了后面调用会很别扭。
- tool:有副作用、需要参数、返回执行结果。比如查数据库、发请求、写文件。Agent 会把它当成「函数」来调。
- resource:只读数据,用 URI 标识,比如
file:///logs/app.log。适合把上下文喂给模型,而不是让模型去执行。 - prompt:预置的提示模板,Server 提供、Client 选用,常用于把复杂任务固化成可复用入口。
判断标准很简单:要执行动作选 tool,要读数据选 resource,要复用一段提示词选 prompt。新手最容易把所有东西都塞成 tool,结果模型面对一堆「读文件」工具反复试错。
2.2 用 uv 装 Python 环境,避开依赖地狱
MCP 官方 Python SDK 迭代快,用 uv 管理最省心。uv 是 Rust 写的包与环境管理器,装依赖比 pip 快一个量级,还能直接锁定 Python 版本。
# 安装 uv(Linux/macOS) curl -LsSf https://astral.sh/uv/install.sh | sh # Ubuntu 上如果 curl 装完找不到命令,重开 shell 或 source 一下 source $HOME/.local/bin/env # 初始化项目,指定 Python 版本 uv init mcp-demo cd mcp-demo uv python pin 3.11 # 添加 MCP SDK uv add "mcp[cli]"逻辑说明:uv init生成pyproject.toml,uv python pin把解释器版本写进.python-version,避免团队里有人用 3.8 跑不起来。uv add "mcp[cli]"会同时装 SDK 和命令行工具mcp,后面调试要用。
参数说明:mcp[cli]里的方括号是 extras 语法,只装核心 SDK 不带 CLI 的话,mcp dev这类命令会缺失。国内网络慢可以设UV_INDEX_URL指向镜像,但别写死在项目文件里,用环境变量更干净。
提示:如果机器完全离线,先在能联网的机器上
uv pip download或直接拷贝 uv 缓存目录,再在目标机uv sync --offline。别用系统 pip 混装,版本冲突排查成本很高。
2.3 传输方式选型:stdio 还是 HTTP
| 传输方式 | 适用场景 | 启动方式 | 注意点 |
|---|---|---|---|
| stdio | 本地工具、编辑器集成 | Client 拉起子进程 | 日志绝不能写 stdout |
| Streamable HTTP | 远程共享、多 Client | Server 常驻监听 | 需要处理会话与鉴权 |
| SSE(旧) | 兼容老 Client | HTTP 长连接 | 新项目不建议再用 |
本地开发一律先上 stdio,因为它最简单:Client 用命令启动 Server,两者通过标准输入输出交换 JSON-RPC 消息。代价是任何print()都会污染协议流,导致 Client 解析失败。调试信息一律走stderr或 logging。
3. 写一个最小 MCP Server 并本地跑通
3.1 用 FastMCP 定义第一个 tool
官方 SDK 提供FastMCP装饰器风格,几行就能起一个 Server。
# server.py from mcp.server.fastmcp import FastMCP mcp = FastMCP("demo-server") @mcp.tool() def add(a: int, b: int) -> int: """两数相加。a 和 b 必须是整数。""" return a + b @mcp.tool() def word_count(text: str) -> dict: """统计文本的字符数和词数。""" return {"chars": len(text), "words": len(text.split())} if __name__ == "__main__": mcp.run(transport="stdio")逻辑说明:@mcp.tool()把普通函数注册成 MCP tool,函数签名和类型注解会被自动转成 JSON Schema,Agent 靠这个 Schema 决定怎么传参。docstring 不是装饰,它会作为工具描述发给模型——写清楚「参数是什么、返回什么」,模型调用准确率会明显提升。
参数说明:FastMCP("demo-server")里的名字是 Server 标识,Client 侧会看到。mcp.run(transport="stdio")指定传输方式,本地调试就用它。返回类型建议用dict或基础类型,别返回自定义对象,序列化会出问题。
3.2 用 mcp dev 做交互式调试
写完别急着接 Agent,先用官方调试器验证工具本身没问题。
# 启动调试界面 uv run mcp dev server.py它会起一个本地 Inspector,你能看到 Server 暴露了哪些 tool、每个 tool 的 Schema、手动填参数调用看返回。这一步能挡掉 80% 的低级错误:类型写错、docstring 缺失、返回值不可序列化。
注意:如果 Inspector 里工具列表是空的,先检查函数有没有被
@mcp.tool()装饰,再看有没有在if __name__ == "__main__"之前定义。装饰器必须在模块加载时就执行到。
3.3 常见启动报错与定位方法
| 现象 | 大概率原因 | 处理 |
|---|---|---|
| Client 连不上,无输出 | Server 往 stdout 打了日志 | 改用 stderr / logging |
| 工具列表为空 | 装饰器没生效或导入失败 | 单独uv run python server.py看报错 |
| 调用返回 schema 错误 | 类型注解缺失或用了复杂类型 | 补注解,返回基础类型 |
| 进程秒退 | 依赖没装进当前环境 | uv sync后重试 |
定位核心思路:把 Server 当普通进程先跑起来。uv run python server.py能正常阻塞等待输入,说明进程没问题,问题在协议层;如果直接报错,就是代码或依赖问题。
4. 用 Client 调通 Server:从握手到工具调用
4.1 最小 Client 的完整调用链
Client 的职责是:启动 Server 子进程 → 初始化握手 → 列出工具 → 调用工具。下面是一个能直接跑的版本。
# client.py import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): params = StdioServerParameters( command="uv", args=["run", "python", "server.py"], ) async with stdio_client(params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() # 握手 tools = await session.list_tools() # 列能力 print("tools:", [t.name for t in tools.tools]) result = await session.call_tool( "add", {"a": 3, "b": 5} ) print("result:", result.content) asyncio.run(main())逻辑说明:StdioServerParameters描述怎么拉起 Server,stdio_client建立管道,ClientSession封装 JSON-RPC 会话。initialize()是必须的握手步骤,跳过它后续调用会失败。list_tools()返回工具元数据,call_tool()按名字和参数字典调用。
参数说明:command和args要能被系统直接执行,uv run python server.py是最稳的写法,因为它会自动用项目环境。call_tool的第二个参数是 dict,键必须和 Schema 里的参数名一致,类型也要对,传字符串给 int 参数会被拒。
4.2 把 Server 接进 Agent 宿主的配置写法
大多数支持 MCP 的编辑器或 Agent 框架,配置本质就是上面那段StdioServerParameters的 JSON 化。
{ "mcpServers": { "demo": { "command": "uv", "args": ["--directory", "/abs/path/to/mcp-demo", "run", "python", "server.py"] } } }逻辑说明:--directory让 uv 切到项目目录再执行,避免相对路径找不到pyproject.toml。路径一定用绝对路径,Agent 宿主的工作目录和你终端不一样。
参数说明:不同宿主字段名可能叫mcpServers或servers,但command/args结构一致。如果宿主支持环境变量,把密钥类配置放env字段,别硬编码进 args。
4.3 调用失败时的排查顺序
遇到「工具调用了但没结果」或「Agent 说找不到工具」,按这个顺序查:
- 单独跑 Client 脚本,确认
list_tools()有输出。没有就是 Server 侧问题。 - 有工具但调用报错,看
call_tool返回的isError字段和错误文本。 - 参数校验失败,对照 Inspector 里的 Schema 逐个核对类型。
- 宿主里不生效,检查配置路径是否为绝对路径、命令是否在宿主 PATH 里。
提示:Agent 宿主通常有日志目录,MCP 握手和调用记录会写进去。比起猜,直接翻日志里 Server 的 stderr 输出最快。
5. MCP 进阶:让工具被 Agent 稳定选中
5.1 工具描述与参数设计的三条经验
工具能不能被正确调用,一半取决于描述质量。三条实操经验:
- 名字用动词开头:
search_docs比docs好,模型对动作语义更敏感。 - docstring 写清边界:说明参数取值范围、返回结构、什么情况下不该用。比如「仅支持 UTF-8 文本,超过 10MB 请改用分片接口」。
- 参数扁平化:能用
str、int、bool就别嵌套对象,嵌套会让模型传参出错率上升。
5.2 用 resource 暴露只读上下文
当你要给模型喂日志、配置、文档时,用 resource 比 tool 更合适。
@mcp.resource("config://app") def get_config() -> str: """返回应用当前配置。""" return open("config.yaml", encoding="utf-8").read()逻辑说明:URI 是 resource 的标识,Client 可以按 URI 读取。只读、无副作用,模型不会「误执行」。参数说明:URI scheme 自定义即可,但要保证唯一性和可读性,方便在 Client 侧做权限控制。
5.3 验证工具是否真的被调用
最后一步是确认 Agent 确实走了 MCP,而不是自己编了答案。最直接的办法是在 tool 里加一行 stderr 日志:
import sys @mcp.tool() def add(a: int, b: int) -> int: print(f"[call] add a={a} b={b}", file=sys.stderr) return a + b跑一次 Agent 任务,看宿主日志里有没有这行输出。有,说明调用链通了;没有,说明模型选择了别的路径或工具没被注册。这个技巧在排查「Agent 答对了但不确定是不是调了工具」时特别有用,也是把 MCP 从 demo 推向可用工具的关键一步。
本文还有配套的精品资源,点击获取