第二章:创建第一个 Python MCP Server
上一章,我们已经明确了 Health Tracker 的开发思路:
ChatGPT 负责理解用户需求,MCP Server 负责提供工具,SQLite 负责保存数据。
这一章,我们暂时不开发健康记录功能,也不连接数据库,而是从零开始创建一个最简单的 MCP Server。
我们的目标只有三个:让 MCP Server 成功启动、让 MCP Inspector 发现工具、让工具能够被正常调用。
2.1 创建 Python 项目
首先打开终端,创建项目目录:
mkdir health-tracker-agent
cd health-tracker-agent
本课程使用 uv 管理 Python 环境和依赖。
如果已经安装了 uv,可以直接初始化项目:
uv init
初始化后,目录中会出现类似下面的文件:
health-tracker-agent/
├── .python-version
├── README.md
├── main.py
└── pyproject.toml
其中,我们需要重点关注两个文件。
main.py 是 Python 程序的入口文件,接下来要在这里编写 MCP Server。
pyproject.toml 则负责保存项目的基本信息和依赖配置。
安装 MCP SDK
执行:
uv add "mcp>=2,<3"
这里使用 MCP Python SDK 2.x,因此后面的代码都按照 SDK 2.x 的接口编写。
安装完成后,项目中还会生成 uv.lock 文件,用于锁定依赖版本。
现在,我们已经有了一个可以开发 MCP Server 的 Python 项目。
2.2 编写第一个 MCP Server
打开 main.py,删除初始化时生成的示例代码,替换成下面的内容。
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("Health Tracker")
@mcp.tool()
def hello(name: str) -> str:
"""根据用户姓名返回一段问候语。"""
return f"你好,{name}!这是我的第一个 MCP 工具。"
if __name__ == "__main__":
mcp.run(
transport="streamable-http",
host="127.0.0.1",
port=8000,
)
先不急着理解每一行代码,我们直接运行它。
uv run python main.py
如果服务正常启动,终端会保持运行状态,等待客户端连接。
我们的 MCP Server 地址是:
http://127.0.0.1:8000/mcp
注意:这是 MCP 协议接口,并不是一个普通网页。
直接在浏览器地址栏中打开它,不一定会看到我们想象中的页面,因为 MCP 客户端需要按照协议发送请求。
那么,应该如何测试它呢?
这就需要使用 MCP Inspector。
2.3 使用 MCP Inspector 测试工具
MCP Inspector 是专门用于调试 MCP Server 的工具。
它可以帮助我们查看服务提供了哪些工具、每个工具接收什么参数,以及调用工具后返回什么结果。
保持 Python MCP Server 正常运行,再打开一个终端。
执行:
npx @modelcontextprotocol/inspector
第一次运行时,npx 可能会提示安装相关依赖,按照终端提示操作即可。
启动后,终端会显示 Inspector 的本地访问地址。打开该地址,进入调试界面。
连接 MCP Server
在 Inspector 中,选择:
- Transport Type:
Streamable HTTP - URL:
http://127.0.0.1:8000/mcp
然后点击连接。
如果连接成功,就可以继续查看 MCP Server 提供的工具。
查看工具列表
找到 Tools 页面,点击列出工具。
正常情况下,我们应该看到:
hello
工具的描述为:
根据用户姓名返回一段问候语。
同时,Inspector 还可以识别这个工具需要一个参数:
name: string
这就说明,我们刚才编写的 Python 函数,已经成功注册成了 MCP 工具。
调用 hello 工具
接下来,选择 hello,输入参数:
{
"name": "Andy"
}
点击执行。
如果一切正常,返回结果应包含:
你好,Andy!这是我的第一个 MCP 工具。
到这里,我们就完成了第一个 MCP Server 的开发与测试。
不过,有一个问题值得思考:
Inspector 为什么知道我们编写了一个叫 hello 的工具?它又是怎么知道这个工具需要一个字符串参数的?
接下来,我们通过代码解释其中的原理。
2.4 MCP Server 是如何注册工具的?
重新回到 main.py。
首先看这一行:
mcp = MCPServer("Health Tracker")
它创建了一个 MCP Server 实例,并将服务器名称设置为 Health Tracker。
这里的名称是服务器标识,不是 Python 文件名,也不是工具名称。
接下来是:
@mcp.tool()
def hello(name: str) -> str:
"""根据用户姓名返回一段问候语。"""
return f"你好,{name}!这是我的第一个 MCP 工具。"
其中最关键的是:
@mcp.tool()
这是一个 Python 装饰器。
它的作用是把下面的函数注册为 MCP 工具。
如果没有这个装饰器,hello() 就只是一个普通 Python 函数,MCP 客户端不会自动把它当作可调用工具。
工具名称从哪里来?
def hello(name: str) -> str:
函数名 hello 就是默认的工具名称。
所以,Inspector 才会在工具列表中显示 hello。
工具参数从哪里来?
注意函数中的类型注解:
name: str
它告诉 MCP SDK,name 参数应该是字符串。
MCP SDK 可以根据函数签名和类型注解生成工具的输入 Schema。
因此,客户端能够知道工具需要哪些参数以及参数类型。
工具描述从哪里来?
函数中的文档字符串:
"""根据用户姓名返回一段问候语。"""
不仅是写给程序员看的注释,也可以作为工具描述提供给 MCP 客户端。
这非常重要。
因为当我们将 MCP Server 连接到 ChatGPT 后,ChatGPT 需要理解每个工具的用途,才能判断什么时候应该调用它。
所以,在 MCP 开发中,函数名称、参数类型和 Docstring 都应该尽量准确。
2.5 MCP Server 是如何接收请求的?
接下来,看程序最后的代码:
if __name__ == "__main__":
mcp.run(
transport="streamable-http",
host="127.0.0.1",
port=8000,
)
这里涉及三个重要概念。
transport:通信方式
transport="streamable-http"
我们选择的是 Streamable HTTP 传输方式。
它允许 MCP 客户端通过 HTTP 与 MCP Server 通信。
本课程后面需要通过 Tunnel 将本地 MCP Server 连接到 ChatGPT,因此使用 Streamable HTTP 比较合适。
MCP 还支持其他传输方式,例如 stdio,它通常用于客户端直接启动本地 MCP Server 进程,并通过标准输入输出通信。
host:监听地址
host="127.0.0.1"
127.0.0.1 表示本机回环地址。
这意味着当前服务主要供本机程序访问。
我们在开发阶段使用这个地址,可以避免将服务直接暴露给局域网中的其他设备。
后续部署到 Docker 容器时,我们会根据容器网络环境调整监听地址。
port:监听端口
port=8000
这里指定 MCP Server 使用 8000 端口。
如果端口已经被其他程序占用,可以更换为其他未使用的端口,但 Inspector 中的连接地址也需要同步修改。
2.6 为什么 Inspector 能够自动发现工具?
现在,我们已经知道 @mcp.tool() 可以注册 Python 函数。
但是 Inspector 并不是通过读取 main.py 文件发现这些工具的。
它根本不需要访问我们的 Python 源代码。
真正的过程是:

