第 2 课

官方天气服务器:自测后接入 Claude Code、Claude Desktop、Cursor 与 VS Code

按 MCP 官网教程写出查询美国天气的 weather.py,记住 stdio 服务器不能往 stdout 打印,用宿主同款启动命令和 Python 客户端自测,再接入 Claude Code、Claude Desktop、Cursor、VS Code,并学会从三处排查连接失败。

图文25 分钟Claude Code 官方文档 ↗

课程目录第 2 / 3 课

学习位置仅保存在当前浏览器,有效期 180 天。

01 / 图文教材

图文教材

本课依据:MCP 官网「Build an MCP server」https://modelcontextprotocol.io/docs/develop/build-server 、Python SDK「Connect to a real host」https://py.sdk.modelcontextprotocol.io/get-started/real-host/ 、「Client transports」https://py.sdk.modelcontextprotocol.io/client/transports/ 、Claude Code 文档「Connect Claude Code to tools via MCP」https://code.claude.com/docs/en/mcp (2026-10-25 核对)。

这一课做完你会得到什么

一个按 MCP 官网教程写成、能查美国天气预警和天气预报的 weather.py。你会先用 Python 模拟宿主自测,再把它接进 Claude Code,并学会 Claude Desktop、Cursor、VS Code 的配置写法。最后学会在服务器连不上时从哪三处查起。

官方教程用的是美国国家气象局(NWS)的免费接口,不需要 API Key,但只覆盖美国,查国内坐标会返回失败提示。本课照用官方示例,等你跑通流程,再把它换成自己的接口就行。

第 1 步:建项目

uv init weather
cd weather
uv venv
source .venv/bin/activate
uv add "mcp[cli]"
touch weather.py

Windows PowerShell 用 .venv\Scripts\activate 激活虚拟环境,用 new-item weather.py 新建文件。官方教程不另装 HTTP 库:示例用的 httpx2 是 SDK 本身的依赖,装 mcp 时已经一起装好了。

第 2 步:写 weather.py

下面的代码与官网教程完全一致,按顺序粘进 weather.py:

from typing import Any

import httpx2
from mcp.server import MCPServer

# Initialize MCPServer
mcp = MCPServer("weather")

# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"


async def make_nws_request(url: str) -> dict[str, Any] | None:
    """Make a request to the NWS API with proper error handling."""
    headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
    async with httpx2.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception:
            return None


def format_alert(feature: dict) -> str:
    """Format an alert feature into a readable string."""
    props = feature["properties"]
    return f"""
Event: {props.get("event", "Unknown")}
Area: {props.get("areaDesc", "Unknown")}
Severity: {props.get("severity", "Unknown")}
Description: {props.get("description", "No description available")}
Instructions: {props.get("instruction", "No specific instructions provided")}
"""

接着粘贴两个工具和入口:

@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get weather alerts for a US state.

    Args:
        state: Two-letter US state code (e.g. CA, NY)
    """
    url = f"{NWS_API_BASE}/alerts/active/area/{state}"
    data = await make_nws_request(url)

    if not data or "features" not in data:
        return "Unable to fetch alerts or no alerts found."

    if not data["features"]:
        return "No active alerts for this state."

    alerts = [format_alert(feature) for feature in data["features"]]
    return "\n---\n".join(alerts)


@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get weather forecast for a location.

    Args:
        latitude: Latitude of the location
        longitude: Longitude of the location
    """
    # First get the forecast grid endpoint
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)

    if not points_data:
        return "Unable to fetch forecast data for this location."

    # Get the forecast URL from the points response
    forecast_url = points_data["properties"]["forecast"]
    forecast_data = await make_nws_request(forecast_url)

    if not forecast_data:
        return "Unable to fetch detailed forecast."

    # Format the periods into a readable forecast
    periods = forecast_data["properties"]["periods"]
    forecasts = []
    for period in periods[:5]:  # Only show next 5 periods
        forecast = f"""
{period["name"]}:
Temperature: {period["temperature"]}°{period["temperatureUnit"]}
Wind: {period["windSpeed"]} {period["windDirection"]}
Forecast: {period["detailedForecast"]}
"""
        forecasts.append(forecast)

    return "\n---\n".join(forecasts)


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

