第 2 课
官方天气服务器:自测后接入 Claude Code、Claude Desktop、Cursor 与 VS Code
按 MCP 官网教程写出查询美国天气的 weather.py,记住 stdio 服务器不能往 stdout 打印,用宿主同款启动命令和 Python 客户端自测,再接入 Claude Code、Claude Desktop、Cursor、VS Code,并学会从三处排查连接失败。
课程目录第 2 / 3 课
学习位置仅保存在当前浏览器,有效期 180 天。
图文教材
本课依据: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 步的启动命令,确认它能安静地等待、没有报错。之后的问题几乎都出在下面三处:
- 路径不是绝对路径。 宿主是从它自己的工作目录启动服务器的,写成
weather.py就会找不到文件。宿主找不到uv时,也要把uv换成which uv(Windows 用where uv)查到的绝对路径。 - 宿主还在用旧配置。 宿主只在启动时读取配置,改完要重启宿主,Claude Desktop 需要彻底退出后再打开。
- 有东西写进了 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()
