什么是MCP(Model Context Protocol)?
-
MCP(Model Context Protocol)是Anthropic推出的一个开放协议,旨在统一LLM应用与外部数据源和工具之间的通信协议,为AI开发提供了标准化的上下文交互方式。
-
MCP的主要功能包括数据集成、工具集成、模板化交互、安全性、开发者支持、预构建服务器和上下文维护。它通过客户端-服务器架构,支持多个服务连接到任何兼容的客户端,提供标准化的、通用的协议共享资源、工具和提示。MCP能访问本地和远程资源,内置安全机制,保护API密钥不被泄露,是构建互联AI系统的重要工具。
为什么要用MCP
MCP(Model Context Protocol)是一个由Anthropic提出的开放协议,它旨在简化大型语言模型(LLM)与外部数据源和工具之间的通信。使用MCP的原因主要包括:
- 简化开发流程:MCP协议允许开发者通过一个标准化的接口与不同的数据源和服务进行交互,无需为每个数据源编写单独的连接器,从而降低了开发成本和维护负担。
- 跨平台支持:MCP提供了一种统一的接入方式,使得开发者能够自由选择不同的大语言模型提供商,不再受限于单一平台。
- 数据安全:通过本地服务器与数据源的连接,MCP避免了将敏感数据上传到第三方平台,最大限度地保障了数据隐私。
- 增强人工智能能力:MCP通过为AI模型提供对各种数据源的无缝访问,增强了其生成更相关、更准确响应的能力。
- 促进安全:MCP的设计充分考虑了安全性,服务器控制自己的资源,无需与AI提供商共享敏感的API密钥,确保数据访问既可控又可审计。
- 团队协作:作为一项开源计划,MCP鼓励开发者社区做出贡献,这种协作环境加速了创新并扩大了可用连接器和工具的范围。
- 模块化和可扩展性:MCP的架构允许模块化开发,多个MCP服务器可以连接到单个主机,每个服务器处理不同的资源。
- 互操作性:通过MCP标准化通信,使不同的AI工具和资源能够无缝协作。
- 降低开发成本,加速创新:采用MCP,开发者无需为每个数据源构建定制的集成方案,只需针对MCP协议构建一次即可。
- 构建可持续的AI架构:MCP促进了更可持续的AI架构,随着生态系统的成熟,AI系统将在不同工具和数据集之间移动时保持上下文。
MCP的核心架构和工作原理
MCP的工作原理可以概括为三个步骤:
- 首先,通过调用聊天完成API,将函数和用户输入传递给MCP服务器;
- 其次,使用模型的响应来调用API或函数;
- 最后,再次调用聊天完成API,包括从函数获得的响应,以得到最终响应。
MCP的应用场景
MCP的应用场景包括但不限于以下几个方面:
- 软件开发:通过将AI模型连接到代码存储库或问题跟踪器来增强代码生成工具,从而提高开发效率和代码质量。
- 数据分析:允许AI助手访问和分析来自数据库或云存储的数据集,使得数据分析更加高效和深入。
- 企业自动化:将AI与CRM系统或项目管理平台等业务工具相结合,实现企业流程的自动化和优化。
- 智能客服系统:在智能客服系统中,MCP协议可以帮助从多个数据源获取用户信息、订单记录和商品数据,实现不同数据源之间的无缝对接,提高开发效率和系统稳定性。
- 内容生成平台:在内容生成平台中,MCP协议可以解决数据连接问题,使得开发者可以更高效地处理文本、图片和视频数据,加快开发进度并提升平台性能。
- 数据分析系统:在数据分析系统中,MCP协议可以处理来自多个数据源的大规模数据,确保数据的快速传输和处理,提升系统性能和响应速度。
- 医疗场景:通过MCP提供患者病史,模型可以更精准地生成诊断建议,同时可以集成实验室检测系统和医学影像分析工具,为医生提供全面的患者信息和诊断支持。
- 教育场景:使用MCP集成专业知识或工具,可以设计课程内容、生成多语言学习材料、解答学生问题,扩大AI在教育领域的应用潜力。
- 金融场景:在金融领域,MCP可以协作完成市场趋势分析、投资报告生成和多语言客户支持,提高金融服务的质量和效率。
MCP核心功能和特性
MCP(Model Context Protocol)的核心功能和特性可以简要概括为:
- 数据访问标准化:MCP提供了一个通用的开放协议,允许开发者通过统一的方式连接各种数据源,如Google Drive、Slack、GitHub等,无需为每种数据源单独开发复杂的接口代码。
- 双向安全连接:MCP支持在AI应用和数据源之间建立双向的、安全的通信通道,确保数据的隐私性和交互的完整性。
- 上下文感知能力:MCP允许AI助手从数据源中提取更全面的上下文信息,提供更加精准和相关的回答。
- 模块化与可扩展性:MCP的架构灵活,支持模块化开发,允许开发者扩展MCP,创建更多的数据源支持。
- 开源与社区支持:MCP是一个完全开源的标准,鼓励开发者社区贡献代码或创建新的连接器,形成健康的开发者生态。
- 多场景应用支持:MCP适用于多种场景,包括但不限于软件开发、数据分析、企业自动化等。
- 安全性:内置安全机制,保护数据和API密钥。
- 开发者支持:提供SDK和文档,支持开发者构建和测试MCP连接器。
- 预构建服务器:提供预构建的MCP服务器,快速集成流行企业系统。
- 上下文维护:在不同工具和数据集之间保持上下文,实现更智能的任务处理。
MCP的工作流程
MCP的工作流程通常包括以下步骤:
-
初始化:主机应用程序启动并初始化客户端,每个客户端与一个服务器建立连接。
-
功能协商:客户端和服务器之间进行功能协商,确定它们可以相互提供哪些功能和服务。
-
请求处理:客户端根据用户请求或AI模型的需要,向服务器发送请求。服务器处理这些请求,并可能与本地或远程资源进行交互。
-
响应返回:服务器将处理结果返回给客户端,客户端再将信息传递回主机应用程序。
MCP通信机制
MCP(Model Context Protocol)的连接机制遵循客户端-服务器架构。在这种架构中,MCP Clients与MCP Servers之间建立一对一的连接。
这种设计允许MCP Hosts(如AI应用程序)通过MCP Clients与一个或多个MCP Servers进行通信,以获取数据和执行任务。
MCP支持两种类型的通信机制:
- 标准输入输出(Stdio):适用于本地进程间通信,其中Client启动Server程序作为子进程,消息通讯通过stdin/stdout进行,消息格式为JSON-RPC 2.0。
- 服务器发送事件(SSE):用于基于HTTP的通信,允许服务器向客户端推送消息,而客户端到服务器的消息传递则使用HTTP POST,同样采用JSON-RPC 2.0格式进行消息交换。
所有传输都使用JSON-RPC 2.0进行消息交换,这为MCP Clients和MCP Servers之间的通信提供了统一的消息格式。至于连接类型,MCP没有明确指出是长连接还是短连接,但考虑到其基于JSON-RPC 2.0的特性,它更可能支持长连接,以便保持客户端和服务器之间的持久交互状态。
MCP的通信协议可以是TCP或UDP,这取决于具体的实现和部署需求。
例如,如果MCP服务器和客户端在同一台机器上运行,可能会使用UDP。如果它们分布在不同的机器上,或者需要跨越网络边界,那么TCP可能是更好的选择,因为它提供了更可靠的传输保证。然而,MCP的设计允许它适应不同的网络环境和通信需求。
实战
- 声明:此部分代码参考引用大神文章出处: https://www.studywithgpt.com/zh-cn/tutorial/vu2u2w
以下节选部分步骤并整理代码,以供参考,详细的参考上面链接查看。
客户端代码client.py
import asyncio
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv() # 从.env加载环境变量
class MCPClient:
def __init__(self):
# 初始化会话和客户端对象
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.anthropic = Anthropic()
async def connect_to_server(self, server_script_path: str):
"""连接到MCP服务器
参数:
server_script_path: 服务器脚本的路径(.py或.js)
"""
is_python = server_script_path.endswith('.py')
is_js = server_script_path.endswith('.js')
if not (is_python or is_js):
raise ValueError("服务器脚本必须是.py或.js文件")
command = "python" if is_python else "node"
server_params = StdioServerParameters(
command=command,
args=[server_script_path],
env=None
)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
await self.session.initialize()
# 列出可用工具
response = await self.session.list_tools()
tools = response.tools
print("\n成功连接到服务器,工具列表:", [tool.name for tool in tools])
async def process_query(self, query: str) -> str:
"""使用Claude及可用工具处理查询"""
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]
# 初始Claude API调用
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
tools=available_tools
)
# 处理响应和工具调用
tool_results = []
final_text = []
for content in response.content:
if content.type == 'text':
final_text.append(content.text)
elif content.type == 'tool_use':
tool_name = content.name
tool_args = content.input
# 执行工具调用
result = await self.session.call_tool(tool_name, tool_args)
tool_results.append({"call": tool_name, "result": result})
final_text.append(f"[调用工具 {tool_name},参数 {tool_args}]")
# 将工具结果继续对话
if hasattr(content, 'text') and content.text:
messages.append({
"role": "assistant",
"content": content.text
})
messages.append({
"role": "user",
"content": result.content
})
# 从Claude获取下一个响应
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
)
final_text.append(response.content[0].text)
return "\n".join(final_text)
async def chat_loop(self):
"""运行交互式聊天循环"""
print("\nMCP客户端已启动!")
print("输入您的查询,或输入'quit'退出。")
while True:
try:
query = input("\n查询: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query)
print("\n" + response)
except Exception as e:
print(f"\n错误: {str(e)}")
async def cleanup(self):
"""清理资源"""
await self.exit_stack.aclose()
# 入口点
async def main():
if len(sys.argv) < 2:
print("用法: python client.py <path_to_server_script>")
sys.exit(1)
client = MCPClient()
try:
await client.connect_to_server(sys.argv[1])
await client.chat_loop()
finally:
await client.cleanup()
if __name__ == "__main__":
import sys
asyncio.run(main())
服务端代码server.py
import os
import json
import logging
from datetime import datetime, timedelta
from collections.abc import Sequence
from functools import lru_cache
from typing import Any
import urllib.request
import httpx
import asyncio
from dotenv import load_dotenv
from mcp.server import Server
from mcp.types import (
Resource,
Tool,
TextContent,
ImageContent,
EmbeddedResource,
LoggingLevel
)
from pydantic import AnyUrl
# Load environment variables
load_dotenv()
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("weather-server")
# API configuration
API_KEY = "abcdefgxxxxxxxx" #os.getenv("OPENWEATHER_API_KEY")
if not API_KEY:
raise ValueError("OPENWEATHER_API_KEY environment variable required")
API_BASE_URL = "http://api.openweathermap.org/data/2.5"
DEFAULT_CITY = "London"
CURRENT_WEATHER_ENDPOINT = "weather"
FORECAST_ENDPOINT = "forecast"
# The rest of our server implementation will go here
# Create reusable params
http_params = {
"appid": API_KEY,
"units": "metric"
}
async def fetch_weather(city: str) -> dict[str, Any]:
print("fetching weather...")
async with httpx.AsyncClient() as client:
# response = await client.get(
# f"{API_BASE_URL}/weather",
# params={"q": city, **http_params}
# )
response = response = urllib.request.urlopen("http://hq.sinajs.cn/list=000001")
response.raise_for_status()
data = response.json()
print(data)
return {
"temperature": data["main"]["temp"],
"conditions": data["weather"][0]["description"],
"humidity": data["main"]["humidity"],
"wind_speed": data["wind"]["speed"],
"timestamp": datetime.now().isoformat()
}
app = Server("weather-server")
@app.list_resources()
async def list_resources() -> list[Resource]:
"""List available weather resources."""
uri = AnyUrl(f"weather://{DEFAULT_CITY}/current")
return [
Resource(
uri=uri,
name=f"Current weather in {DEFAULT_CITY}",
mimeType="application/json",
description="Real-time weather data"
)
]
@app.read_resource()
async def read_resource(uri: AnyUrl) -> str:
"""Read current weather data for a city."""
city = DEFAULT_CITY
if str(uri).startswith("weather://") and str(uri).endswith("/current"):
city = str(uri).split("/")[-2]
else:
raise ValueError(f"Unknown resource: {uri}")
try:
weather_data = await fetch_weather(city)
return json.dumps(weather_data, indent=2)
except httpx.HTTPError as e:
raise RuntimeError(f"Weather API error: {str(e)}")
# Resource implementation ...
@app.list_tools()
async def list_tools() -> list[Tool]:
"""List available weather tools."""
return [
Tool(
name="get_forecast",
description="Get weather forecast for a city",
inputSchema={
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name"
},
"days": {
"type": "number",
"description": "Number of days (1-5)",
"minimum": 1,
"maximum": 5
}
},
"required": ["city"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
"""Handle tool calls for weather forecasts."""
if name != "get_forecast":
raise ValueError(f"Unknown tool: {name}")
if not isinstance(arguments, dict) or "city" not in arguments:
raise ValueError("Invalid forecast arguments")
city = arguments["city"]
days = min(int(arguments.get("days", 3)), 5)
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{API_BASE_URL}/{FORECAST_ENDPOINT}",
params={
"q": city,
"cnt": days * 8, # API returns 3-hour intervals
**http_params,
}
)
response.raise_for_status()
data = response.json()
forecasts = []
for i in range(0, len(data["list"]), 8):
day_data = data["list"][i]
forecasts.append({
"date": day_data["dt_txt"].split()[0],
"temperature": day_data["main"]["temp"],
"conditions": day_data["weather"][0]["description"]
})
return [
TextContent(
type="text",
text=json.dumps(forecasts, indent=2)
)
]
except httpx.HTTPError as e:
logger.error(f"Weather API error: {str(e)}")
raise RuntimeError(f"Weather API error: {str(e)}")
async def main():
# Import here to avoid issues with event loops
from mcp.server.stdio import stdio_server
print("正在启动服务器...")
async with stdio_server() as (read_stream, write_stream):
print("正在启动stdio_server...")
print(read_stream, write_stream)
await app.run(
read_stream,
write_stream,
app.create_initialization_options()
)
print("Server stopped")
Demo结果展示
-
查询指定股票信息(调用外部API),以下是查询结果
-
查询本地数据库指定表的所有商品信息,以下是查询结果
总结
- 总体来说,MCP在Function calling的基础上加了一层封装,对外暴露特定装饰器,在方便调用的同时也保护里面的数据交互安全,总体感觉很不错,各位小伙伴可以自己尝试一下。
参考资料
- Demo可以参考并学习大神文章 https://www.studywithgpt.com/zh-cn/tutorial/vu2u2w
- 源码地址Github: https://github.com/modelcontextprotocol
- Claude Desktop需要使用VPN下载,并且不支持中国大陆的手机号注册,有个人免费版本,下载地址:https://claude.ai/download