模型会读到每个工具的 docstring,包括 Args: 里的参数说明,并据此决定什么时候调用、怎么传参。所以 docstring 要写给模型看,写清楚、写具体。

第 3 步:记住 stdio 服务器的头号规则

宿主通过 stdin/stdout 与 stdio 服务器交换 JSON-RPC 消息,stdout 就是通信线路。往 stdout 打印任何东西都可能把消息弄乱,导致宿主断开连接。需要输出调试信息时,用 logging,它默认写到 stderr:

import logging

logger = logging.getLogger(__name__)

# ❌ Bad (STDIO)
print("Processing request")

# ✅ Good (STDIO)
logger.info("Processing request")  # writes to stderr

SDK 在服务期间会把已刷新的 stdout 输出转到 stderr,但服务启动前的输出,以及缓冲到进程退出时才写出的 print(),仍然会落到通信线路上。最稳妥的做法还是一律用 logging。HTTP 传输的服务器不受这条限制。

第 4 步:用宿主同款启动命令自测

官方给所有宿主用的是同一条启动命令,必须写绝对路径:

uv run --with "mcp[cli]" mcp run /ABSOLUTE/PATH/TO/weather/weather.py

直接运行这条命令时,终端什么都不显示,也不会退出,这是正常的:stdio 服务器在等宿主先发消息。按 Ctrl-C 停止。如果看到报错堆栈,或者命令立即退出,那才是真问题。

再用 SDK 的 Client 扮演宿主,按同样的命令把服务器作为子进程启动,并真正调用一次工具。在 weather 文件夹里新建 host_check.py,把 SERVER 改成你的绝对路径(macOS/Linux 可以用 pwd 查看当前路径):

import anyio

from mcp import Client, StdioServerParameters

SERVER = "/ABSOLUTE/PATH/TO/weather/weather.py"

params = StdioServerParameters(
    command="uv",
    args=["run", "--with", "mcp[cli]", "mcp", "run", SERVER],
)


async def main() -> None:
    async with Client(params) as client:
        tools = await client.list_tools()
        print("宿主视角看到的工具:", [t.name for t in tools.tools])
        args = {"latitude": 38.5816, "longitude": -121.4944}
        r = await client.call_tool("get_forecast", args)
        print(r.content[0].text.split("---")[0].strip())  # 只看第一个时段


anyio.run(main)
uv run python host_check.py

对照结果(2026-10-25 实测,坐标是官方教程用的萨克拉门托;天气内容每天不同,格式应当一样):

宿主视角看到的工具: ['get_alerts', 'get_forecast']
Today:
Temperature: 71°F
Wind: 5 mph NW
Forecast: Sunny, with a high near 71. Northwest wind around 5 mph.

同时,终端会出现几行 INFO HTTP Request: GET https://api.weather.gov/... "HTTP/1.1 200 OK"。这些是子进程写到 stderr 的日志,正好印证了第 3 步:日志走 stderr,不会干扰通信。如果你的网络访问不了 api.weather.gov,工具会返回 Unable to fetch forecast data for this location.,但能列出工具,就说明 MCP 这一层已经通了。

注意:StdioServerParameters 启动的子进程不会继承你的全部环境变量,只保留 HOME、PATH、USER 等少数几项。服务器需要 API Key 时,要用 env={"KEY": "..."} 显式传入。宿主那边也一样。

第 5 步:接入 Claude Code

Claude Code 不需要编辑配置文件,用 claude 命令注册即可。-- 之后的内容就是启动命令,会原样交给服务器:

claude mcp add weather -- \
  uv run --with "mcp[cli]" mcp run /ABSOLUTE/PATH/TO/weather/weather.py
claude mcp list
claude mcp get weather

