【大模型实战篇】基于Claude MCP协议的智能体落地示例

1. 背景

        之前我们在《MCP(Model Context Protocol) 大模型智能体第一个开源标准协议》一文中,介绍了MCP的概念,虽然了解了其概念、架构、解决的问题,但还缺少具体的示例,来帮助进一步理解整套MCP框架如何落地。

        今天我们基于claude的官方例子--获取天气预报【1】,来理解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)安装所需的依赖包

# Create a new directory for our project
uv init weather
cd weather

# Create virtual environment and activate it
uv venv
source .venv/bin/activate

# Install dependencies
uv add "mcp[cli]" httpx

# Create our server file
touch server.py

        (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.list_tools() - 注册用于列出可用工具的处理器
#@server.call_tool() - 注册用于执行工具调用的处理器

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={},
                ),
            ),
        )

# This is needed if you'd like to connect to a custom client
if __name__ == "__main__":
    asyncio.run(main())

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

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

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

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={},
                ),
            ),
        )

# This is needed if you'd like to connect to a custom client
if __name__ == "__main__":
    asyncio.run(main())

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

        测试服务器与 Claude for Desktop。【2】也给出了构建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 发生变化时手动更新【3,4】。

        如下图所示:

        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

【2】For Client Developers - Model Context Protocol

【3】AI Agent框架综述

【4】MCP工作原理

<think>好的,用户想了解或下载Claude的桌面版应用。首先,我需要检查现有的引用内容是否有相关信息。根据引用[3],提到了MCP Hosts,其中包括Claude Desktop、IDE或AI工具,说明Claude确实有桌面版应用的存在。然而,引用中没有直接提供下载链接或详细的使用步骤。 接下来,我应该考虑用户可能的真实需求。他们可能希望获得安全可靠的下载渠道,以及基本的使用指南。同时,用户可能对Claude的功能和系统要求有疑问。需要提醒用户注意从官方渠道下载以避免安全风险,因为引用[1]提到C/C++程序可能导致系统问题,但这里可能不适用,不过安全意识仍需强调。 另外,引用[3]提到MCP协议和相关组件,如MCP Clients和Servers,可能需要解释Claude桌面应用如何与这些组件互动,比如访问本地资源或远程API。但用户的问题主要集中在下载和使用,所以应优先回答这些,再提及相关组件作为补充信息。 由于目前没有直接的下载链接,建议用户访问官方网站或开发者平台,并提供一般性指导步骤,如系统要求、安装流程和基本使用方法。同时,生成相关问题可以帮助用户进一步了解Claude的应用场景和配置方法,结合引用中的MCP结构进行扩展。</think>关于Claude桌面版应用的下载和使用,目前没有公开的官方下载渠道或详细文档说明。根据现有信息分析: 1. **产品定位**:Claude Desktop属于MCP Hosts类别中的本地化AI工具,通过与MCP协议客户端建立连接来访问本地资源(如数据库、文档)和远程API[^3]。 2. **获取建议**: - 建议访问Anthropic公司(Claude开发者)的官方网站查询最新动态 - 关注开发者社区论坛获取测试版信息 - 企业用户可通过合作伙伴渠道申请接入权限 3. **预期功能**: ```python # 伪代码示例MCP客户端连接 def connect_mcp_server(): client = MCPClient() client.register_local_resources(["documents", "system_services"]) client.connect("claude-desktop.mcp") ``` 4. **安全提示**: 根据系统安全经验[^1],建议: - 仅从可信来源获取安装包 - 验证数字签名 - 在沙箱环境中测试运行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

源泉的小广场

感谢大佬的支持和鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值