【大模型实战篇】基于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

Qt步进电机上位机控制程序源代码:跨平台C/C++编写,支持多种端口类型与详细注释

Qt步进电机上位机控制程序源代码:跨平台C/C++编写,支持多种端口类型与详细注释

Qt步进电机上位机控制程序源代码Qt跨平台C/C++语言编写 支持串口Tcp网口Udp网络三种端口类型 提供,提供详细注释和人工讲解 1.功能介绍: 可控制步进电机的上位机程序源代码,基于Qt库,采用C/C++语言编写。 支持串口、Tcp网口、Udp网络三种端口类型,带有调试显示窗口,接收数据可实时显示。 带有配置自动保存功能,用户的配置数据会自动存储,带有超时提醒功能,如果不回复则弹框提示。 其中三个端口,采用了类的继承与派生方式编写,对外统一接口,实现多态功能,具备较强的移植性。 2.环境说明: 开发环境是Qt5.10.1,使用Qt自带的QSerialPort,使用网络的Socket编程。 源代码中包含详细注释,使用说明,设计文档等。 请将源码放到纯英文路径下再编译。 3.使用介绍: 可直接运行在可执行程序里的exe文件,操作并了解软件运行流程。 本代码产品特点: 1、尽量贴合实际应用,细节考虑周到。 2、注释完善,讲解详细,还有相关扩展知识点介绍。

By Ne0inhk
Windows下MATLAB与C/C++混合编程:DLL生成与调用实战

Windows下MATLAB与C/C++混合编程:DLL生成与调用实战

Windows下MATLAB与C/C++混合编程:DLL生成与调用实战 在科学计算与工程开发中,MATLAB凭借其便捷的矩阵运算和可视化能力广受青睐,但面对大规模数据处理或高性能算法时,C/C++的执行效率优势无可替代。将二者结合,通过动态链接库(DLL) 实现混合编程,既能发挥MATLAB的易用性,又能借助C/C++提升核心代码性能。本文将手把手教你在Windows环境下完成从C/C++ DLL编写、编译到MATLAB调用的全流程,附带完整代码与避坑指南! 一、核心原理与准备工作 1. 核心逻辑 C/C++编译生成的DLL文件包含可被外部程序调用的函数,通过__declspec(dllexport)声明导出函数,并使用extern "C"指定C链接规范,避免C++的名称修饰(name mangling)问题,确保MATLAB能正确识别函数名。 MATLAB通过loadlibrary函数加载DLL,解析函数接口后,使用calllib函数调用目标函数,实现数据交互。 2.

By Ne0inhk
软件解耦与扩展:插件式开发方式(基于 C++ 与 C# 的实现)

软件解耦与扩展:插件式开发方式(基于 C++ 与 C# 的实现)

软件解耦与扩展:插件式开发方式 * 🤔 什么是插件式开发? * 🧩 为何选择插件式开发?—— 解耦与扩展的艺术 * 1. 高度解耦 * 2. 极致的扩展性 * 3. 增强可维护性 * 4. 支持动态加载与卸载 * 🏗️ 插件系统的核心架构 * 💻 实践篇:C# 下的插件式开发 * 1. 定义插件契约 * 2. 实现一个具体插件 * 3. 构建宿主程序(插件加载器) * 应用案例:可扩展的日志系统 * ⚙️ 实践篇:C++ 下的插件式开发 * 1. 定义插件契约 * 2. 实现一个具体插件 * 3. 构建宿主程序(插件加载器) * 📊 C# 与 C++ 实现对比 * ⚠️ 挑战与注意事项 * 🎯 总结:何时使用插件式架构? 🚀在软件工程的漫长演进中,我们始终在追求一个核心目标:构建稳定而灵活的系统。一个优秀的软件架构,如同人体的骨骼,既要坚实稳固,又要具备生长与适应的能力。

By Ne0inhk
C++ 面试题常用总结 详解(满足c++ 岗位必备,不定时更新)

C++ 面试题常用总结 详解(满足c++ 岗位必备,不定时更新)

📚 本文主要总结了一些常见的C++面试题,主要涉及到语法基础、STL标准库、内存相关、类相关和其他辅助技能,掌握这些内容,基本上就满足C++的岗位技能(红色标记为重点内容),欢迎大家前来学习指正,会不定期去更新面试内容。  Hi~!欢迎来到碧波空间,平时喜欢用博客记录学习的点滴,欢迎大家前来指正,欢迎欢迎~~ ✨✨ 主页:碧波 📚 📚 专栏:C++ 系列文章 目录 一、C ++ 语法基础 🔥 谈谈变量的使用和生命周期,声明和初始化 🔥 谈谈C++的命名空间的作用 🔥  include " " 和 <> 的区别 🔥 指针是什么? 🔥 什么是指针数组和数组指针 🔥 引用是什么? 🔥 指针和引用的区别 🔥 什么是函数指针和指针函数以及区别 🔥 什么是常量指针和指针常量以及区别 🔥 智能指针的本质是什么以及实现原理 🔥 weak_ptr 是否有计数方式,在那分配空间? 🔥 类型强制转换有哪几种? 🔥 函数参数传递时,

By Ne0inhk