注:本文为 “MCP 协议” 相关文章合辑。
图片清晰度受引文原图所限。
略作重排,未整理去重。
如有内容异常,请看原文。
一文带你入门 MCP(模型上下文协议)
kuokay 于 2025-03-16 10:29:55 发布
什么是 MCP
Model Context Protocol(MCP)是一个开放协议,用于实现 LLM 应用与外部数据源和工具之间的无缝集成。无论用户是构建 AI 驱动的 IDE、改善聊天交互,还是构建自定义的 AI 工作流,MCP 提供了一种标准化的方式,将 LLM 与它们所需的上下文连接起来。
目前,MCP 已经积累了足够的临界规模和动能,因此被视为 2023-2025 年“代理开放标准”之争的潜在赢家。有人预计,按照当前的速度,MCP 将在 7 月超越 OpenAPI:
MCP 如何工作
通用架构
MCP 的核心是一个 client-server 架构,主机应用程序可以连接到多个服务器:
- MCP Hosts:如 Claude Desktop、IDEs 或 AI 工具等程序,它们希望通过 MCP 访问资源。
- MCP Clients:维护与服务器 1:1 连接的协议客户端。
- MCP Servers:轻量级程序,通过标准化的 Model Context Protocol 暴露特定功能。
- Local Resources:本地计算机资源(如数据库、文件、服务),MCP 服务器可以安全地访问这些资源。
- Remote Resources:通过互联网可用的资源(例如,通过 APIs),MCP 服务器可以连接到这些资源。
MCP 客户端
MCP 客户端是模型上下文协议(MCP)架构中的核心组件,负责建立和管理与 MCP 服务器的连接。它实现了协议的客户端部分,处理以下功能:
- 协议版本协商,以确保与服务器的兼容性
- 能力协商,以确定可用功能
- 消息传输和 JSON-RPC 通信
- 工具发现和执行
- 资源访问和管理
- 提示系统交互
- 可选功能,如根目录管理和采样支持
MCP 客户端的工作流程如下:
- MCP 客户端从 MCP 服务器获取可用的工具列表。
- 将用户的查询连同工具描述通过 function calling 一起发送给 LLM。
- LLM 决定是否需要使用工具以及使用哪些工具。
- 如果需要使用工具,MCP 客户端会通过 MCP 服务器执行相应的工具调用。
- 工具调用的结果会被发送回 LLM。
- LLM 基于所有信息生成自然语言响应。
- 最后将响应展示给用户。
MCP 服务端
MCP 服务器是模型上下文协议(MCP)架构中的基础组件,为客户端提供工具、资源和功能。它实现了协议的服务器端,负责:
- 暴露客户端可以发现和执行的工具
- 管理基于 URI 的资源访问模式
- 提供提示模板并处理提示请求
- 支持与客户端的能力协商
- 实现服务器端协议操作
- 管理并发客户端连接
- 提供结构化日志和通知
连接生命周期
1. 初始化
- 客户端发送包含协议版本和能力的
initialize
请求。 - 服务器以其协议版本和能力响应。
- 客户端发送
initialized
通知作为确认。 - 开始正常消息交换。
2. 消息交换
初始化后,支持以下模式:
- 请求-响应:客户端或服务器发送请求,另一方响应。
- 通知:任一方发送单向消息。
3. 终止
任一方可以终止连接:
- 通过
close()
进行干净关闭。 - 传输断开。
- 错误条件。
快速入门
SQLite 实现一个集中示例
- Claude Desktop 作为 MCP 客户端。
- 一个 SQLite MCP 服务器提供安全的数据库访问。
- 本地 SQLite 数据库存储实际数据。
SQLite MCP 服务器和本地 SQLite 数据库之间的通信完全发生在本地机器上,本地 SQLite 数据库不会暴露在互联网上。Model Context Protocol 确保 Claude Desktop 只能通过定义良好的接口执行批准的数据库操作,为用户提供了一种安全的方式,让 Claude 分析和交互本地数据,同时完全控制其可以访问的内容。
前提条件
- macOS 或 Windows
- 安装最新版本的 Claude Desktop
- UV 0.4.18 或更高版本(检查命令:
uv --version
) - Git(检查命令:
git --version
) - SQLite(检查命令:
sqlite3 --version
)
例如,在 Windows 上安装:
# 使用 winget
winget install --id=astral-sh.uv -e
winget install git.git sqlite.sqlite
# 或直接下载:
# uv: https://docs.astral.sh/uv/
# Git: https://git-scm.com
# SQLite: https://www.sqlite.org/download.html
1. 安装
创建一个简单的 Windows SQLite 数据库进行测试:
# 创建一个新的 SQLite 数据库
$sql = @'
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT,
price REAL
);
INSERT INTO products (name, price) VALUES
('Widget', 19.99),
('Gadget', 29.99),
('Gizmo', 39.99),
('Smart Watch', 199.99),
('Wireless Earbuds', 89.99),
('Portable Charger', 24.99),
('Bluetooth Speaker', 79.99),
('Phone Stand', 15.99),
('Laptop Sleeve', 34.99),
('Mini Drone', 299.99),
('LED Desk Lamp', 45.99),
('Keyboard', 129.99),
('Mouse Pad', 12.99),
('USB Hub', 49.99),
('Webcam', 69.99),
('Screen Protector', 9.99),
('Travel Adapter', 27.99),
('Gaming Headset', 159.99),
('Fitness Tracker', 119.99),
('Portable SSD', 179.99);
'@
cd ~
& sqlite3 test.db $sql
2. 配置 Claude Desktop
在文本编辑器中打开 Claude Desktop 应用配置文件 %APPDATA%\Claude\claude_desktop_config.json
。例如,如果你安装了 VS Code:
code $env:AppData\Claude\claude_desktop_config.json
添加以下配置(将 YOUR_USERNAME
替换为你的实际用户名):
{
"mcpServers": {
"sqlite": {
"command": "uvx",
"args": [
"mcp-server-sqlite",
"--db-path",
"C:\\Users\\YOUR_USERNAME\\test.db"
]
}
}
}
这告诉 Claude Desktop:
- 存在一个名为 “sqlite” 的 MCP 服务器。
- 通过运行
uvx mcp-server-sqlite
启动它。 - 将其连接到你的测试数据库。
保存文件,并重新启动 Claude Desktop。
3. 测试
问题:
你能连接到我的 SQLite 数据库并告诉我有哪些产品及其价格吗?
Claude Desktop 将会:
- 连接到 SQLite MCP 服务器。
- 查询本地数据库。
- 格式化并展示结果。
原理解析
背后发生了什么?
当你使用 MCP 与 Claude Desktop 交互时:
- 服务器发现:Claude Desktop 在启动时连接到你配置的 MCP 服务器。
- 协议握手:当你询问数据时,Claude Desktop:
- 确定哪个 MCP 服务器可以提供帮助(在本例中是 sqlite)。
- 通过协议协商能力。
- 从 MCP 服务器请求数据或操作。
- 交互流程:
- 安全性:
- MCP 服务器仅暴露特定、受控的功能。
- MCP 服务器在本地机器上运行,它们访问的资源不会暴露在互联网上。
- Claude Desktop 需要用户确认才能进行敏感操作。
Python 创建一个简单的 MCP 服务器
先决条件
- 需要 Python 3.10 或更高版本:
python --version # 应为 3.10 或更高
- 通过 homebrew 安装 uv:
更多信息请参考:https://docs.astral.sh/uv/brew install uv uv --version # 应为 0.4.18 或更高
- 使用 MCP 项目创建器创建新项目:
uvx create-mcp-server --path weather_service cd weather_service
- 安装其他依赖项:
uv add httpx python-dotenv
- 设置环境:
OPENWEATHER_API_KEY=your-api-key-here # 创建:.env
创建您的服务器
-
添加基本导入和设置
在
weather_service/src/weather_service/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 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 = 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]: async with httpx.AsyncClient() as client: response = await client.get( f"{API_BASE_URL}/weather", params={"q": city, **http_params} ) response.raise_for_status() data = response.json() 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 = 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)}")
-
实工具处理程序
添加以下与工具相关的处理程序:
app = Server("weather-server") # 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)}")
-
添加 main 函数
将以下代码添加到
weather_service/src/weather_service/server.py
的末尾:async def main(): # Import here to avoid issues with event loops from mcp.server.stdio import stdio_server async with stdio_server() as (read_stream, write_stream): await app.run( read_stream, write_stream, app.create_initialization_options() )
-
在
__init__.py
中检查您的切入点将以下代码添加到
weather_service/src/weather_service/__init__.py
的末尾:from . import server import asyncio def main(): """Main entry point for the package.""" asyncio.run(server.main()) # Optionally expose other important items at package level __all__ = ['main', 'server']
连接到 Claude Desktop
-
更新 Claude 配置
修改
claude_desktop_config.json
文件:{ "mcpServers": { "weather": { "command": "uv", "args": [ "--directory", "path/to/your/project", "run", "weather-service" ], "env": { "OPENWEATHER_API_KEY": "your-api-key" } } } }
-
重启 Claude
- 彻底退出 Claude。
- 再次启动 Claude。
- 在 ⚡ 菜单中查找你的天气服务器。
测试
-
查询天气:
What’s the current weather in San Francisco? Can you analyze the conditions and tell me if it’s a good day for outdoor activities?
-
比较天气
Can you analyze the forecast for both Tokyo and San Francisco and tell me which city would be better for outdoor photography this week?
参考文档:
- Python SDK: https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#documentation
- MCP 中文文档:https://mcp-docs.cn/clients
终于有人把 MCP 讲透了!(附 MCP 实战完整代码)
赋范大模型技术社区 于 2025-03-19 19:35:22 发布
一、MCP 技术体系介绍
1. MCP 入门介绍
MCP(Model Context Protocol,模型上下文协议)由 Claude 母公司 Anthropic 于 2024 年 11 月正式提出。MCP 刚发布时并未引起广泛关注,直到 2025 年 Agent 技术大爆发,MCP 才逐渐受到重视。2025 年 2 月,Cursor 正式宣布加入 MCP 功能支持,将 MCP 推到了全体开发人员面前。
MCP 是一种技术协议,用于规范智能体 Agent 开发过程中的协作方式。它类似于秦始皇的“书同文、车同轨”,通过统一规范,大幅提高协作效率,进而提升智能体 Agent 的开发效率。截至目前,已有上千种 MCP 工具诞生,极大地丰富了 MCP 生态。
MCP 解决的最大痛点是 Agent 开发中调用外部工具的技术门槛过高。大模型本身无法直接与外部工具通信,因此需要通过创建外部函数(function)作为中介,实现大模型对外部工具的间接调用。然而,传统方法中编写这些外部函数的工作量极大,通常需要编写大量代码,并且还需要为每个外部函数编写 JSON Schema 格式的功能说明,以便让大模型“认识”这些函数。此外,还需要精心设计提示词模板,才能提高调用的准确率。
MCP 的目标是让大模型在 Agent 开发过程中更加便捷地调用外部工具。为此,MCP 提出了两个方案:
- 统一规范:将大模型运行环境称为 MCP 客户端,将外部函数运行环境称为 MCP 服务器,并统一它们之间的运行规范和通信协议。
- 共享资源:通过统一规范,避免重复开发外部函数。例如,像查询天气、网页爬取、查询本地数据库等通用需求,只需开发一次,即可在社区中共享和复用。
这种“车同轨、书同文”的规范在技术领域被称为协议,例如 HTTP 就是网络信息交换的技术协议。MCP(Model Context Protocol)也是一种旨在提高大模型 Agent 开发效率的技术协议。
为了进一步普及 MCP 协议,Anthropic 提供了一整套 MCP 客户端和服务器开发的 SDK(开发工具),支持 Python、TypeScript 和 Java 等多种语言。借助这些 SDK,开发者仅需几行代码即可快速开发一个 MCP 服务器,并将其接入任意一个 MCP 客户端来构建智能体。
2. Function calling 技术回顾
Function calling 是一种技术设计,允许大模型通过外部函数间接调用外部工具。其基本流程如下:
- 大模型生成一个请求,调用外部函数。
- 外部函数接收请求,并与外部工具进行通信。
- 外部工具返回结果,外部函数将结果传递回大模型。
例如,当查询当前天气时,大模型通过 Function calling 调用外部工具的过程如下:
3. 大模型 Agent 开发技术体系回顾
大模型 Agent 开发的核心在于如何高效地调用外部工具。MCP 的出现极大地简化了这一过程,使得开发者可以专注于核心逻辑的实现,而无需重复开发通用功能。
更多关于大模型 Agent 开发的基础理论可以参考以下资源:
二、MCP 客户端 Client 开发流程
1. uv 工具入门使用指南
1.1 uv 入门介绍
MCP 开发需要借助 uv
进行虚拟环境创建和依赖管理。uv
是一个 Python 依赖管理工具,类似于 pip
和 conda
,但它更快、更高效,并且可以更好地管理 Python 虚拟环境和依赖项。它的核心目标是替代 pip
、venv
和 pip-tools
,提供更好的性能和更低的管理开销。
uv
的特点包括:
- 速度更快:相比
pip
,uv
采用 Rust 编写,性能更优。 - 支持 PEP 582:无需
virtualenv
,可以直接使用__pypackages__
进行管理。 - 兼容
pip
:支持requirements.txt
和pyproject.toml
依赖管理。 - 替代
venv
:提供uv venv
进行虚拟环境管理,比venv
更轻量。 - 跨平台:支持 Windows、macOS 和 Linux。
1.2 uv 安装流程
安装 uv
有以下两种方法:
方法 1:使用 pip
安装(适用于已安装 pip
的系统)
pip install uv
方法 2:使用 curl
直接安装
如果你的系统没有 pip
,可以直接运行以下命令:
curl -LsSf https://astral.sh/uv/install.sh | sh
这会自动下载 uv
并安装到 /usr/local/bin
。
1.3 uv 的基本用法介绍
安装 uv
后,你可以像使用 pip
一样使用它,但它的语法更简洁,速度也更快。以下是一些基本用法示例:
-
安装 Python 依赖
uv pip install requests
与
pip install requests
类似,但速度更快。 -
创建虚拟环境
uv venv myenv
等效于
python -m venv myenv
,但更高效。 -
激活虚拟环境
source myenv/bin/activate # Linux/macOS myenv\Scripts\activate # Windows
-
安装
requirements.txt
uv pip install -r requirements.txt
-
直接运行 Python 项目
如果项目中包含
pyproject.toml
,你可以直接运行:uv run python script.py
这等效于:
pip install -r requirements.txt python script.py
但
uv
的速度更快,管理更高效。
2. MCP 极简客户端搭建流程
2.1 创建 MCP 客户端项目
# 创建项目目录
uv init mcp-client
cd mcp-client
2.2 创建 MCP 客户端虚拟环境
# 创建虚拟环境
uv venv
# 激活虚拟环境
source .venv/bin/activate
需要注意的是,与 pip
相比,uv
会自动识别当前项目主目录并创建虚拟环境。
然后可以通过 add
方法在虚拟环境中安装相关的库:
# 安装 MCP SDK
uv add mcp
2.3 编写基础 MCP 客户端
在当前项目主目录中创建 client.py
文件,并写入以下代码:
import asyncio
from mcp import ClientSession
from contextlib import AsyncExitStack
class MCPClient:
def __init__(self):
"""初始化 MCP 客户端"""
self.session = None
self.exit_stack = AsyncExitStack()
async def connect_to_mock_server(self):
"""模拟 MCP 服务器的连接(暂不连接真实服务器)"""
print("✅ MCP 客户端已初始化,但未连接到服务器")
async def chat_loop(self):
"""运行交互式聊天循环"""
print("\nMCP 客户端已启动!输入 'quit' 退出")
while True:
try:
query = input("\nQuery: ").strip()
if query.lower() == 'quit':
break
print(f"\n🤖 [Mock Response] 你说的是:{query}")
except Exception as e:
print(f"\n⚠️ 发生错误: {str(e)}")
async def cleanup(self):
"""清理资源"""
await self.exit_stack.aclose()
async def main():
client = MCPClient()
try:
await client.connect_to_mock_server()
await client.chat_loop()
finally:
await client.cleanup()
if __name__ == "__main__":
asyncio.run(main())
这段代码能够初始化 MCP 客户端(但不连接服务器),并提供一个交互式 CLI,可以输入查询(但只返回模拟回复),通过输入 quit
退出程序。需要注意的是,此时客户端没有关联任何大模型,因此只会重复用户的输入。
2.4 MCP 客户端基本代码结构
以下是 client.py
代码详解,代码核心功能包括:
- 初始化 MCP 客户端
- 提供一个命令行交互界面
- 模拟 MCP 服务器连接
- 支持用户输入查询并返回「模拟回复」
- 支持安全退出
代码具体解释如下:
首先是导入必要的库:
import asyncio # 让代码支持异步操作
from mcp import ClientSession # MCP 客户端会话管理
from contextlib import AsyncExitStack # 资源管理(确保客户端关闭时释放资源)
asyncio
:Python 内置的异步编程库,让 MCP 可以非阻塞地执行任务(比如聊天、查询)。mcp.ClientSession
:用于管理 MCP 客户端会话(但目前我们先不连接 MCP 服务器)。AsyncExitStack
:自动管理资源,确保程序退出时正确关闭 MCP 连接。
然后创建 MCPClient
类:
class MCPClient:
def __init__(self):
"""初始化 MCP 客户端"""
self.session = None # 先不连接 MCP 服务器
self.exit_stack = AsyncExitStack() # 创建资源管理器
self.session = None
:暂时不连接 MCP 服务器,后续可以修改来真正连接。self.exit_stack = AsyncExitStack()
:管理 MCP 客户端的资源,确保程序退出时可以正确释放资源。
紧接着模拟 MCP 服务器连接:
async def connect_to_mock_server(self):
"""模拟 MCP 服务器的连接(暂不连接真实服务器)"""
print("✅ MCP 客户端已初始化,但未连接到服务器")
- 这个函数不会真的连接 MCP 服务器,只是打印一条信息,表示客户端已经初始化。
async def
:因为我们用的是异步编程,所以需要用async
关键字。
然后创建交互式聊天循环:
async def chat_loop(self):
"""运行交互式聊天循环"""
print("\nMCP 客户端已启动!输入 'quit' 退出")
while True: # 无限循环,直到用户输入 'quit'
try:
query = input("\nQuery: ").strip() # 让用户输入问题
if query.lower() == 'quit': # 如果用户输入 quit,退出循环
break
print(f"\n🤖 [Mock Response] 你说的是:{query}") # 返回模拟回复
except Exception as e: # 发生错误时捕获异常
print(f"\n⚠️ 发生错误: {str(e)}")
while True
:无限循环,让用户可以不断输入查询。query = input("\nQuery: ").strip()
:获取用户输入的查询。if query.lower() == 'quit'
:如果用户输入quit
,退出循环。print(f"\n🤖 [Mock Response] 你说的是:{query}")
:模拟 MCP 服务器的响应,暂时只是回显用户输入的内容。
最后是清理资源的代码:
async def cleanup(self):
"""清理资源"""
await self.exit_stack.aclose() # 关闭资源管理器
aclose()
确保程序退出时正确关闭 MCP 连接(尽管目前没有真正的连接)。
并定义 main()
主函数:
async def main():
client = MCPClient() # 创建 MCP 客户端
try:
await client.connect_to_mock_server() # 连接(模拟)服务器
await client.chat_loop() # 进入聊天循环
finally:
await client.cleanup() # 确保退出时清理资源
client = MCPClient()
:创建一个 MCP 客户端实例。await client.connect_to_mock_server()
:初始化 MCP 客户端(暂不连接服务器)。await client.chat_loop()
:启动交互式聊天。finally:
确保不管程序是否异常退出,都会正确释放资源。
以及运行代码:
if __name__ == "__main__":
asyncio.run(main())
if __name__ == "__main__":
:确保代码只能在 Python 直接运行时执行(而不是作为库导入时)。asyncio.run(main())
:启动main()
,运行 MCP 客户端。
MCP 中一个基础的客户端代码结构总结如下:
代码部分 | 作用 |
---|---|
MCPClient.__init__() | 初始化 MCP 客户端 |
connect_to_mock_server() | 模拟 MCP 服务器连接 |
chat_loop() | 提供交互式聊天界面 |
cleanup() | 释放资源 |
main() | 启动客户端 |
asyncio.run(main()) | 运行程序 |
2.5 运行 MCP 客户端
尝试运行这个极简的 MCP 客户端:
uv run client.py
运行效果如下所示:
2.6 在 Jupyter 中运行代码
MCP 客户端也可以在 Jupyter 中运行。此时运行代码如下:
!pip install mcp
import asyncio
import nest_asyncio
from mcp import ClientSession
from contextlib import AsyncExitStack
# 解决 Jupyter Notebook 的 asyncio 事件循环冲突
nest_asyncio.apply()
class MCPClient:
def __init__(self):
"""初始化 MCP 客户端"""
self.session = None
self.exit_stack = AsyncExitStack()
async def connect_to_mock_server(self):
"""模拟 MCP 服务器的连接(暂不连接真实服务器)"""
print("✅ MCP 客户端已初始化,但未连接到服务器")
async def chat_loop(self):
"""运行交互式聊天循环"""
print("\n📢 MCP 客户端已启动!输入 'quit' 退出\n")
while True:
try:
# 直接使用 input() 获取用户输入
query = input("📝 请输入您的问题: ").strip()
if query.lower() == 'quit':
print("\n👋 退出聊天...")
break
# 模拟服务器返回响应
response = f"🤖 [Mock Response] 你说的是:{query}"
print(response)
except Exception as e:
print(f"\n⚠️ 发生错误: {str(e)}")
async def cleanup(self):
"""清理资源"""
await self.exit_stack.aclose()
async def main():
client = MCPClient()
try:
await client.connect_to_mock_server()
await client.chat_loop()
finally:
await client.cleanup()
然后即可输入如下代码开启对话:
# 在 Jupyter Notebook 中运行
await main()
运行效果如下所示:
3. MCP 客户端接入 OpenAI、DeepSeek 在线模型流程
接下来尝试在客户端中接入 OpenAI 和 DeepSeek 等在线模型进行对话。需要注意的是,由于 OpenAI 和 DeepSeek 的调用方法几乎完全相同,因此这套服务器客户端代码可以同时适用于 GPT 模型和 DeepSeek。
3.1 新增依赖
为了支持调用 OpenAI 模型,以及在环境变量中读取 API-KEY 等信息,需要先安装如下依赖:
uv add mcp openai python-dotenv
3.2 创建 .env
文件
创建 .env
文件,并写入 OpenAI 的 API-Key,以及反向代理地址。借助反向代理,国内可以无门槛直连 OpenAI 官方服务器,并调用官方 API。
写入如下内容:
BASE_URL="反向代理地址"
MODEL=gpt-4
OPENAI_API_KEY="OpenAI-API-Key"
如果是使用 DeepSeek 模型,则需要在 .env
中写入如下内容:
BASE_URL=https://api.deepseek.com
MODEL=deepseek-chat
OPENAI_API_KEY="DeepSeek API-Key"
3.3 修改 client.py
代码
接下来修改客户端代码:
import asyncio
import os
from openai import OpenAI
from dotenv import load_dotenv
from contextlib import AsyncExitStack
# 加载 .env 文件,确保 API Key 受到保护
load_dotenv()
class MCPClient:
def __init__(self):
"""初始化 MCP 客户端"""
self.exit_stack = AsyncExitStack()
self.openai_api_key = os.getenv("OPENAI_API_KEY") # 读取 OpenAI API Key
self.base_url = os.getenv("BASE_URL") # 读取 BASE URL
self.model = os.getenv("MODEL") # 读取 model
if not self.openai_api_key:
raise ValueError("❌ 未找到 OpenAI API Key,请在 .env 文件中设置 OPENAI_API_KEY")
self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url)
async def process_query(self, query: str) -> str:
"""调用 OpenAI API 处理用户查询"""
messages = [{"role": "system", "content": "你是一个智能助手,帮助用户回答问题。"},
{"role": "user", "content": query}]
try:
# 调用 OpenAI API
response = await asyncio.get_event_loop().run_in_executor(
None,
lambda: self.client.chat.completions.create(
model=self.model,
messages=messages
)
)
return response.choices[0].message.content
except Exception as e:
return f"⚠️ 调用 OpenAI API 时出错: {str(e)}"
async def chat_loop(self):
"""运行交互式聊天循环"""
print("\n🤖 MCP 客户端已启动!输入 'quit' 退出")
while True:
try:
query = input("\n你: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query) # 发送用户输入到 OpenAI API
print(f"\n🤖 OpenAI: {response}")
except Exception as e:
print(f"\n⚠️ 发生错误: {str(e)}")
async def cleanup(self):
"""清理资源"""
await self.exit_stack.aclose()
async def main():
client = MCPClient()
try:
await client.chat_loop()
finally:
await client.cleanup()
if __name__ == "__main__":
asyncio.run(main())
3.4 运行 client.py
运行以下命令开始对话客户端:
uv run client.py
运行效果如下所示:
3.5 client.py
代码解释
加载 OpenAI API Key:
from dotenv import load_dotenv
import os
# 加载 .env 文件,确保 API Key 受到保护
load_dotenv()
self.openai_api_key = os.getenv("OPENAI_API_KEY") # 读取 API Key
if not self.openai_api_key:
raise ValueError("❌ 未找到 OpenAI API Key,请在 .env 文件中设置 OPENAI_API_KEY")
load_dotenv()
:自动加载.env
文件,避免在代码中直接暴露 API Key。os.getenv("OPENAI_API_KEY")
:从环境变量中读取OPENAI_API_KEY
。raise ValueError(...)
:如果 API Key 为空,则抛出错误,提醒用户必须配置 API Key。
发送用户输入到 OpenAI API:
async def process_query(self, query: str) -> str:
"""调用 OpenAI API 处理用户查询"""
messages = [
{"role": "system", "content": "你是一个智能助手,帮助用户回答问题。"},
{"role": "user", "content": query}
]
try:
# 调用 OpenAI GPT-4 API
response = await asyncio.get_event_loop().run_in_executor(
None,
lambda: self.client.chat.completions.create(
model=self.model,
messages=messages,
max_tokens=1000,
temperature=0.7
)
)
return response.choices[0].message.content.strip()
except Exception as e:
return f"⚠️ 调用 OpenAI API 时出错: {str(e)}"
messages
:创建对话上下文,让 OpenAI 知道如何回答问题:system
角色:设定 AI 角色(如“你是一个智能助手”)。user
角色:存储用户输入。
self.client.chat.completions.create(...)
:model=self.model
:使用 OpenAI 的 GPT-4 进行对话。messages=messages
:提供聊天记录,让 AI 生成回答。max_tokens=1000
:限制 AI 生成的最大字数。temperature=0.7
:控制 AI 回答的随机性(越高越随机)。
run_in_executor(...)
:- 因为 OpenAI API 是同步的,但我们用的是异步代码。
- 这里用
asyncio.get_event_loop().run_in_executor(...)
将 OpenAI API 变成异步任务,防止程序卡顿。
交互式聊天:
async def chat_loop(self):
"""运行交互式聊天循环"""
print("\n🤖 MCP 客户端已启动!输入 'quit' 退出")
while True:
try:
query = input("\n你: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query) # 发送用户输入到 OpenAI API
print(f"\n🤖 OpenAI: {response}")
except Exception as e:
print(f"\n⚠️ 发生错误: {str(e)}")
- 输入查询
query = input("\n你: ").strip()
,支持多轮对话。 - 调用
process_query()
,将用户输入发送到 OpenAI API 并获取回复。 - 显示 OpenAI 生成的回复:
print(f"\n🤖 OpenAI: {response}")
- 用户输入
quit
退出。
4. MCP 客户端接入本地 ollama、vLLM 模型流程
接下来尝试将 ollama、vLLM 等模型调度框架接入 MCP 的客户端。由于 ollama 和 vLLM 均支持 OpenAI API 风格调用方法,因此上述 client.py
并不需要进行任何修改,我们只需要启动相应的调度框架服务,然后修改 .env
文件即可。
4.1 MCP 客户端接入本地 ollama
以 QwQ-32B 为例,尝试借助 ollama 接入 MCP 客户端。
-
启动 ollama
ollama start
-
测试模型能否调用
ollama list ollama run qwq
-
修改配置文件
修改
.env
配置文件:BASE_URL=http://localhost:11434/v1/ MODEL=qwq OPENAI_API_KEY=ollama
-
运行客户端
运行 MCP 客户端:
uv run client.py
此时是推理模型,因此会出现标志。
4.2 MCP 客户端接入 vLLM
类似地,我们也可以把 vLLM 接入 MCP 客户端中。
-
启动 vLLM 服务
cd /root/autodl-tmp CUDA_VISIBLE_DEVICES=0,1 vllm serve ./QwQ-32B --tensor-parallel-size 2
-
修改配置文件
修改
.env
配置文件:BASE_URL=http://localhost:8000/v1 MODEL=./QwQ-32B OPENAI_API_KEY=EMPTY
-
运行客户端
运行 MCP 客户端:
uv run client.py
此时是推理模型,因此会出现标志。
三、MCP 天气查询服务器 Server 与使用
1. MCP 服务器概念介绍
根据 MCP 协议定义,Server 可以提供以下三种类型的标准能力:
- Resources:资源,类似于文件数据读取,可以是文件资源或是 API 响应返回的内容。
- Tools:工具,第三方服务、功能函数,通过此可控制 LLM 可调用哪些函数。
- Prompts:提示词,为用户预先定义好的完成特定任务的模板。
2. MCP 服务器通讯机制
Model Context Protocol(MCP)是一种由 Anthropic 开源的协议,旨在将大型语言模型直接连接至数据源,实现无缝集成。根据 MCP 的规范,当前支持两种传输方式:标准输入输出(stdio)和基于 HTTP 的服务器推送事件(SSE)。近期,开发者在 MCP 的 GitHub 仓库中提交了一项提案,建议采用“可流式传输的 HTTP”来替代现有的 HTTP+SSE 方案。此举旨在解决当前远程 MCP 传输方式的关键限制,同时保留其优势。
HTTP 和 SSE(服务器推送事件)在数据传输方式上存在明显区别:
特性 | HTTP | SSE |
---|---|---|
通信方式 | 请求-响应模式 | 单向推送 |
连接特性 | 每次请求建立新连接 | 基于长连接 |
适用场景 | 传统的请求-响应场景 | 实时通知、股票行情更新等 |
需要注意的是,SSE 仅支持服务器向客户端的单向通信,而 WebSocket 则支持双向通信。
具体来说,MCP 定义了 Client 与 Server 进行通讯的协议与消息格式,其支持两种类型通讯机制:标准输入输出通讯、基于 SSE 的 HTTP 通讯,分别对应着本地与远程通讯。Client 与 Server 间使用 JSON-RPC 2.0 格式进行消息传输。
- 本地通讯:使用
stdio
传输数据。Client 启动 Server 程序作为子进程,其消息通讯是通过stdin
/stdout
进行的,消息格式为 JSON-RPC 2.0。 - 远程通讯:Client 与 Server 可以部署在任何地方,Client 使用 SSE 与 Server 进行通讯,消息的格式为 JSON-RPC 2.0。Server 定义了
/events
与/messages
接口用于推送与接收数据。
这里我们尝试一个入门级的示例,那就是创建一个天气查询的服务器。通过使用 OpenWeather API,创建一个能够实时查询天气的服务器(server),并使用 stdio
方式进行通信。
3. 天气查询服务器 Server 创建流程
3.1 服务器依赖安装
由于我们需要使用 HTTP 请求来查询天气,因此需要在当前虚拟环境中添加如下依赖:
uv add mcp httpx
3.2 服务器代码编写
接下来尝试创建服务器代码,此时 MCP 基本执行流程如下:
对应 Server 服务器代码如下:
import json
import httpx
from typing import Any
from mcp.server.fastmcp import FastMCP
# 初始化 MCP 服务器
mcp = FastMCP("WeatherServer")
# OpenWeather API 配置
OPENWEATHER_API_BASE = "https://api.openweathermap.org/data/2.5/weather"
API_KEY = "YOUR_API_KEY" # 请替换为你自己的 OpenWeather API Key
USER_AGENT = "weather-app/1.0"
async def fetch_weather(city: str) -> dict[str, Any] | None:
"""
从 OpenWeather API 获取天气信息。
:param city: 城市名称(需使用英文,如 Beijing)
:return: 天气数据字典;若出错返回包含 error 信息的字典
"""
params = {
"q": city,
"appid": API_KEY,
"units": "metric",
"lang": "zh_cn"
}
headers = {"User-Agent": USER_AGENT}
async with httpx.AsyncClient() as client:
try:
response = await client.get(OPENWEATHER_API_BASE, params=params, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json() # 返回字典类型
except httpx.HTTPStatusError as e:
return {"error": f"HTTP 错误: {e.response.status_code}"}
except Exception as e:
return {"error": f"请求失败: {str(e)}"}
def format_weather(data: dict[str, Any] | str) -> str:
"""
将天气数据格式化为易读文本。
:param data: 天气数据(可以是字典或 JSON 字符串)
:return: 格式化后的天气信息字符串
"""
# 如果传入的是字符串,则先转换为字典
if isinstance(data, str):
try:
data = json.loads(data)
except Exception as e:
return f"无法解析天气数据: {e}"
# 如果数据中包含错误信息,直接返回错误提示
if "error" in data:
return f"⚠️ {data['error']}"
# 提取数据时做容错处理
city = data.get("name", "未知")
country = data.get("sys", {}).get("country", "未知")
temp = data.get("main", {}).get("temp", "N/A")
humidity = data.get("main", {}).get("humidity", "N/A")
wind_speed = data.get("wind", {}).get("speed", "N/A")
# weather 可能为空列表,因此用 [0] 前先提供默认字典
weather_list = data.get("weather", [{}])
description = weather_list[0].get("description", "未知")
return (
f"🌍 {city}, {country}\n"
f"🌡 温度: {temp}°C\n"
f"💧 湿度: {humidity}%\n"
f"🌬 风速: {wind_speed} m/s\n"
f"🌤 天气: {description}\n"
)
@mcp.tool()
async def query_weather(city: str) -> str:
"""
输入指定城市的英文名称,返回今日天气查询结果。
:param city: 城市名称(需使用英文)
:return: 格式化后的天气信息
"""
data = await fetch_weather(city)
return format_weather(data)
if __name__ == "__main__":
# 以标准 I/O 方式运行 MCP 服务器
mcp.run(transport='stdio')
代码解释如下:
Part 1. 异步获取天气数据
- 函数
fetch_weather(city: str)
- 使用
httpx.AsyncClient()
发送异步 GET 请求到 OpenWeather API。 - 如果请求成功,则调用
response.json()
返回一个字典。 - 出现异常时,返回包含错误信息的字典。
- 使用
Part 2. 格式化天气数据
- 函数
format_weather(data: dict | str)
- 首先检查传入的数据是否为字符串,如果是,则使用
json.loads
将其转换为字典。 - 检查数据中是否包含
"error"
字段,如果有,直接返回错误提示。 - 使用
.get()
方法提取name
、sys.country
、main.temp
、main.humidity
、wind.speed
和weather[0].description
等数据,并为可能缺失的字段提供默认值。 - 将提取的信息拼接成一个格式化字符串,方便阅读。
- 首先检查传入的数据是否为字符串,如果是,则使用
Part 3. MCP 工具 query_weather(city: str)
- 函数
query_weather
- 通过
@mcp.tool()
装饰器注册为 MCP 服务器的工具,使其能够被客户端调用。 - 调用
fetch_weather(city)
获取天气数据,然后用format_weather(data)
将数据格式化为易读文本,最后返回该字符串。
- 通过
Part 4. 运行服务器
if __name__ == "__main__":
块- 调用
mcp.run(transport='stdio')
启动 MCP 服务器,采用标准 I/O 通信方式,等待客户端调用。
- 调用
此外,上述代码有两个注意事项:
query_weather
函数的函数说明至关重要,相当于是此后客户端对函数进行识别的基本依据,因此需要谨慎编写。- 当指定
transport='stdio'
运行 MCP 服务器时,客户端必须在启动时同时启动当前这个脚本,否则无法顺利通信。这是因为stdio
模式是一种本地进程间通信(IPC,Inter-Process Communication)方式,它需要服务器作为子进程运行,并通过标准输入输出(stdin/stdout)进行数据交换。
因此,当我们编写完服务器后,并不能直接调用这个服务器,而是需要创建一个对应的能够进行 stdio
的客户端,才能顺利进行通信。
4. 天气查询客户端 client 创建流程
4.1 代码编写
import asyncio
import os
import json
from typing import Optional
from contextlib import AsyncExitStack
from openai import OpenAI
from dotenv import load_dotenv
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
# 加载 .env 文件,确保 API Key 受到保护
load_dotenv()
class MCPClient:
def __init__(self):
"""初始化 MCP 客户端"""
self.exit_stack = AsyncExitStack()
self.openai_api_key = os.getenv("OPENAI_API_KEY") # 读取 OpenAI API Key
self.base_url = os.getenv("BASE_URL") # 读取 BASE URL
self.model = os.getenv("MODEL") # 读取 model
if not self.openai_api_key:
raise ValueError("❌ 未找到 OpenAI API Key,请在 .env 文件中设置 OPENAI_API_KEY")
self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url) # 创建 OpenAI client
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
async def connect_to_server(self, server_script_path: str):
"""连接到 MCP 服务器并列出可用工具"""
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
)
# 启动 MCP 服务器并建立通信
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()
# 列出 MCP 服务器上的工具
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:
"""
使用大模型处理查询并调用可用的 MCP 工具 (Function Calling)
"""
messages = [{"role": "user", "content": query}]
response = await self.session.list_tools()
available_tools = [{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
}
} for tool in response.tools]
# print(available_tools)
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
tools=available_tools
)
# 处理返回的内容
content = response.choices[0]
if content.finish_reason == "tool_calls":
# 如果需要调用工具
tool_call = content.message.tool_calls[0]
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
# 执行工具
result = await self.session.call_tool(tool_name, tool_args)
print(f"\n\n[Calling tool {tool_name} with args {tool_args}]\n\n")
# 将模型返回的调用哪个工具数据和工具执行完成后的数据都存入 messages 中
messages.append(content.message.model_dump())
messages.append({
"role": "tool",
"content": result.content[0].text,
"tool_call_id": tool_call.id,
})
# 将上面的结果再返回给大模型用于生产最终的结果
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
)
return response.choices[0].message.content
return content.message.content
async def chat_loop(self):
"""运行交互式聊天循环"""
print("\n🤖 MCP 客户端已启动!输入 'quit' 退出")
while True:
try:
query = input("\n你: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query) # 发送用户输入到 OpenAI API
print(f"\n🤖 OpenAI: {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("Usage: 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())
4.2 测试运行
# 确认进入到项目目录
cd /root/autodl-tmp/MCP/mcp-client
# 确认激活虚拟环境
source .venv/bin/activate
运行客户端并连接到服务器:
uv run client.py server.py
直接提问“请问北京今天天气如何?”运行结果如下所示:
4.3 代码解释
client 代码是整个 MCP 服务的核心,以下是这段代码的详细解释。
导入基本类
import asyncio
import os
import json
from typing import Optional
from contextlib import AsyncExitStack
from openai import OpenAI
from dotenv import load_dotenv
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
- 导入必要库:
asyncio
:支持异步编程。os
/json
:读取环境变量、解析 JSON。typing.Optional
:类型提示。contextlib.AsyncExitStack
:用于安全管理异步资源(如 MCP 连接)。openai.OpenAI
:你的自定义 OpenAI Client 类。dotenv.load_dotenv
:从.env
文件加载环境变量(如 API Key)。MCP
相关:mcp.ClientSession
、mcp.client.stdio
、StdioServerParameters
。
加载环境变量
load_dotenv()
- 从
.env
文件中加载环境变量,例如:OPENAI_API_KEY=sk-xxxxxx BASE_URL=... MODEL=...
MCPClient 类创建过程
class MCPClient:
def __init__(self):
"""初始化 MCP 客户端"""
self.exit_stack = AsyncExitStack()
self.openai_api_key = os.getenv("OPENAI_API_KEY") # 读取 OpenAI API Key
self.base_url = os.getenv("BASE_URL") # 读取 BASE URL
self.model = os.getenv("MODEL") # 读取 model
if not self.openai_api_key:
raise ValueError("❌ 未找到 OpenAI API Key,请在 .env 文件中设置 OPENAI_API_KEY")
self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url) # 创建 OpenAI client
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.exit_stack = AsyncExitStack()
:用于统一管理异步上下文(如 MCP 连接)的生命周期,可以在退出(cleanup)时自动关闭。- 读取环境变量:
openai_api_key
:OpenAI API Key。base_url
:模型请求的 Base URL(如你自建的反代地址)。model
:OpenAI 模型名称。
- 初始化 OpenAI 客户端:
OpenAI(api_key=self.openai_api_key, base_url=self.base_url)
:你自定义的 OpenAI 客户端,用来与 OpenAI Chat Completion API 通信。
self.session
:用于保存 MCP 的客户端会话,默认是None
,稍后通过connect_to_server
进行连接。- 再次声明
self.exit_stack = AsyncExitStack()
:这里两次赋值其实有点冗余(前面已赋值过一次)。不过并不影响功能,等同于覆盖掉前面的对象。可能是手误或调试时多写了一次。
连接到 MCP 服务器
async def connect_to_server(self, server_script_path: str):
...
- 负责启动并连接到 MCP 服务器,并列出可用工具。
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"
- 判断服务器脚本是 Python 还是 Node.js,选择对应的运行命令。
server_params = StdioServerParameters(
command=command,
args=[server_script_path],
env=None
)
StdioServerParameters
:告诉 MCP 客户端如何启动服务器。command=command
:如"python"
。args=[server_script_path]
:如["weather_server.py"]
。
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()
stdio_client(server_params)
:启动服务器进程,并建立标准 I/O 通信管道。self.stdio, self.write = stdio_transport
:拿到读写流。ClientSession(...)
:创建 MCP 客户端会话,与服务器交互。await self.session.initialize()
:发送初始化消息给服务器,等待服务器就绪。
# 列出 MCP 服务器上的工具
response = await self.session.list_tools()
tools = response.tools
print("\n已连接到服务器,支持以下工具:", [tool.name for tool in tools])
list_tools()
:向 MCP 服务器请求所有已注册的工具(用@mcp.tool()
标记)。- 打印工具列表,例如
["get_forecast", "query_db", ...]
。
处理用户查询并调用工具
async def process_query(self, query: str) -> str:
"""
使用大模型处理查询并调用可用的 MCP 工具 (Function Calling)
"""
messages = [{"role": "user", "content": query}]
- 收到用户输入后,先把它组装进一个
messages
列表,目前只包含用户信息({"role": "user", "content": query}
)。
response = await self.session.list_tools()
available_tools = [{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
}
} for tool in response.tools]
# print(available_tools)
- 获取服务器上的工具,再转换成
available_tools
的格式。 - 这里你自定义了一个结构:每个工具对应一个
{"type": "function", "function": {...}}
的字典。 - 方便后面发给 OpenAI,告诉它:可以调用这些工具。
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
tools=available_tools
)
- 使用 OpenAI 客户端的
chat.completions.create
方法发送请求:model=self.model
:比如"gpt-4"
或"deepseek-chat"
。messages=messages
:聊天上下文。tools=available_tools
:让模型知道有哪些可调用的「函数」。这是你自定义的“Function Calling”协议(非官方 JSON schema)。
content = response.choices[0]
if content.finish_reason == "tool_calls":
# 如果模型想调用工具
tool_call = content.message.tool_calls[0]
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
# 执行工具
result = await self.session.call_tool(tool_name, tool_args)
print(f"\n\n[Calling tool {tool_name} with args {tool_args}]\n\n")
# 将模型返回的调用哪个工具数据和工具执行完成后的数据都存入 messages 中
messages.append(content.message.model_dump())
messages.append({
"role": "tool",
"content": result.content[0].text,
"tool_call_id": tool_call.id,
})
# 将上面的结果再返回给大模型用于生产最终的结果
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
)
return response.choices[0].message.content
return content.message.content
if content.finish_reason == "tool_calls"
:- 如果模型的输出表示「想调用工具」,它会在
content.message.tool_calls
列表中声明要用哪个函数、参数是什么。 - 这是你自定义的一种函数调用机制,和官方
function_call
格式略有不同,但逻辑相似。
- 如果模型的输出表示「想调用工具」,它会在
- 取出工具名
tool_name
和参数tool_args
,再调用self.session.call_tool(tool_name, tool_args)
执行 MCP 工具。 - 把工具调用结果以「
role=tool
」的形式写入messages
。这样相当于把“函数调用结果”再喂给模型。 - 再次调用 OpenAI,让模型阅读到这个新上下文,产出最终回答。
- 如果没有要调用工具,直接返回
content.message.content
(模型的文本回答)。
交互式聊天循环
async def chat_loop(self):
"""运行交互式聊天循环"""
print("\n🤖 MCP 客户端已启动!输入 'quit' 退出")
while True:
try:
query = input("\n你: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query) # 发送用户输入到 OpenAI API
print(f"\n🤖 OpenAI: {response}")
except Exception as e:
print(f"\n⚠️ 发生错误: {str(e)}")
- 提供一个简单的命令行界面,反复让用户输入问题。
- 每个问题交给
process_query
,把结果打印出来。 - 输入
'quit'
退出循环。
清理资源
async def cleanup(self):
"""清理资源"""
await self.exit_stack.aclose()
self.exit_stack.aclose()
:异步地关闭所有在exit_stack
中注册的资源(包括 MCP 会话)。
主函数
async def main():
if len(sys.argv) < 2:
print("Usage: 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())
- 读取命令行参数,获取服务器脚本路径(如
weather_server.py
)。 - 创建
MCPClient
实例。 - 调用
connect_to_server
,启动并连接服务器。 - 进入
chat_loop
让用户输入多轮对话。 - 退出时调用
client.cleanup()
释放资源。
代码总结如下:
-
MCPClient 的主要职责:
- 启动 MCP 服务器(通过
StdioServerParameters
)。 - 建立 MCP 会话,列出可用工具。
- 处理用户输入,将其发送给 OpenAI 模型。
- 如果模型想调用 MCP 工具(Function Calling),就执行
call_tool
。 - 将结果重新发给模型,并返回最终回答。
- 启动 MCP 服务器(通过
-
Function Calling 逻辑(你的自定义版):
tools=available_tools
:在completions.create
时告诉模型有哪些工具可用。- 模型返回
finish_reason=="tool_calls"
→ 说明它想用工具。 - 解析
tool_calls[0]
,执行 MCP 工具 → 再次发给模型 → 返回最终答案。
-
为什么要两次请求?
- 第一次:模型根据你的指令,决定要不要用工具。
- 如果需要用工具 → 返回工具名称和参数 → 执行工具 → 把结果作为新的上下文发给模型。
- 第二次:模型基于工具结果给出最终回答。
-
如何运行:
python client.py weather_server.py
- 这会自动启动
weather_server.py
(MCP 服务器)并进行stdio
通讯。
- 这会自动启动
可能需要的改进:
- 多轮对话上下文:把所有消息都存进
messages
,让模型能记住以前的对话。 - 错误处理:当工具调用失败时,给用户提示。
5. MCP Inspector 功能介绍
在实际开发 MCP 服务器的过程中,Anthropic 提供了一个非常便捷的 debug 工具:Inspector。借助 Inspector,我们能够非常快捷地调用各类 server,并测试其功能。Inspector 具体功能实现流程如下。
安装 Node.js
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt install -y nodejs
验证安装:
npx -v
运行 Inspector
npx -y @modelcontextprotocol/inspector uv run server.py
然后即可在本地浏览器查看当前工具运行情况:http://127.0.0.1:5173/#resources
浏览器内容示例
查看当前服务器运行状况
四、MCP 更多进阶使用
基于我们当前介绍的 MCP 开发入门,MCP 还有许多待探索的进阶功能。
1. MCP 服务器 Server 进阶功能
- 基于 SSE 传输的 MCP 服务器功能实现
- 除了
stdio
连接模式外,MCP 还提供了可以服务器、客户端异地运行的 SSE 传输模式,以适用于更加通用的开发情况。若要实现 SSE 的 MCP 服务器通信,需要同时修改客户端和服务器代码。
- 除了
- Resources、Prompt 类功能 MCP 服务器
- 除了 Tools 功能的服务器外,MCP 还支持 Resources 类服务器和 Prompt 类服务器。
- Resources 服务器:主要负责提供更多的资源接口,如调用本地文件、数据等。
- Prompt 类服务器:用于设置 Agent 开发过程中各环节的提示词模板。
- 除了 Tools 功能的服务器外,MCP 还支持 Resources 类服务器和 Prompt 类服务器。
- 更多通用公开 & 在线服务器调用指南
- MCP 标准通信协议带来的最大价值之一,就是让广大 Agent 开发者能够基于此进行协作。在 MCP 推出后的若干时间,已经诞生了数以千计的 MCP 服务器,允许用户直接下载并进行调用。几个有名的 MCP 服务器合集(导航站)地址:
2. MCP 客户端 Client 进阶功能
此外,除了能在命令行中创建 MCP 客户端外,还支持各类客户端的调用:
其中借助对话类客户端,如 Claude Desktop,我们能够轻易地将各类服务器进行集成,从而拓展 Claude Desktop 的性能:
而在一些 IDE 客户端里,如 Cline 或者 Cursor,我们能够直接调用服务器进行开发:
此外,还有一些为 MCP 量身定制的 Agent 开发框架,通过集成 MCP 来提高 Agent 开发进度:
via:
-
一文带你入门 MCP(模型上下文协议)-优快云 博客
https://blog.youkuaiyun.com/qq_45066628/article/details/146225428 -
终于有人把 MCP 讲透了!(附 MCP 实战完整代码)_mcp 开发 - 优快云 博客
https://blog.youkuaiyun.com/fufan_LLM/article/details/146377471