AI编程 2026-07-29 71 次浏览

MCP 入门实战:用 Python 跑通一个可调用的天气工具

使用官方 Python SDK 2.0.0,实现一个不依赖网络和密钥的天气工具,并验证工具发现、参数协议、正常调用和异常返回。

摘要:本文面向会基础 Python、第一次接触 MCP 的读者。我们将使用官方 Python SDK 2.0.0,实现一个不依赖网络和密钥的天气工具,并用客户端验证工具发现、参数协议、正常调用和异常返回。

MCP 通过客户端把主机与工具服务器连接起来

大模型能回答问题,但它不会天然知道你的文件、数据库或业务接口。要让模型安全、稳定地使用这些外部能力,每个应用都自己设计一套接入方式,很快就会出现重复开发和兼容问题。

Model Context Protocol(模型上下文协议,MCP)解决的正是“应用如何用统一方式连接外部上下文与工具”这个问题。它采用客户端—服务器架构,服务器可以暴露工具、资源和提示词;本文只聚焦最适合入门的 Tool(工具)

我们会沿着一条最短路径完成 Demo:先写 MCP Server,再用 MCP Client 发现并调用工具,最后用 Inspector 做可视化验证。全部代码已经在本机实际运行通过。

完成后,你会得到什么

最终项目只有三个核心文件:

mcp-weather-demo/
├── pyproject.toml
├── server.py
└── client.py

运行 client.py 后,你将看到三类可观察结果:

  1. 客户端发现服务器提供了 get_weather 工具。
  2. SDK 根据 Python 类型注解生成了工具的输入 Schema。
  3. 北京返回结构化天气;杭州返回可被客户端识别的工具错误。

这个 Demo 不调用真实天气 API。固定数据可以排除网络、限流和密钥问题,让第一次练习只关注 MCP 本身。

开始前准备 Python 与 uv

本文代码以 2026-07-29 的稳定版 mcp==2.0.0 为准。官方 SDK v2 要求 Python 3.10 或更高版本,安装包名称仍然是 mcp。版本信息可在 MCP Python SDK 文档PyPI 核对。

先检查环境:

python3 --version
uv --version

如果还没有 uv,可以先安装:

python3 -m pip install uv

然后创建项目并锁定依赖:

mkdir mcp-weather-demo
cd mcp-weather-demo
uv init --bare
uv add "mcp[cli]==2.0.0"

锁定主版本很重要。MCP Python SDK 2.0 更换了部分核心 API;如果把旧教程里的 FastMCP 与本文的 MCPServer 混在一起,代码很容易在导入阶段失败。

先认清主机、客户端、服务器和工具

MCP 的角色名称很接近,但职责不同。根据 MCP 官方架构说明

  • 主机(Host):用户正在使用的 AI 应用,例如 IDE 或桌面聊天应用。
  • 客户端(Client):主机内部负责说 MCP 的组件;通常每个服务器对应一个客户端连接。
  • 服务器(Server):向客户端提供能力的程序,可以运行在本地,也可以远程部署。
  • 工具(Tool):服务器暴露的可执行函数,例如查天气、读数据库或创建工单。

主机内的客户端连接服务器,服务器再暴露天气工具

上图最值得记住的是两层包含关系:客户端属于主机,工具属于服务器。MCP 规范负责客户端和服务器之间的通信,但不会替你实现具体天气业务,也不会规定模型应该如何组织最终回答。

本文的 client.py 是测试程序,不包含大模型。它只扮演主机侧的调用者,帮助我们确认服务器遵守 MCP 协议。等 Demo 验证通过后,再接入真实 AI 主机,排错会容易得多。

第一步:用一个函数创建 MCP Server

新建 server.py,写入下面的完整代码:

from typing import Annotated

from mcp.server import MCPServer
from mcp.types import ToolAnnotations
from pydantic import BaseModel, Field


mcp = MCPServer(
    "天气小助手",
    instructions="查询内置示例城市的天气。所有数据仅用于 MCP 入门演示。",
)


class WeatherReport(BaseModel):
    """天气工具的结构化返回值。"""

    city: str
    temperature_c: int
    condition: str
    advice: str


WEATHER_DATA: dict[str, WeatherReport] = {
    "北京": WeatherReport(
        city="北京",
        temperature_c=28,
        condition="晴",
        advice="紫外线较强,外出注意防晒。",
    ),
    "上海": WeatherReport(
        city="上海",
        temperature_c=31,
        condition="多云",
        advice="体感闷热,建议及时补水。",
    ),
    "深圳": WeatherReport(
        city="深圳",
        temperature_c=30,
        condition="阵雨",
        advice="短时降雨,出门记得带伞。",
    ),
}


@mcp.tool(
    title="查询示例天气",
    annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False),
)
def get_weather(
    city: Annotated[
        str,
        Field(description="要查询的城市,目前支持北京、上海、深圳。"),
    ],
) -> WeatherReport:
    """查询一个示例城市的天气。"""
    normalized_city = city.strip().removesuffix("市")
    if normalized_city not in WEATHER_DATA:
        supported = "、".join(WEATHER_DATA)
        raise ValueError(f"暂不支持 {city!r},可选城市:{supported}")
    return WEATHER_DATA[normalized_city]


if __name__ == "__main__":
    mcp.run()

SDK 根据函数名、类型注解和文档说明生成工具协议

