用30行Python给AI装上"手和脚",从本地stdio到远程HTTP完整实战
一、AI连天气都查不了?
你问Claude:今天北京天气怎么样?
它大概率回你:我无法获取实时天气数据。
你再问:明天上海有暴雨预警吗?
它还是那句:我没有联网查询能力。
这种对话我经历过太多次。每次都想摔键盘——你明明是个AI,号称什么都会,结果连查个天气都做不到?
问题不在AI不够聪明。
Claude能写诗、能翻译、能写代码。但它有三件事天生做不到:查实时天气、读你的数据库、调公司内部API。
原因很简单。大模型的训练数据有截止日期,而且模型本身不会主动发起网络请求。
这就像你请了一个智商180的助理,但他被关在没窗户的房间里。你问他外面下没下雨,他只能猜。
2026年,解决这个问题有一个标准答案:MCP协议。
MCP全称Model Context Protocol,Anthropic 2024年发布的开放标准。到今天,500多个开源MCP Server已上线,Claude Desktop、Cursor、Cline全面支持。
它的定位很简单——给AI装上"手和脚",让它能调工具、查接口、读数据。
本文的目标也很直接:跟着我,用Python写一个天气查询MCP Server。
从本地stdio到远程HTTP部署,每一步都有完整代码。写完之后,你的AI就能真正"动手干活"了。
二、AI的"残疾"现状
大模型很强,但有三件事它天生做不到。
第一件事:查实时数据。
你问GPT-4"今天北京气温多少",它要么编一个数字,要么告诉你"我的知识截止到某年某月"。不是它不想帮你,是它根本没有联网查询的能力。
第二件事:读你的私有数据。
你的公司数据库、你的本地Excel、你的内部OA系统——这些对AI来说都是黑盒。它看不见,也摸不着。
第三件事:执行操作。
你想让AI帮你"把上周销售数据导出成CSV,发到钉钉群里"。这件事涉及查数据库、生成文件、调用钉钉API三个步骤。没有外部工具支持,AI只能给你写一段代码,剩下的你自己跑。
三、之前的"土办法"为什么不行
行业早就意识到这个问题,也试过一些方案。
方案一:Function Calling。
OpenAI、Claude、Google各自实现了函数调用,但格式不统一。你给Claude写的一套工具,搬到GPT那边要重写一遍。维护成本爆炸。
方案二:API直接调。
你自己写个中间层,把AI的输出解析成API调用。问题是AI不知道你的API有什么功能、该传什么参数。你得自己写编排逻辑,AI只是"建议",真正执行的还是你。
这两个方案的共同点:AI和工具之间没有标准接口,每次对接都要重新造轮子。
四、MCP是AI世界的USB-C
MCP的设计思路很简洁:给AI世界造一个通用接口。
|
对比维度
|
传统HTTP API
|
Function Calling
|
MCP
|
| — | — | — | — |
|
通信方向
|
单向
|
单向
|
双向
|
|
工具发现
|
手动查文档
|
手动定义
|
自动发现
|
|
调用方式
|
固定路径
|
各平台格式不同
|
统一标准
|
|
跨平台
|
需重新适配
|
需重新适配
|
一次开发,到处运行
|
|
状态管理
|
无状态
|
无状态
|
支持会话上下文
|
MCP的核心价值就一句话:你的API按MCP标准封装一次,Claude、Cursor、Cline都能自动发现、自动调用。
AI根据对话上下文,自己决定调用哪个工具、传什么参数——不需要你写任何编排代码。
这就是MCP被称为"AI世界USB-C"的原因。
五、环境准备:5分钟搞定
你需要三样东西:Python 3.10+、uv包管理器、一颗不怕报错的心。
检查Python版本:
python --version
# 预期输出:Python 3.
10.x
或更高
安装uv(比pip快10倍的包管理器):
# macOS / Linux
curl -LsSf
https://astral.sh/uv/install.sh
| sh
# Windows
powershell -ExecutionPolicy ByPass -c "irm
https://astral.sh/uv/install.ps1
| iex"
装完重启终端,确保uv命令能被识别。
创建项目:
uv init weather
cd weather
# 创建虚拟环境
uv venv
source .venv/bin/activate # macOS/Linux
# .venv\Scripts\activate # Windows
# 安装依赖
uv add "mcp[cli]" httpx
# 创建服务器文件
touch
weather.py
常见坑:fastmcp和mcp包冲突
如果你之前装过官方的mcp包,可能会和fastmcp冲突。两个包都有FastMCP类,但版本不同。
# 检查是否冲突
pip list | grep -i mcp
# 如果同时有mcp和fastmcp,卸载mcp
pip uninstall mcp
FastMCP 1.0已合并进官方SDK,但fastmcp包是独立维护的 v3.x版本,功能更全。本文统一用 fastmcp。
六、写你的第一个MCP Server:15分钟
打开 [weather.py](http://weather.py) ,把下面这段代码贴进去:
from typing import Any
import httpx
from
mcp.server.fastmcp
import FastMCP
# 初始化FastMCP服务器
mcp = FastMCP("weather")
# 常量
NWS_API_BASE = "
https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
async def make_nws_request(url: str) -> dict[str, Any] | None:
"""向NWS API发出GET请求,处理错误并返回JSON响应"""
headers = {
"User-Agent": USER_AGENT,
"Accept": "application/geo+json"
}
async with
httpx.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:
"""将警报特征格式化为可读字符串"""
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:
"""获取指定州的天气警报(使用两字母州代码如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 "无法获取警报或未找到警报。"
if not data["features"]:
return "该州没有活动警报。"
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:
"""获取位置的天气预报。
Args:
latitude: 位置的纬度
longitude: 位置的经度
"""
points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
points_data = await make_nws_request(points_url)
if not points_data:
return "无法为此位置获取预报数据。"
forecast_url = points_data["properties"]["forecast"]
forecast_data = await make_nws_request(forecast_url)
if not forecast_data:
return "无法获取详细预报。"
periods = forecast_data["properties"]["periods"]
forecasts = []
for period in periods[:5]:
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')
这段代码的核心逻辑:
-
mcp = FastMCP("weather"):创建MCP服务器实例,名字叫"weather"。
-
@ [mcp.tool()](http://mcp.tool\(\)):用装饰器把Python函数注册为MCP工具。
-
docstring:函数的文档字符串自动变成工具描述,告诉AI这个工具是干什么的、什么时候该用。
-
transport='stdio':使用标准输入输出传输,适合本地部署。 一个关键设计: 你不需要手写任何JSON Schema。FastMCP自动从Python的类型注解和docstring中提取参数定义。
latitude: float自动变成JSON Schema里的{"type": "number"}。docstring里的Args部分自动变成参数描述。 代码即协议。写Python代码就是在写MCP协议定义。 运行一下确认没问题:
uv run
weather.py
终端没有报错,说明Server能正常启动。
七、用MCP Inspector测试:5分钟
接入Claude Desktop之前,先用MCP Inspector测试。这个工具让你不需要配置Claude就能验证Server是否正常工作。
启动Inspector:
fastmcp dev inspector
weather.py
浏览器自动打开http://localhost:6274。
测试Tools:
-
点击Tools标签页
-
你应该看到两个工具:
get_alerts和get_forecast -
点击
get_forecast,填入参数:
-
- 39.9042(北京纬度)
latitude -
- 116.4074(北京经度)
longitude
- 点击Run Tool 返回了天气预报数据?恭喜你,Server工作正常。 为什么建议先测再接Claude? Claude Desktop的报错信息很模糊。配置有问题,你根本不知道错在哪。Inspector界面直观,参数、返回值一目了然,排错效率高10倍。
八、接入Claude Desktop:5分钟
找到配置文件:
|
操作系统
|
配置文件路径
|
| — | — |
|
macOS
| ~/Library/Application Support/Claude/ [claude_desktop_config.json](http://claude_desktop_config.json) |
|
Windows
| %APPDATA%\Claude\ [claude_desktop_config.json](http://claude_desktop_config.json) |
|
Linux
| ~/.config/Claude/ [claude_desktop_config.json](http://claude_desktop_config.json) |
文件不存在就创建一个空的JSON文件。
添加Server配置:
{
"mcpServers": {
"weather": {
"command": "uv",
"args": [
"--directory",
"/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
"run",
"
weather.py"
]
}
}
}
三个注意事项:
-
必须用绝对路径
。Claude Desktop启动Server时的工作目录不确定,相对路径会找不到文件。
-
Windows路径用双反斜杠
\\或正斜杠
/。 -
JSON格式不能有多余逗号
,最后一个字段后面不要加逗号。 重启Claude Desktop: 完全退出(不是最小化),重新打开。 验证: 输入框旁边应该出现一个锤子图标🔧。点击它,确认能看到
get_alerts和get_forecast两个工具。 实际对话测试: 试试这样跟Claude对话:
帮我查一下北京(39.9, 116.4)未来几天的天气预报
Claude自动调用get_forecast工具,传入经纬度参数,然后把返回的天气数据整理成易读格式呈现给你。
加州CA最近有天气预警吗?
Claude自动调用get_alerts工具,查询加州的天气警报。
整个过程中,你不需要告诉Claude该调哪个工具、传什么参数——它自己根据你的问题内容决定。
九、为什么需要远程部署
stdio传输有个致命限制:Server和Client必须在同一台机器上。
Claude Desktop把MCP Server当作子进程启动,通过操作系统的stdin/stdout管道通信。这就像你在终端里python [script.py](http://script.py) ,不需要网络,但也出不了本机。
实际工作中,远程部署是刚需:
-
团队共享
:一个MCP Server部署在公司服务器上,全团队都能用
-
资源隔离
:数据库在公司内网,AI在员工个人电脑,两者不能装在一起
-
服务化
:把MCP Server当作独立微服务运维,支持负载均衡、监控告警 架构对比: architecturediagram.jpg" alt=“MCP架构对比:stdio本地模式 vs HTTP远程模式” style=“max-width:100%;height:auto;display:block;margin:12px 0;border-radius:4px;”>
左图:stdio模式下,MCP Server作为Claude Desktop的子进程运行,通过stdin/stdout管道通信,仅限本机。 右图:HTTP模式下,MCP Server部署在远程服务器上,Claude Desktop通过网络(HTTP POST + SSE推送)与之通信,支持跨主机部署。
十、改造为HTTP + SSE传输
HTTP + SSE是MCP支持的远程传输方式之一。Server用SSE向Client推送消息,Client用HTTP POST向Server发送请求。
第一步:安装额外依赖
uv add fastapi starlette uvicorn
第二步:改造 weather.py
把stdio模式改成SSE模式:
from typing import Any
import httpx
from
mcp.server.fastmcp
import FastMCP
from
mcp.server.sse
import SseServerTransport
from
starlette.applications
import Starlette
from
starlette.routing
import Route, Mount
from
starlette.requests
import Request
import uvicorn
# 初始化FastMCP服务器
mcp = FastMCP("weather")
# 常量
NWS_API_BASE = "
https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
async def make_nws_request(url: str) -> dict[str, Any] | None:
"""向NWS API发出GET请求,处理错误并返回JSON响应"""
headers = {
"User-Agent": USER_AGENT,
"Accept": "application/geo+json"
}
async with
httpx.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:
"""将警报特征格式化为可读字符串"""
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:
"""获取指定州的天气警报(使用两字母州代码如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 "无法获取警报或未找到警报。"
if not data["features"]:
return "该州没有活动警报。"
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:
"""获取位置的天气预报。
Args:
latitude: 位置的纬度
longitude: 位置的经度
"""
points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
points_data = await make_nws_request(points_url)
if not points_data:
return "无法为此位置获取预报数据。"
forecast_url = points_data["properties"]["forecast"]
forecast_data = await make_nws_request(forecast_url)
if not forecast_data:
return "无法获取详细预报。"
periods = forecast_data["properties"]["periods"]
forecasts = []
for period in periods[:5]:
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)
# SSE传输配置
sse = SseServerTransport("/messages/")
async def handle_sse(request: Request):
"""处理SSE连接"""
async with
sse.connect_sse(request.scope,
request.receive,
request._send)
as (read_stream, write_stream):
await
mcp.run(read_stream,
write_stream,
mcp.create_initialization_options())
# 创建Starlette应用
app = Starlette(
routes=[
Route("/sse", handle_sse),
Mount("/messages", app=
sse.handle_post_message),
]
)
if __name__ == "__main__":
uvicorn.run(app,
host="0.0.0.0", port=8000)
关键变化:
-
去掉了
[mcp.run(transport='stdio')](http://mcp.run\(transport='stdio'\)) -
增加了
SseServerTransport,负责SSE消息传输 -
用Starlette创建HTTP服务,暴露两个端点:
-
/sse:SSE连接端点,Server向Client推送消息
-
/messages:HTTP POST端点,Client向Server发送请求
-
用uvicorn启动服务,监听
0.0.0.0:8000启动Server:
uv run
weather.py
终端显示:Uvicorn running on http://0.0.0.0:8000
配置Claude Desktop连接远程Server:
{
"mcpServers": {
"weather-remote": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/sse-client",
"http://your-server-ip:8000/sse"
]
}
}
}
把your-server-ip换成你的服务器IP地址。
重启Claude Desktop,测试对话。功能跟本地stdio完全一样,但Server跑在远程机器上。
十一、生产环境三件事
远程部署比本地stdio复杂,有三件事必须做。
第一件事:认证。
HTTP传输必须额外实现OAuth 2.1认证。没有认证的MCP Server暴露在网络上,任何人都能调用你的工具。
# 简化示例:在Starlette中添加Bearer Token验证
from
starlette.middleware
import Middleware
from
starlette.middleware.authentication
import AuthenticationMiddleware
# 实际生产环境建议用成熟的OAuth 2.1方案
第二件事:HTTPS。
生产环境必须用HTTPS,不能用明文HTTP。建议用Nginx做反向代理,自动处理SSL证书。
# Nginx配置示例
server {
listen 443 ssl;
server_name
mcp.yourcompany.com;
ssl_certificate /path/to/
cert.pem;
ssl_certificate_key /path/to/
key.pem;
location / {
proxy_pass http://localhost:8000;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
第三件事:防火墙。
确保服务器防火墙放行8000端口(或你配置的端口)。云服务器还要在安全组里加规则。
# Ubuntu/Debian
sudo ufw allow 8000
# CentOS
sudo firewall-cmd --permanent --add-port=8000/tcp
sudo firewall-cmd --reload
十二、传输方式怎么选
|
维度
|
stdio
|
HTTP + SSE
|
Streamable HTTP
|
| — | — | — | — |
|
适用场景
|
本地开发、桌面应用
|
远程部署、浏览器前端
|
生产环境、云原生
|
|
跨主机
|
否
|
是
|
是
|
|
需要IP/端口
|
不需要
|
需要
|
需要
|
|
配置复杂度
|
极低
|
中等
|
中等
|
|
安全要求
|
无(本机通信)
|
OAuth 2.1 + HTTPS
|
OAuth 2.1 + HTTPS
|
|
性能
|
最高(进程间通信)
|
中等
|
中等
|
|
扩展性
|
差(单机)
|
好
|
最好(无状态)
|
我的建议:
-
个人学习、本地开发:用stdio,零配置,5分钟跑通
-
团队共享、内网部署:用HTTP + SSE,一台服务器供多人使用
-
大规模生产环境:用Streamable HTTP,支持负载均衡、无状态扩展
十三、写在最后
MCP不是又一个技术概念。
它是AI从"能聊天"到"能干活"的桥梁。当你把第一个MCP Server跑通之后,你会发现:给AI装"手和脚"这件事,没有想象中难。
本地stdio跑通是起点。远程HTTP部署才是生产落地的关键。
下一步,你可以试着把公司的内部API封装成MCP Server,让AI帮你查数据、填表单、跑流程。那个"智商180但被关在房间里"的助理,终于可以走出去了。
2424

被折叠的 条评论
为什么被折叠?



