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

基于 MCP 协议的 Claude 智能体天气服务落地示例

综述由AI生成MCP 协议解决了大模型工具调用繁琐及非实时性问题。通过构建 MCP 服务器暴露天气查询工具,连接 Claude for Desktop 客户端,实现自动发现与调用。示例展示了使用 Python 编写 weather 服务器,配置 uv 环境,注册 list_tools 和 call_tool 处理器,并在客户端完成工具交互的全链路流程。

橘子海发布于 2026/2/4更新于 2026/6/26.5K 浏览
基于 MCP 协议的 Claude 智能体天气服务落地示例

1. 背景

在之前的介绍中,我们了解了 MCP(Model Context Protocol)的概念、架构及解决的问题,但缺少具体的示例来帮助进一步理解整套 MCP 框架如何落地。

今天我们基于 Claude 的官方例子——获取天气预报,来理解 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 连接 的协议客户端。

流程 1

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

流程 2

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

流程 3

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

4. 参考材料

  1. For Server Developers - Model Context Protocol
  2. For Client Developers - Model Context Protocol
  3. AI Agent 框架综述
  4. MCP 工作原理

目录

  1. 1. 背景
  2. 2. MCP 示例
  3. 3. MCP 到底解决了什么问题
  4. 4. 参考材料
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 网络安全基础入门与学习路径指南
  • 几款免费 AI 生成内容检测工具及降重方法指南
  • Python 代码检查工具 Ruff 使用指南
  • 二叉树前中后序遍历详解:递归与迭代实现
  • 基于 Astro 和 Claude Code 为 ShareLatex-CE 构建 GitHub Pages 落地页
  • 基于 TensorFlow 和 ResNet50 的宠物识别系统设计与实现
  • RabbitMQ 消息确认机制详解:自动与手动模式
  • 掌握 Python 可从事的七大技术岗位及发展建议
  • AI 前沿动态:自进化代理、云端开发环境与多模态模型更新
  • Python 开发 Node.js 项目结构生成工具详解
  • SRC 漏洞挖掘入门指南:流程、技能与实战建议
  • 基于中文金融知识的 LLaMA 系微调模型智能问答系统
  • MCP 插件配置实战:browser-tools-mcp 使用指南
  • IDEA Maven 运行出现 JAVA_TOOL_OPTIONS 编码提示的解决方法
  • 8 个高效 Python 数据分析技巧
  • C 语言 Web 开发:CGI、FastCGI、Nginx 深度解析
  • C++ 面试核心考点:内存管理、编译机制与类型系统
  • C++ 核心基础特性详解:重载、引用、内联、auto 与 nullptr
  • pg_lake 核心功能:Parquet/CSV/JSON 文件查询与导入技巧
  • AionUi:首个开源运行时生成式 UI 框架,意图驱动界面渲染

相关免费在线工具

  • 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