真正与 MCP 相关的核心只有三处:

  1. MCPServer(...) 创建服务器。
  2. @mcp.tool() 把普通 Python 函数注册为工具。
  3. mcp.run() 在直接运行文件时启动 stdio 服务器。

函数名会成为工具名,文档字符串会成为工具描述,参数类型和 Field 会生成 JSON Schema。WeatherReport 则定义结构化输出。官方 v2 文档的 Tools 章节 对这套推导规则有完整说明。

ToolAnnotations 里的 read_only_hint=True 表示工具只读,open_world_hint=False 表示它只查询当前内置数据。它们是给客户端的行为提示,不是权限控制;真正的安全边界仍要在服务器内部实现。

第二步:发现工具并完成一次调用

新建 client.py

import asyncio
import json

from mcp import Client

from server import mcp


async def main() -> None:
    async with Client(mcp) as client:
        tools_result = await client.list_tools()
        print("可用工具:", [tool.name for tool in tools_result.tools])
        print(
            "输入协议:",
            json.dumps(
                tools_result.tools[0].input_schema,
                ensure_ascii=False,
                indent=2,
            ),
        )

        success = await client.call_tool("get_weather", {"city": "北京"})
        assert success.is_error is False
        assert success.structured_content is not None
        assert success.structured_content["city"] == "北京"
        print(
            "成功结果:",
            json.dumps(success.structured_content, ensure_ascii=False, indent=2),
        )

        failure = await client.call_tool("get_weather", {"city": "杭州"})
        assert failure.is_error is True
        print("异常分支:", failure.content[0].text)


if __name__ == "__main__":
    asyncio.run(main())

这里使用 Client(mcp) 直接连接服务器对象,属于官方 SDK 提供的内存传输。它不启动子进程,也不占用端口,特别适合单元测试和入门验证;官方测试指南 也采用同样方式。

客户端先发现工具,再传入城市,最后接收结构化天气结果

一次调用并不是“直接执行函数”这么简单。客户端先通过 list_tools() 获得工具名称、描述和输入 Schema,再通过 call_tool() 发送参数,最后检查 is_error 并读取 structured_content。真实 AI 主机只是在这条链路前后增加了模型决策和自然语言回复。

第三步:运行并验证正常与异常路径

在项目目录执行:

uv sync
uv run python client.py

已验证的关键输出如下:

可用工具: ['get_weather']
成功结果: {
  "city": "北京",
  "temperature_c": 28,
  "condition": "晴",
  "advice": "紫外线较强,外出注意防晒。"
}
异常分支: Error executing tool get_weather: 暂不支持 '杭州',可选城市:北京、上海、深圳

只看到成功结果还不够。杭州不在内置数据中,工具抛出的 ValueError 会被 MCP 转成 is_error=True 的工具结果,而不是让整个客户端崩溃。模型或上层应用因此可以读取错误信息,再决定换一个参数或向用户追问。

还可以启动官方 MCP Inspector 做可视化验证:

uv run mcp dev server.py

打开命令输出的网址,进入 Tools 页签,选择 get_weather 并传入 北京。Inspector 会根据同一份输入 Schema 自动生成表单。

最常见的四个问题

导入 FastMCP 失败

你很可能混用了 1.x 教程和 2.0 依赖。本文使用 from mcp.server import MCPServer,并把依赖锁定为 mcp[cli]==2.0.0。如果维护旧项目,应先阅读官方迁移指南,不要只替换一个类名。

Inspector 连不上服务器

先运行 uv run python client.py。内存调用成功后,再检查 uv run mcp dev server.py。分层验证可以区分“工具实现错误”和“stdio 启动错误”。

在 stdio 模式里随意 print

stdio 的标准输出承载协议消息。服务器日志不要写到 stdout,应使用默认写入 stderr 的日志配置。本文的输出全部发生在测试客户端,不会污染服务器协议流。

接入真实主机后找不到文件

真实主机通常从自己的工作目录启动服务器。配置启动命令时应使用 server.pyuv 的绝对路径。官方 Connect to a real host 页面给出了 Claude Desktop、Claude Code、Cursor 和 VS Code 的配置方式。

这个 Demo 有意省略了什么

为了让第一次运行稳定,本文没有加入真实 HTTP 天气接口、鉴权、数据库、重试、超时和限流。内存 Client 验证了工具发现、参数 Schema、工具调用和错误语义,但没有验证子进程 stdio 或远程 Streamable HTTP 的部署行为。

把它改成真实服务时,至少需要补上:

  • 将固定字典替换为异步 HTTP 请求,并设置超时与明确的错误映射。
  • 不把 API Key 写进代码或返回结果,改用环境变量和密钥管理服务。
  • 对会产生副作用的工具增加权限校验、幂等设计和人工确认。
  • 为正常、无数据、上游超时和非法参数分别编写测试。

总结与下一步

通过这个最小 Demo,可以先建立四个可靠认识:

  1. MCP 是客户端与服务器之间的统一协议,不是模型本身。
  2. MCPServer 通过函数名、文档字符串和类型注解生成工具协议。
  3. 客户端先发现工具,再调用工具,并通过结构化结果或错误结果继续处理。
  4. 第一次练习应先用固定数据和内存 Client 验证协议,再接真实 API 与 AI 主机。

下一步可以把 WEATHER_DATA 换成真实天气 API,再用 Inspector 验证;随后把同一个 server.py 接入你常用的 MCP 主机。此时你已经不是在“背 MCP 概念”,而是在扩展一条已经跑通的调用链。