跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
PythonAI

基于 MCP 协议的智能体落地示例:天气预报工具实现

基于 MCP 协议构建智能体服务器的实践。通过 Python 开发一个提供天气警报和预报工具的服务端,并配置连接到 Claude for Desktop。文章详细展示了环境安装、工具注册、API 调用逻辑以及客户端交互流程,解决了大模型缺乏实时数据获取能力的问题,实现了智能体对工具的自动发现与使用。

墨染流年发布于 2026/2/9更新于 2026/7/2143 浏览
基于 MCP 协议的智能体落地示例:天气预报工具实现

1. 背景

在了解 MCP(Model Context Protocol)概念及架构后,需要具体示例帮助理解整套框架如何落地。

本文基于官方示例——获取天气预报,来理解 MCP 落地的整条链路。

2. MCP 示例

该案例是构建一个简单的 MCP 天气预报服务器,并将其连接到主机,即 Claude for Desktop。从基本设置开始,然后逐步发展到更复杂的使用场景。

大模型虽然能力非常强,但其弊端就是内容是过时的,这里的过时不是说内容很旧,只是表达内容具有非实时性。比如没有获取天气预报和严重天气警报的能力。因此我们将使用 MCP 来解决这一问题。

构建一个服务器,该服务器提供两个工具:获取警报(get-alerts)和获取预报(get-forecast)。然后,将该服务器连接到 MCP 主机(在本例中为 Claude for Desktop)。

首先我们配置下环境:

(1)安装 uv

curl -LsSf https://astral.sh/uv/install.sh | sh

安装完成后,会提示:

downloading uv 0.6.9 aarch64-apple-darwin no checksums to verify installing to /Users/nicolas/.local/bin uv uvx everything's installed!

(2)安装所需的依赖包

文章配图

(3)在 server.py 中构建相应的 get-alerts 和 get-forecast 工具

from typing import Any
import asyncio
import httpx
from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
import mcp.server.stdio

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

server = Server("weather")

@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    """ List available tools.
    Each tool specifies its arguments using JSON Schema validation.
    """
    return [
        types.Tool(
            name="get-alerts",
            description="Get weather alerts for a state",
            inputSchema={
                "type": "object",
                "properties": {
                    "state": {
                        "type": "string",
                        "description": "Two-letter state code (e.g. CA, NY)",
                    },
                },
                "required": ["state"],
            },
        ),
        types.Tool(
            name="get-forecast",
            description="Get weather forecast for a location",
            inputSchema={
                "type": "object",
                "properties": {
                    "latitude": {
                        "type": "number",
                        "description": "Latitude of the location",
                    },
                    "longitude": {
                        "type": "number",
                        "description": "Longitude of the location",
                    },
                },
                "required": ["latitude", "longitude"],
            },
        ),
    ]

