MCP Server和Client实战:如何用Python快速搭建一个天气查询工具(附完整代码)

📅 发布时间:2026/7/3 22:53:49 👁️ 浏览次数:
MCP Server和Client实战:如何用Python快速搭建一个天气查询工具(附完整代码)
从零构建一个智能天气助手深入MCP架构的Python实践最近在和一些开发者朋友交流时发现大家对如何让大语言模型LLM真正“落地”到具体业务场景中特别感兴趣。我们经常遇到这样的困境模型本身能力很强但一涉及到实时数据查询、系统操作或者特定业务逻辑就显得有些“束手无策”。这其实不是模型的问题而是我们缺少一套标准化的桥梁让模型能够安全、可控地调用外部能力。这就是MCPModel Context Protocol要解决的核心问题。今天我不打算讲太多抽象的概念而是直接带大家动手用Python搭建一个完整的天气查询工具。通过这个具体的例子你会直观地理解MCP Server和Client各自扮演什么角色以及它们如何协同工作。整个过程就像搭积木一样清晰即使你对MCP完全陌生跟着做一遍也能掌握精髓。1. 理解MCP为什么我们需要这套协议在开始写代码之前我们得先搞清楚MCP到底解决了什么痛点。想象一下你正在使用一个强大的语言模型想让它帮你查一下明天北京的天气。模型本身并不知道实时的天气数据它需要调用一个外部的天气API。如果没有标准化的协议你可能需要写一段复杂的提示词告诉模型如何构造API请求在模型输出后手动解析它的回复提取出URL和参数自己写代码去调用天气API再把API返回的结果“喂”回给模型让它生成最终的回答这个过程不仅繁琐而且充满了不确定性——模型可能构造出错误的请求格式或者无法正确解析API的响应。更严重的是如果每个外部服务都需要这样定制化的对接系统的可维护性会变得极差。MCP的出现就是为了标准化模型与外部工具之间的通信。它定义了一套清晰的“游戏规则”Server服务端负责“能做什么”。它像一个功能超市明码标价地列出自己提供的所有工具比如“查询天气”、“读写文件”、“执行计算”并准备好具体的实现代码。Client客户端负责“要做什么”。它像一个聪明的助手理解用户或模型的意图然后去Server的“超市”里找到对应的工具按照标准格式下单最后把执行结果带回来。这套协议的核心价值在于解耦和标准化。工具提供者Server只需要关心功能的实现不用管谁来调用、怎么调用工具使用者Client只需要知道协议格式就能接入任何符合规范的Server极大地提升了灵活性和开发效率。提示你可以把MCP类比为计算机上的驱动程序。硬件厂商Server提供标准的驱动接口操作系统Client通过这个标准接口来使用硬件双方都不需要为对方做特殊适配。2. 环境准备与项目初始化好了理论部分点到为止我们直接进入实战环节。今天要构建的天气查询工具会包含一个MCP Server提供天气查询能力和一个MCP Client作为用户或LLM的交互界面。整个项目结构清晰适合作为你的第一个MCP项目模板。2.1 创建项目与安装依赖首先为项目创建一个独立的目录并初始化Python虚拟环境。这是保持依赖整洁的好习惯。# 创建项目目录并进入 mkdir mcp-weather-demo cd mcp-weather-demo # 创建虚拟环境这里使用venv你也可以用conda python -m venv venv # 激活虚拟环境 # 在Windows上 venv\Scripts\activate # 在macOS/Linux上 source venv/bin/activate接下来创建两个关键文件requirements.txt用于管理依赖server.py和client.py作为我们稍后要编写的主文件。touch requirements.txt server.py client.py现在编辑requirements.txt文件加入我们需要的库。除了MCP的核心库我们还需要requests来调用真实的天气API以及python-dotenv来管理敏感信息如API密钥。# requirements.txt mcp1.0.0 requests2.31.0 python-dotenv1.0.0 uvicorn[standard]0.24.0 # 一个轻量级的ASGI服务器用于运行我们的Server使用pip安装这些依赖pip install -r requirements.txt2.2 获取天气API密钥我们的Server需要调用一个真实的天气服务来获取数据。这里我以 OpenWeatherMap 为例它提供免费的API额度足够用于学习和测试。访问 OpenWeatherMap 官网并注册一个免费账户。登录后在控制面板找到你的API Keys。生成一个新的Key并复制下来。为了安全地使用这个Key我们不会把它硬编码在代码里。在项目根目录创建一个.env文件touch .env在.env文件中填入你的API密钥和城市信息这里以北京为例# .env OPENWEATHER_API_KEY你的_真实_API_密钥_在这里 DEFAULT_CITYBeijing注意务必把.env文件添加到你的.gitignore中避免将敏感信息提交到版本控制系统。3. 构建MCP Server定义天气查询工具MCP Server是整个架构的能力提供者。它的核心任务是声明自己有哪些工具并实现这些工具的具体逻辑。对于我们的天气查询Server它只需要提供一个工具叫做get_weather。3.1 Server的核心结构与工具定义打开server.py让我们从导入必要的库开始。# server.py import os import json from typing import Any import requests from dotenv import load_dotenv from mcp.server import Server from mcp.server.models import InitializationOptions import mcp.server.stdio # 加载环境变量 load_dotenv() # 从环境变量中读取配置 API_KEY os.getenv(OPENWEATHER_API_KEY) DEFAULT_CITY os.getenv(DEFAULT_CITY, London) # 设置默认城市 if not API_KEY: raise ValueError(请在 .env 文件中设置 OPENWEATHER_API_KEY)接下来我们初始化MCP Server实例。Server类是来自mcp库的核心类它负责处理协议通信、工具注册和请求分发。# 创建Server实例 app Server(weather-server)现在来到最关键的一步定义并注册工具。在MCP中一个工具Tool需要明确告诉Client三件事1我叫什么名字2我需要什么参数3我是干什么的描述。这通过一个结构化的字典来定义。# 定义天气查询工具 app.list_tools() async def handle_list_tools(): 列出本Server提供的所有工具。Client会调用此函数来发现可用的工具。 return [ { name: get_weather, description: 获取指定城市的当前天气信息包括温度、湿度、天气状况和风速。, inputSchema: { type: object, properties: { city: { type: string, description: 城市名称例如Beijing, Shanghai, New York。如果未提供将使用默认城市。, } }, required: [], # city 参数不是必须的有默认值 }, } ]我们来拆解一下这个工具定义name: get_weather这是工具的标识符Client调用时必须使用这个名字。description清晰描述工具的功能这有助于LLM理解在什么场景下使用这个工具。inputSchema定义了工具需要的输入参数。这里我们只定义了一个可选参数city类型是字符串。required: []表示该参数不是调用时必须提供的。3.2 实现工具的执行逻辑工具定义好了但光有“菜单”不行还得有“厨师”来做菜。我们需要实现当Client调用get_weather时Server具体要执行什么操作。app.call_tool() async def handle_call_tool(name: str, arguments: dict[str, Any]) - list[dict]: 执行具体的工具调用。 if name get_weather: # 从参数中获取城市名如果未提供则使用默认值 city arguments.get(city, DEFAULT_CITY) # 调用真实的天气API weather_data await fetch_weather_from_api(city) # 按照MCP协议返回结果 return [ { type: text, text: weather_data, } ] else: # 如果Client请求了一个不存在的工具返回错误信息 return [ { type: text, text: f未知的工具{name}, } ]上面的handle_call_tool函数是一个路由分发器。它根据Client传来的工具名name执行对应的代码块。对于get_weather它做了三件事提取参数城市名。调用一个辅助函数fetch_weather_from_api去获取真实数据。将结果包装成MCP协议要求的格式一个包含type和text的字典列表并返回。现在让我们实现那个真正与外部API交互的辅助函数。async def fetch_weather_from_api(city_name: str) - str: 调用OpenWeatherMap API获取天气数据并格式化为易读的字符串。 base_url http://api.openweathermap.org/data/2.5/weather params { q: city_name, appid: API_KEY, units: metric, # 使用摄氏度 lang: zh_cn # 返回中文描述 } try: response requests.get(base_url, paramsparams, timeout10) response.raise_for_status() # 如果HTTP请求失败抛出异常 data response.json() # 解析API返回的JSON数据 main data.get(main, {}) weather_list data.get(weather, [{}]) wind data.get(wind, {}) city data.get(name, 未知城市) temp main.get(temp, N/A) humidity main.get(humidity, N/A) description weather_list[0].get(description, 未知) if weather_list else 未知 wind_speed wind.get(speed, N/A) # 格式化成友好的文本 result_text f {city} 当前天气状况 ---------------------------- ️ 温度{temp}°C 湿度{humidity}% ️ 天气{description} 风速{wind_speed} m/s ---------------------------- 数据来源OpenWeatherMap .strip() return result_text except requests.exceptions.RequestException as e: return f请求天气API时出错{e} except (KeyError, json.JSONDecodeError) as e: return f解析天气数据时出错{e}这个函数是Server的“实干家”。它构造HTTP请求调用第三方天气服务处理可能的错误并将原始的JSON数据转换成对人类和LLM都友好的自然语言文本。这种格式化至关重要它直接决定了Client以及最终的LLM或用户接收到的信息质量。3.3 启动Server最后我们需要让Server运行起来监听来自Client的请求。MCP Server通常通过标准输入输出stdio或HTTP与Client通信。这里我们使用stdio方式因为它最简单适合本地开发和调试。# 主程序入口 if __name__ __main__: # 使用 stdio 方式运行 Server这是与Client通信的一种标准方式 mcp.server.stdio.run(app)至此一个功能完整的MCP Server就构建完成了。你可以通过运行python server.py来启动它但它现在只会等待输入因为我们还没有Client来连接它。Server的主要工作就是“待命”等待Client的指令。4. 构建MCP Client连接用户与服务的桥梁如果说Server是默默奉献的后台工作者那么Client就是活跃在前台的交互专家。它的核心职责是理解需求找到合适的工具完成调用并呈现结果。在我们的Demo中Client将作为一个简单的命令行程序模拟LLM或用户与Server的交互过程。4.1 Client的初始化与Server连接创建client.py文件开始构建Client。# client.py import asyncio import json from typing import Any from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client class WeatherClient: 一个简单的MCP Client用于与天气查询Server交互。 def __init__(self, server_script_path: str): 初始化Client。 参数: server_script_path: MCP Server的Python脚本路径。 self.server_script_path server_script_path self.session: ClientSession | None NoneClient需要知道如何启动并连接Server。我们使用StdioServerParameters来配置一个通过命令行启动的Server进程。async def connect(self): 启动Server进程并建立MCP会话连接。 # 配置如何启动Server进程通过运行python脚本 server_params StdioServerParameters( commandpython, args[self.server_script_path], ) # 创建stdio通信通道并启动Server进程 async with stdio_client(server_params) as (read_stream, write_stream): # 利用通信通道创建MCP会话 self.session ClientSession(read_stream, write_stream) # 初始化会话这是MCP协议要求的握手过程 await self.session.initialize( initialization_options{ protocolVersion: 1.0, capabilities: {}, # 声明Client支持的能力这里为空 } ) print(✅ 已成功连接至MCP Server。) # 连接建立后立即向Server询问它提供了哪些工具 await self.list_available_tools()connect方法完成了几个关键动作配置并启动server.py进程。建立标准输入输出的数据流管道。创建ClientSession对象这是与Server进行所有协议通信的入口。执行初始化握手。连接成功后立即列出Server提供的工具让用户知道可以做什么。4.2 发现工具与调用工具连接建立后Client首先要做的就是“探索”Server的能力。这通过调用MCP协议的tools/list方法实现。async def list_available_tools(self): 向Server请求可用的工具列表并显示。 if not self.session: print(❌ 未连接到Server。) return try: # 调用标准的 list_tools 方法 response await self.session.list_tools() tools response.tools print(\n️ Server提供的工具列表) print(- * 30) for tool in tools: print(f工具名称{tool.name}) print(f功能描述{tool.description}) # 打印输入参数要求 if tool.inputSchema and tool.inputSchema.get(properties): print(输入参数) for param_name, param_schema in tool.inputSchema[properties].items(): required (必填) if param_name in tool.inputSchema.get(required, []) else (可选) print(f - {param_name}: {param_schema.get(description, 无描述)} {required}) print(- * 30) except Exception as e: print(f获取工具列表时出错{e})这个方法不仅获取工具列表还将其格式化成易读的形式展示给用户包括工具名、描述和每个参数的说明。这模拟了LLM在决定使用哪个工具时需要的信息。当用户或LLM决定使用某个工具时Client需要构建一个符合MCP协议格式的请求并发送给Server。async def call_tool(self, tool_name: str, arguments: dict[str, Any]): 调用指定的工具并打印结果。 if not self.session: print(❌ 未连接到Server。) return print(f\n 正在调用工具 {tool_name}参数{arguments}) try: # 调用MCP协议的 call_tool 方法 response await self.session.call_tool(tool_name, arguments) # 处理并显示结果 if response.content: print(\n 工具执行结果) print( * 40) for content_item in response.content: # 目前我们只处理文本类型的结果 if content_item.type text: print(content_item.text) print( * 40) else: print(工具未返回任何内容。) except Exception as e: print(f调用工具时出错{e})call_tool方法是Client的核心。它接收工具名和参数字典通过session.call_tool()发送给Server并等待响应。收到响应后它解析并格式化输出结果。这个过程完全遵循MCP协议因此这个Client可以调用任何符合MCP规范的Server而不仅仅是我们的天气Server。4.3 实现交互式命令行界面为了让Demo更直观我们给Client添加一个简单的交互式循环允许用户手动输入命令来查询天气。async def interactive_loop(self): 运行一个简单的交互式命令行循环。 print(\n *50) print(欢迎使用MCP天气查询客户端) print(命令说明) print( - list: 查看可用工具) print( - weather [城市名]: 查询指定城市天气如 weather 北京) print( - exit: 退出程序) print(*50) while True: try: user_input input(\n请输入命令 ).strip() if user_input.lower() exit: print(再见) break elif user_input.lower() list: await self.list_available_tools() elif user_input.lower().startswith(weather): # 解析命令提取城市名 parts user_input.split( , 1) city parts[1] if len(parts) 1 else None # 构建调用参数 args {} if city: args[city] city await self.call_tool(get_weather, args) else: print(未知命令。请输入 list, weather [城市] 或 exit。) except KeyboardInterrupt: print(\n\n程序被中断。) break except Exception as e: print(f处理命令时发生错误{e})最后编写主函数来启动整个客户端。async def main(): # 创建Client实例指定Server脚本路径 client WeatherClient(server.py) try: # 连接Server await client.connect() # 启动交互式命令行 await client.interactive_loop() except Exception as e: print(f客户端运行失败{e}) finally: print(客户端已关闭。) if __name__ __main__: asyncio.run(main())5. 运行与测试见证MCP的协同效应代码已经完成现在是时候看到它们如何协同工作了。整个过程需要打开两个终端窗口。第一步启动MCP Server在第一个终端中进入项目目录并激活虚拟环境然后运行Server。cd /path/to/your/mcp-weather-demo source venv/bin/activate # Windows: venv\Scripts\activate python server.py你会看到Server启动并开始等待连接。它本身不会有任何输出直到Client发起请求。第二步启动MCP Client在第二个终端中同样进入项目目录并激活环境然后运行Client。cd /path/to/your/mcp-weather-demo source venv/bin/activate python client.py如果一切正常你将看到Client成功连接Server并打印出可用的工具列表目前只有get_weather。接着你会进入交互式命令行。第三步进行测试在Client的命令行中你可以尝试以下命令输入list再次确认工具信息。输入weather查询默认城市在.env中设置的DEFAULT_CITY比如Beijing的天气。输入weather Shanghai查询上海的天气。输入weather New York查询纽约的天气注意城市名要符合API的查询格式。对于每一次查询Client都会将请求发送给ServerServer调用真实的天气API获取数据并格式化最后将结果返回给Client显示。一个完整的、基于MCP协议的工具调用流程就在你眼前跑通了。5.1 深入理解通信过程为了让你更透彻地理解底层发生了什么我们来看一个简化的、模拟的协议通信序列。当你输入weather Shanghai时Client - Server (请求调用工具):{ jsonrpc: 2.0, id: 1, method: tools/call, params: { name: get_weather, arguments: { city: Shanghai } } }Server 内部处理:收到请求路由到handle_call_tool函数。执行fetch_weather_from_api(Shanghai)。向 OpenWeatherMap 发送HTTP GET请求。收到JSON响应解析并格式化成文本。Server - Client (返回结果):{ jsonrpc: 2.0, id: 1, result: { content: [ { type: text, text: 上海 当前天气状况\n----------------------------\n️ 温度22.5°C\n 湿度65%\n️ 天气多云\n 风速3.1 m/s\n----------------------------\n数据来源OpenWeatherMap } ] } }Client 处理结果:解析响应提取content[0].text。将格式化后的文本打印到终端。这个过程完美诠释了MCP的价值Client不需要知道天气API的URL、参数格式和认证方式Server也不需要知道Client是命令行程序、Web应用还是LLM。双方通过一个标准的协议对话各司其职高效协作。6. 扩展思路从Demo到生产我们构建的这个天气查询工具虽然简单但它完整展示了MCP的核心模式。基于这个模式你可以轻松地进行扩展构建出真正强大的AI应用。扩展Server的能力一个Server可以提供多个工具。你可以轻松地在handle_list_tools函数里返回更多工具定义并实现相应的handle_call_tool逻辑。get_forecast: 获取多天天气预报。search_city: 根据名称搜索城市ID解决同名城市问题。set_unit: 让用户切换温度单位摄氏度/华氏度。升级Client的智能目前的Client只是一个简单的命令行解析器。在一个真实的AI应用中Client的角色通常由LLM来扮演。LLM作为决策者用户说“我明天去上海出差需要带伞吗”。LLM作为智能Client会理解意图发现需要查询天气预报然后自动调用get_weather工具并根据返回的“天气状况”判断是否需要带伞最后生成自然语言回答。构建工具使用工作流LLM可以连续调用多个工具。例如先调用search_city确认“Springfield”的具体是哪个城市再调用get_forecast获取该城市未来三天的天气。部署与集成Server部署可以将server.py封装成一个HTTP服务使用FastAPI、Flask等这样任何能发送HTTP请求的Client都可以调用它而不仅限于stdio方式。与现有AI平台集成许多AI应用框架如LangChain、LlamaIndex已经开始支持MCP。这意味着你写的这个天气Server可以几乎零成本地接入这些生态被成千上万的LLM应用所使用。注意在生产环境中你需要考虑更多因素如Server的认证授权谁可以调用、限流、错误监控和日志记录。MCP协议本身是灵活的可以容纳这些企业级需求。通过这个从零开始的实战项目你应该已经感受到MCP并不是一个遥不可及的复杂框架而是一套非常务实的接口标准。它把“AI调用外部功能”这个难题分解成了“提供功能”和“使用功能”两个清晰的子问题让开发者可以专注于自己擅长的部分。下次当你需要让LLM操作数据库、发送邮件或者查询股票信息时不妨先想想是不是可以把它封装成一个MCP Server