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

【大模型实战篇】基于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)安装所需的依赖包

        (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工作原理

Read more

将现有 REST API 转换为 MCP Server工具 -higress

将现有 REST API 转换为 MCP Server工具 -higress

Higress 是一款云原生 API 网关,集成了流量网关、微服务网关、安全网关和 AI 网关的功能。 它基于 Istio 和 Envoy 开发,支持使用 Go/Rust/JS 等语言编写 Wasm 插件。 提供了数十个通用插件和开箱即用的控制台。 Higress AI 网关支持多种 AI 服务提供商,如 OpenAI、DeepSeek、通义千问等,并具备令牌限流、消费者鉴权、WAF 防护、语义缓存等功能。 MCP Server 插件配置 higress 功能说明 * mcp-server 插件基于 Model Context Protocol (MCP),专为 AI 助手设计,

By Ne0inhk
MCP 工具速成:npx vs. uvx 全流程安装指南

MCP 工具速成:npx vs. uvx 全流程安装指南

在现代 AI 开发中,Model Context Protocol(MCP)允许通过外部进程扩展模型能力,而 npx(Node.js 生态)和 uvx(Python 生态)则是两种即装即用的客户端工具,帮助你快速下载并运行 MCP 服务器或工具包,无需全局安装。本文将从原理和对比入手,提供面向 Windows、macOS、Linux 的详细安装、验证及使用示例,确保你能在本地或 CI/CD 流程中无缝集成 MCP 服务器。 1. 工具简介 1.1 npx(Node.js/npm) npx 是 npm CLI(≥v5.2.0)

By Ne0inhk
如何在Cursor中使用MCP服务

如何在Cursor中使用MCP服务

前言 随着AI编程助手的普及,越来越多开发者选择在Cursor等智能IDE中进行高效开发。Cursor不仅支持代码补全、智能搜索,还能通过MCP(Multi-Cloud Platform)服务,轻松调用如高德地图API、数据库等多种外部服务,实现数据采集、处理和自动化办公。 本文以“北京一日游自动化攻略”为例,详细讲解如何在 Cursor 中使用 MCP 服务,完成数据采集、数据库操作、文件生成和前端页面展示的全流程。 学习视频:cursor中使用MCP服务 一、什么是MCP服务? MCP(Multi-Cloud Platform)是Cursor内置的多云服务接口,支持调用地图、数据库、文件系统等多种API。通过MCP,开发者无需手动写HTTP请求或繁琐配置,只需在对话中描述需求,AI助手即可自动调用相关服务,极大提升开发效率。 二、环境准备 2.1 cursor Cursor重置机器码-解决Too many free trials. 2.

By Ne0inhk
解锁Dify与MySQL的深度融合:MCP魔法开启数据新旅程

解锁Dify与MySQL的深度融合:MCP魔法开启数据新旅程

文章目录 * 解锁Dify与MySQL的深度融合:MCP魔法开启数据新旅程 * 引言:技术融合的奇妙开篇 * 认识主角:Dify、MCP 与 MySQL * (一)Dify:大语言模型应用开发利器 * (二)MCP:连接的桥梁 * (三)MySQL:经典数据库 * 准备工作:搭建融合舞台 * (一)环境搭建 * (二)安装与配置 Dify * (三)安装与配置 MySQL * 关键步骤:Dify 与 MySQL 的牵手过程 * (一)安装必要插件 * (二)配置 MCP SSE * (三)创建 Dify 工作流 * (四)配置 Agent 策略 * (五)搭建MCP

By Ne0inhk