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

实战指南:利用jsEncrypter插件突破前端加密测试瓶颈

1. 为什么前端加密会成为测试的“拦路虎”? 如果你做过Web安全测试,尤其是登录、注册、支付这类涉及敏感数据交互的功能点,那你一定遇到过这种情况:用BurpSuite抓到的请求包,里面的密码、验证码、身份证号等关键字段,是一长串完全看不懂的乱码。你精心准备的测试用例,比如尝试输入admin' or '1'='1,结果到了服务器端,收到的却是类似aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789+/==这样的密文。这还怎么测?SQL注入、XSS、越权这些攻击手法,在密文面前全都失效了。 这就是前端加密给我们测试人员带来的核心挑战。它的初衷是好的,为了保护数据在传输过程中的安全,防止被中间人窃听。但对于安全测试而言,它就像给测试目标穿上了一层“加密盔甲”,我们的“测试矛”直接戳上去,毫无反应。传统的手工测试和自动化脚本,在加密字段面前都束手无策。你总不能每次都去猜加密算法和密钥吧?那效率太低了。 我刚开始遇到这个问题时也很头疼,尝试过各种笨办法。比如,手动在浏览器控制台里执行加密函数,把测试载荷加密后再粘贴到BurpSuite里重放。

By Ne0inhk
【年终总结】从非科班无实习到准字节前端:我始终相信,开发之外的事,才是破局关键

【年终总结】从非科班无实习到准字节前端:我始终相信,开发之外的事,才是破局关键

目录 【年终总结】从非科班无实习到准字节前端:我始终相信,开发之外的事,才是破局关键 一、求其外,善其内 1、坚持出发点正确的博文写作 2、博文更新对我心态的淬炼 3、社区交流对我视野的启发 4、向外拓展,反哺内修 二、陷入前端则前端死,跳出前端则前端活 1、从不务正业到泛前端 2、从泛前端到大前端,从有形到无形 三、秋招多少事 四、结语         作者:watermelo37         ZEEKLOG优质创作者、华为云云享专家、阿里云专家博主、腾讯云“创作之星”特邀作者、火山KOL、支付宝合作作者,全平台博客昵称watermelo37。         一个假装是giser的coder,做不只专注于业务逻辑的前端工程师,Java、Docker、Python、LLM均有涉猎。 --------------------------------------------------------------------- 温柔地对待温柔的人,包容的三观就是最大的温柔。

By Ne0inhk
前端真的能防录屏?EME(加密媒体扩展) DRM 反录屏原理 + 实战代码

前端真的能防录屏?EME(加密媒体扩展) DRM 反录屏原理 + 实战代码

🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志 🎐 个人CSND主页——Micro麦可乐的博客 🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战 🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战 🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解 🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用 🍎 《前端技术》专栏以实战为主介绍日常开发中前端应用的一些功能以及技巧,均附有完整的代码示例 ✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧 💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程 🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整 👍《Spring Security》专栏中我们将逐步深入Spring Security的各个

By Ne0inhk

Flutter 三方库 xpath_selector 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、精准的 HTML/XML 数据抓取与 Web 结构解析引擎

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 xpath_selector 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、精准的 HTML/XML 数据抓取与 Web 结构解析引擎 在鸿蒙(OpenHarmony)系统的网络爬虫、自动化测试审计、或者是从复杂的第三方 Web 公告(HTML)中提取关键数据(如新闻标题、资产负债表)时,如何摆脱凌乱的正向正则(Regex),转而使用业界标准的 XPath 语法进行语义化选取?xpath_selector 为开发者提供了一套工业级的、基于 Dart 的 HTML/XML 结构化查询方案。本文将深入实战其在鸿蒙端数据治理中的应用。 前言 什么是 XPath Selector?

By Ne0inhk