当 Inspector 与 MCP Server 建立连接后,双方会进行初始化交互。
随后,Inspector 可以发送 tools/list 请求,获取服务器提供的工具列表。
例如,服务器可能返回类似下面的工具描述:
{
"name": "hello",
"description": "根据用户姓名返回一段问候语。",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": ["name"]
}
}
这里展示的是简化后的结构,用来帮助理解工具 Schema;实际返回内容可能包含其他字段。
当我们在 Inspector 中输入 Andy 并点击执行时,客户端又会发送 tools/call 请求。
请求中包含工具名称以及参数:
{
"name": "hello",
"arguments": {
"name": "Andy"
}
}
MCP Server 根据工具名称找到对应的 Python 函数,完成调用,再将结果返回。
因此,MCP 的关键并不是让客户端直接执行 Python 源代码,而是通过统一协议发现和调用服务端提供的工具。
2.7 添加第二个 MCP 工具
为了进一步理解工具注册机制,我们再增加一个简单的计算工具。
在 main.py 中,继续添加:
@mcp.tool()
def add(a: float, b: float) -> float:
"""计算两个数字的和。"""
return a + b
现在,完整代码如下:
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("Health Tracker")
@mcp.tool()
def hello(name: str) -> str:
"""根据用户姓名返回一段问候语。"""
return f"你好,{name}!这是我的第一个 MCP 工具。"
@mcp.tool()
def add(a: float, b: float) -> float:
"""计算两个数字的和。"""
return a + b
if __name__ == "__main__":
mcp.run(
transport="streamable-http",
host="127.0.0.1",
port=8000,
)
保存文件后,停止之前运行的 MCP Server,然后重新启动:
uv run python main.py
回到 MCP Inspector,重新连接或刷新工具列表。
现在应该能够看到两个工具:
hello
add
选择 add,输入:
{
"a": 10,
"b": 20
}
执行后,应返回计算结果:
30.0
这个例子说明,一个 MCP Server 可以注册多个工具。
未来的 Health Tracker 就是利用同样的方式,注册 record_food、record_weight、get_daily_summary 等工具。
2.8 常见问题
问题一:启动时报端口占用
如果出现类似:
Address already in use
说明 8000 端口已经被其他程序占用。
有两种解决办法:关闭占用端口的程序,或者给 MCP Server 换一个端口。
1.Mac:查找并关闭占用程序
在终端运行:
lsof -nP -iTCP:8000 -sTCP:LISTEN
可能看到:
COMMAND PID USER FD TYPE NAME
Python 12345 andy 10u IPv4 TCP *:8000 (LISTEN)
其中:
Python:占用端口的程序。12345:进程编号,也就是 PID。
先确认这是不是之前启动的服务。 如果它所在的终端还在,回到那个终端按 Ctrl + C 即可停止。
如果找不到原来的终端,可以执行:
kill 12345
把 12345 替换成你查到的 PID。如果普通关闭无效,再强制结束:
kill -9 12345
然后重新启动 MCP Server。
2.Windows:查找并关闭占用程序
打开 命令提示符 CMD,运行:
netstat -ano | findstr :8000
找到本地地址端口为 8000,且状态为 LISTENING 的一行:
TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING 12345
最后的 12345 就是进程 PID。查看它对应什么程序:
tasklist /FI "PID eq 12345"
如果是之前启动的 MCP Server,优先回到原来的终端按 Ctrl + C 停止。
也可以根据 PID 结束进程:
taskkill /PID 12345 /F
如果提示“拒绝访问”,以管理员身份打开 CMD,再执行。
3.如果占用程序还要继续使用,就换端口
例如,把 MCP Server 从 8000 改为 8001。
如果你使用 Python MCP SDK 的 FastMCP,并在创建实例时配置端口,可以修改为:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(
"Health Tracker",
host="0.0.0.0",
port=8001,
)
if __name__ == "__main__":
mcp.run(transport="streamable-http")
如果你通过 Uvicorn 启动,则修改启动命令中的端口:
uvicorn main:app --host 0.0.0.0 --port 8001
这两种方式按项目实际启动方式选择即可。
换端口后,相关连接也要同步修改。 例如,Tunnel 原来转发到 http://localhost:8000,现在就要改成 http://localhost:8001;直接连接本地服务的客户端,也要更新地址。
端口就像窗口号,8000 号窗口已经有人使用,我们可以让原来的程序退出,也可以换到 8001 号窗口。
问题二:Inspector 无法连接 MCP Server
首先确认 Python 服务仍然运行。
然后检查 Inspector 中是否选择了正确的传输方式:
Streamable HTTP
以及正确的 URL:
http://127.0.0.1:8000/mcp
特别注意最后的 /mcp 路径。
问题三:新增工具后,Inspector 看不到
通常是因为修改 Python 代码后,没有重新启动 MCP Server,或者 Inspector 仍在使用之前的连接状态。
可以先重启 Python 服务,再重新连接 Inspector,并刷新工具列表。
问题四:MCP 工具能否直接访问数据库?
可以。
MCP 工具本质上就是 Python 函数。
只要服务端程序拥有相应权限,函数就可以执行 SQLite 查询、访问文件或调用其他服务。
不过,MCP 工具的权限应遵循最小权限原则,尤其是后面接入 ChatGPT、开放网络访问时,不能将没有身份验证的敏感数据接口直接暴露到公网。
2.9 本章总结
这一章,我们从一个空目录开始,完成了第一个 MCP Server 的开发。
回顾一下整个过程:
- 使用
uv初始化 Python 项目。 - 安装 MCP Python SDK 2.x。
- 创建
MCPServer实例。 - 使用
@mcp.tool()注册 Python 函数。 - 通过 Streamable HTTP 启动服务。
- 使用 MCP Inspector 发现并调用工具。
- 增加第二个工具,验证一个 MCP Server 可以提供多种能力。
其中,最重要的知识点是:
MCP Server 将 Python 函数以标准化工具接口的形式提供给 MCP 客户端。
客户端不需要了解 Python 源代码,只需要通过 MCP 协议获取工具的名称、描述和参数 Schema,就能够调用这些工具。
到这里,我们已经具备了开发 Health Tracker 的基础。
下一章预告:使用 SQLite 保存饮食记录
目前,我们编写的 hello 和 add 工具只能返回计算结果,还没有保存任何数据。
下一章,我们将开始开发第一个真正有实际用途的健康记录工具:
record_food(...)
我们会先创建 SQLite 数据库和饮食记录表,再编写数据保存与查询功能。
最终要实现的效果是:通过 MCP Inspector 调用 record_food,将一条饮食记录真正写入 SQLite,并且在程序重启后依然能够查询到它。
从下一章开始,Health Tracker 将不再只是一个演示程序,而是逐步成为能够保存真实数据的应用。
想系统学习这门课程?
你已阅读完本文。需要更完整的知识体系,可以继续了解「Vibe Coding 实战课程」。
查看课程详情