对照结果(依据 Claude Code 官方文档):add 成功时会打印一行 Added ...。claude mcp list 会在每个服务器后面显示健康状态,正常是 ✔ Connected,连不上是 ✘ Failed to connect,后面会附上失败原因。进入 Claude Code 会话后输入 /mcp,可以看到 weather 已连接,并列出 get_alerts、get_forecast 两个工具。然后直接提问试试:

What are the active weather alerts in Texas?

Claude Code 会请求调用 get_alerts,同意后就会根据返回内容作答。默认写入的是本地作用域(只对当前项目生效、只有你自己用)。想在所有项目里使用,加 --scope user;想和团队共享,加 --scope project,配置会写进项目根部的 .mcp.json。不用时可以执行 claude mcp remove weather 删除。

第 6 步:其他宿主的写法

所有宿主用的都是同一条启动命令,区别只在于写到哪里。

Claude Desktop:SDK 能自动帮你写配置。

uv run mcp install weather.py

它会把启动命令写进 claude_desktop_config.json,并自动加上三样东西:uv 的绝对路径、--frozen,以及与你本机一致的 mcp[cli]==版本号。配置文件位置:macOS 在 ~/Library/Application Support/Claude/claude_desktop_config.json,Windows 在 %APPDATA%\Claude\claude_desktop_config.json。如果提示 Claude app not found,说明 Claude Desktop 还没装,或者装了但从没打开过。写完配置后要彻底退出 Claude Desktop(只关窗口不算),再重新打开。

Cursor:在项目根部新建 .cursor/mcp.json:

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "run", "--with", "mcp[cli]", "mcp", "run",
        "/ABSOLUTE/PATH/TO/weather/weather.py"
      ]
    }
  }
}

保存后,Cursor 的 MCP 设置里会出现 weather,下面列出两个工具。

VS Code:在项目根部新建 .vscode/mcp.json。和 Cursor 相比只有两处不同:外层键是 servers,每一项要写 type。

{
  "servers": {
    "weather": {
      "type": "stdio",
      "command": "uv",
      "args": [
        "run", "--with", "mcp[cli]", "mcp", "run",
        "/ABSOLUTE/PATH/TO/weather/weather.py"
      ]
    }
  }
}

VS Code 需要 1.99 或更高版本,并登录 GitHub Copilot 扩展(Free 版也行),而且 Copilot Chat 要切到 Agent 模式才会调用工具。确认信任提示后,在命令面板运行 MCP: List Servers,能看到 weather 正在运行。

第 7 步:连不上时按这三处查

先单独运行第 4 步的启动命令,确认它能安静地等待、没有报错。之后的问题几乎都出在下面三处:

  1. 路径不是绝对路径。 宿主是从它自己的工作目录启动服务器的,写成 weather.py 就会找不到文件。宿主找不到 uv 时,也要把 uv 换成 which uv(Windows 用 where uv)查到的绝对路径。
  2. 宿主还在用旧配置。 宿主只在启动时读取配置,改完要重启宿主,Claude Desktop 需要彻底退出后再打开。
  3. 有东西写进了 stdout。 检查有没有启动前或导入时执行的 print(),或者把日志写到 stdout 的自定义 handler。

Claude Desktop 为每个服务器单独记录日志,mcp-server-weather.log 记录的就是你的服务器的 stderr。macOS 的日志在 ~/Library/Logs/Claude,Windows 在 %APPDATA%\Claude\logs。macOS 上可以这样实时查看:

tail -n 20 -F ~/Library/Logs/Claude/mcp*.log

自查清单

  • 单独运行启动命令时,终端安静等待、不报错
  • host_check.py 打印出两个工具名,并返回一段天气预报,或者在网络受限时返回 Unable 提示
  • claude mcp list 中 weather 显示 ✔ Connected
  • 能说出 Cursor 与 VS Code 的 mcp.json 有哪两处不同
  • 知道为什么 stdio 服务器里不能用 print()

本课资料

适用环境

  • macOS
  • Windows
  • Linux
  • 网页
  • iOS

官方文档

Claude Code ↗