async def make_nws_request(client: httpx.AsyncClient, 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"
    }
    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 concise string."""
    props = feature["properties"]
    return (
        f"Event: {props.get('event', 'Unknown')}\n"
        f"Area: {props.get('areaDesc', 'Unknown')}\n"
        f"Severity: {props.get('severity', 'Unknown')}\n"
        f"Status: {props.get('status', 'Unknown')}\n"
        f"Headline: {props.get('headline', 'No headline')}\n"
        "---"
    )

@server.call_tool()
async def handle_call_tool(
    name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
    """ Handle tool execution requests.
    Tools can fetch weather data and notify clients of changes.
    """
    if not arguments:
        raise ValueError("Missing arguments")
    
    if name == "get-alerts":
        state = arguments.get("state")
        if not state:
            raise ValueError("Missing state parameter")
        # Convert state to uppercase to ensure consistent format
        state = state.upper()
        if len(state) != 2:
            raise ValueError("State must be a two-letter code (e.g. CA, NY)")
        async with httpx.AsyncClient() as client:
            alerts_url = f"{NWS_API_BASE}/alerts?area={state}"
            alerts_data = await make_nws_request(client, alerts_url)
            if not alerts_data:
                return [types.TextContent(type="text", text="Failed to retrieve alerts data")]
            features = alerts_data.get("features", [])
            if not features:
                return [types.TextContent(type="text", text=f"No active alerts for {state}")]
            # Format each alert into a concise string
            formatted_alerts = [format_alert(feature) for feature in features[:20]] # only take the first 20 alerts
            alerts_text = f"Active alerts for {state}:\n\n" + "\n".join(formatted_alerts)
            return [
                types.TextContent(
                    type="text",
                    text=alerts_text
                )
            ]
    elif name == "get-forecast":
        try:
            latitude = float(arguments.get("latitude"))
            longitude = float(arguments.get("longitude"))
        except (TypeError, ValueError):
            return [types.TextContent(
                type="text",
                text="Invalid coordinates. Please provide valid numbers for latitude and longitude."
            )]
        # Basic coordinate validation
        if not (-90 <= latitude <= 90) or not (-180 <= longitude <= 180):
            return [types.TextContent(
                type="text",
                text="Invalid coordinates. Latitude must be between -90 and 90, longitude between -180 and 180."
            )]
        async with httpx.AsyncClient() as client:
            # First get the grid point
            lat_str = f"{latitude}"
            lon_str = f"{longitude}"
            points_url = f"{NWS_API_BASE}/points/{lat_str},{lon_str}"
            points_data = await make_nws_request(client, points_url)
            if not points_data:
                return [types.TextContent(type="text", text=f"Failed to retrieve grid point data for coordinates: {latitude}, {longitude}. This location may not be supported by the NWS API (only US locations are supported).")]
            # Extract forecast URL from the response
            properties = points_data.get("properties", {})
            forecast_url = properties.get("forecast")
            if not forecast_url:
                return [types.TextContent(type="text", text="Failed to get forecast URL from grid point data")]
            # Get the forecast
            forecast_data = await make_nws_request(client, forecast_url)
            if not forecast_data:
                return [types.TextContent(type="text", text="Failed to retrieve forecast data")]
            # Format the forecast periods
            periods = forecast_data.get("properties", {}).get("periods", [])
            if not periods:
                return [types.TextContent(type="text", text="No forecast periods available")]
            # Format each period into a concise string
            formatted_forecast = []
            for period in periods:
                forecast_text = (
                    f"{period.get('name', 'Unknown')}:\n"
                    f"Temperature: {period.get('temperature', 'Unknown')}°{period.get('temperatureUnit', 'F')}\n"
                    f"Wind: {period.get('windSpeed', 'Unknown')} {period.get('windDirection', '')}\n"
                    f"{period.get('shortForecast', 'No forecast available')}\n"
                    "---"
                )
                formatted_forecast.append(forecast_text)
            forecast_text = f"Forecast for {latitude}, {longitude}:\n\n" + "\n".join(formatted_forecast)
            return [types.TextContent(
                type="text",
                text=forecast_text
            )]
    else:
        raise ValueError(f"Unknown tool: {name}")

async def main():
    # Run the server using stdin/stdout streams
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream, write_stream, InitializationOptions(
                server_name="weather",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            )
        )

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

这段代码中,最核心的其实就是 @server.list_tools() 以及 @server.call_tools() 这两个注解。

@server.list_tools() - 注册用于列出可用工具的处理器 @server.call_tools() - 注册用于执行工具调用的处理器

调用函数的逻辑也比较简单,匹配到对应的工具名称,然后抽取对应的输入参数,然后发起 api 的请求,对获得的结果进行处理:

文章配图

(4)服务端与客户端交互

测试服务器与 Claude for Desktop。也给出了构建 MCP 客户端的教程。其中核心的逻辑如下:

async def process_query(self, query: str) -> str:
    """Process a query using Claude and available tools"""
    messages = [
        { "role": "user", "content": query }
    ]
    response = await self.session.list_tools()
    available_tools = [{
        "name": tool.name,
        "description": tool.description,
        "input_schema": tool.inputSchema
    } for tool in response.tools]
    # Initial Claude API call
    response = self.anthropic.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1000,
        messages=messages,
        tools=available_tools
    )
    # Process response and handle tool calls
    final_text = []
    assistant_message_content = []
    for content in response.content:
        if content.type == 'text':
            final_text.append(content.text)
            assistant_message_content.append(content)
        elif content.type == 'tool_use':
            tool_name = content.name
            tool_args = content.input
            # Execute tool call
            result = await self.session.call_tool(tool_name, tool_args)
            final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
            assistant_message_content.append(content)
            messages.append({
                "role": "assistant",
                "content": assistant_message_content
            })
            messages.append({
                "role": "user",
                "content": [
                    { "type": "tool_result", "tool_use_id": content.id, "content": result.content }
                ]
            })
    # Get next response from Claude
    response = self.anthropic.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1000,
        messages=messages,
        tools=available_tools
    )
    final_text.append(response.content[0].text)
    return "\n".join(final_text)

启动客户端,需要打开 Claude for Desktop 应用配置文件:

~/Library/Application Support/Claude/claude_desktop_config.json

如果该文件不存在,确保先创建出来,然后配置以下信息,以示例说明,我们 uv init 的是 weather,所以这里 mcpServers 配置 weather 的服务,args 中的路径设置为你 weather 的绝对路径。

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "--directory",
        "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
        "run",
        "server.py"
      ]
    }
  }
}

保存文件,并重新启动 Claude for Desktop。可以看到 Claude for Desktop 能够识别在天气服务器中暴露的两个工具。

文章配图

文章配图

然后在客户端询问天气,会提示调用 get-forecast 的 tool:

文章配图

3. MCP 到底解决了什么问题

工具是智能体框架的重要组成部分,允许大模型与外界互动并扩展其能力。即使没有 MCP 协议,也是可以实现 LLM 智能体,只不过存在几个弊端,当有许多不同的 API 时,启用工具使用变得很麻烦,因为任何工具都需要:手动构建 prompt,每当其 API 发生变化时手动更新。

如下图所示:

文章配图

MCP 其实解决了当存在大量工具时,能够自动发现,并自动构建 prompt。

整体流程示例:

(1)以总结 git 项目最近 5 次提交为例,MCP 主机(与客户端一起)将首先调用 MCP 服务器,询问有哪些工具可用。

MCP 主机:像 Claude Desktop、IDE 或其他 AI 工具等程序,希望通过 MCP 访问数据。MCP 客户端:与服务器保持 1:1 连接 的协议客户端。

文章配图

(2)MPC 客户端接收到所列出的可用工具后,发给 LLM,LLM 收到信息后,可能会选择使用某个工具。它通过主机向 MCP 服务器发送请求,然后接收结果,包括所使用的工具。

文章配图

(3)LLM 收到工具处理结果(包括原始的 query 等信息),之后就可以向用户输出最终的答案。

文章配图

总结起来,就一句话,MCP 协议其实是让智能体更容易管理、发现、使用工具。

4. 参考材料

  1. For Server Developers - Model Context Protocol: https://modelcontextprotocol.io/quickstart/server
  2. For Client Developers - Model Context Protocol: https://modelcontextprotocol.io/quickstart/client

目录

  1. 1. 背景
  2. 2. MCP 示例
  3. (1)安装 uv
  4. (2)安装所需的依赖包
  5. (3)在 server.py 中构建相应的 get-alerts 和 get-forecast 工具
  6. (4)服务端与客户端交互
  7. 3. MCP 到底解决了什么问题
  8. (1)以总结 git 项目最近 5 次提交为例,MCP 主机(与客户端一起)将首先调用 MCP 服务器,询问有哪些工具可用。
  9. (2)MPC 客户端接收到所列出的可用工具后,发给 LLM,LLM 收到信息后,可能会选择使用某个工具。它通过主机向 MCP 服务器发送请求,然后接收结果,包括所使用的工具。
  10. (3)LLM 收到工具处理结果(包括原始的 query 等信息),之后就可以向用户输出最终的答案。
  11. 4. 参考材料
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Star-Office-UI:用像素办公室可视化AI工作状态
  • Visual Studio C++ 项目添加现有项避坑指南
  • 环形链表、数组交集与随机链表复制解析
  • Llama.cpp 跨平台部署本地大模型实战指南
  • 本地部署 AI 服务零公网 IP 安全访问方案
  • OpenClaw 爆火启示录:低代码不是终点,而是走向「意图驱动」的企业级开发新范式
  • 基于 LangChain 实现数据库问答机器人
  • Matomo 埋点实战:运营分析与数据结构设计
  • 前端如何调用后端接口:HTML+JS 与 Vue 实践
  • LeetCode 202 快乐数:快慢指针解法详解
  • Java Web 开发环境搭建:IDEA 与 Tomcat 配置实战
  • Langchain-Chatchat 基于 LLM 构建本地智能知识库
  • 基于深度学习的无人机航拍小目标检测算法研究
  • 用 Nginx 部署 Vue 项目全过程
  • SkyWalking 全链路监控实战:Spring Cloud、Dubbo、RocketMQ 集成指南
  • 顺序表实战:查找及任意位置增删操作
  • MySQL 表基础语法:增删查与函数核心技巧
  • 学术论文 AIGC 检测风险与智能降重工具实战解析
  • 前缀和算法进阶:拆分、同余与取模处理
  • CoPaw:阿里云通义个人智能体工作台

相关免费在线工具

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online