赛道:AI Agent/LLM 技术深度
本文介绍 Model Context Protocol (MCP) 协议的实战应用,教你如何让 AI Agent 安全、高效地连接各种数据源。包含完整的 Python 代码示例,可直接复现。
引言:为什么 AI Agent 需要 MCP?
2026 年,AI Agent 已经成为开发者的标配工具。但大多数 Agent 仍然面临一个核心痛点:无法安全地访问外部数据。
想象一下这些场景:
- 你想让 Claude 分析公司数据库里的销售数据
- 你想让 AI 助手读取你的本地文件系统来整理文档
- 你想让 Agent 调用内部 API 获取实时业务指标
直接给 AI 数据库密码或 API Key?安全团队第一个不答应。
这就是 Model Context Protocol (MCP) 协议诞生的原因。MCP 由 Anthropic 在 2024 年底提出,旨在为 AI 模型与外部数据源的交互建立一套安全、标准化的协议。
本文将带你从零开始,用 Python 实现一个完整的 MCP Client,并连接真实的数据源。
一、MCP 协议核心概念
1.1 MCP 架构概览
MCP 协议采用客户端 - 服务器架构:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ AI Model │ ◄────► │ MCP Client │ ◄───► │ MCP Server │
│ (Claude) │ │ (你的应用) │ │ (数据源) │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────────┐
│ 资源/工具/提示词 │
│ Resources/Tools │
│ Prompts │
└─────────────────┘
三个核心概念:
- Resources(资源):模型可以读取的数据(如文件、数据库记录)
- Tools(工具):模型可以执行的操作(如写入文件、调用 API)
- Prompts(提示词):预定义的交互模板
1.2 MCP 的通信协议
MCP 使用 JSON-RPC 2.0 作为通信协议,支持两种传输方式:
- stdio:通过标准输入输出通信(本地进程)
- SSE:Server-Sent Events(远程服务)
本文重点讲解 stdio 方式,因为它最适合本地开发和调试。
二、环境搭建
2.1 安装依赖
# requirements.txt
mcp>=1.0.0
httpx>=0.25.0
pydantic>=2.0.0
pip install mcp httpx pydantic
2.2 项目结构
mcp-demo/
├── mcp_client.py # MCP 客户端实现
├── mcp_server.py # MCP 服务端示例
├── config.json # 配置文件
└── resources/ # 示例资源目录
三、实现 MCP Client
3.1 基础客户端类
# mcp_client.py
import asyncio
import json
from typing import Any, Optional
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
class MCPClient:
"""MCP 客户端 - 用于连接 MCP 服务器并与 AI 模型交互"""
def __init__(self, server_command: list[str]):
"""
初始化 MCP 客户端
Args:
server_command: 启动 MCP 服务器的命令,如 ["python", "mcp_server.py"]
"""
self.server_command = server_command
self.session: Optional[ClientSession] = None
self.stdio_context = None
async def connect(self):
"""连接到 MCP 服务器"""
print(f"正在启动 MCP 服务器:{' '.join(self.server_command)}")
# 创建 stdio 传输通道
self.stdio_context = stdio_client(
StdioServerParameters(
command=self.server_command[0],
args=self.server_command[1:],
)
)
# 打开通道
read, write = await self.stdio_context.__aenter__()
# 创建会话
self.session = ClientSession(read, write)
await self.session.__aenter__()
# 初始化会话
await self.session.initialize()
print("✓ MCP 连接成功")
async def disconnect(self):
"""断开连接"""
if self.session:
await self.session.__aexit__(None, None, None)
if self.stdio_context:
await self.stdio_context.__aexit__(None, None, None)
print("✓ MCP 连接已关闭")
async def list_resources(self) -> list[dict]:
"""列出所有可用资源"""
if not self.session:
raise RuntimeError("未连接到 MCP 服务器")
response = await self.session.list_resources()
return [res.model_dump() for res in response.resources]
async def read_resource(self, uri: str) -> Any:
"""读取指定资源"""
if not self.session:
raise RuntimeError("未连接到 MCP 服务器")
response = await self.session.read_resource(uri)
return response.contents
async def list_tools(self) -> list[dict]:
"""列出所有可用工具"""
if not self.session:
raise RuntimeError("未连接到 MCP 服务器")
response = await self.session.list_tools()
return [tool.model_dump() for tool in response.tools]
async def call_tool(self, name: str, arguments: dict) -> Any:
"""调用工具"""
if not self.session:
raise RuntimeError("未连接到 MCP 服务器")
response = await self.session.call_tool(name, arguments)
return response
3.2 使用示例
async def demo():
# 启动本地 MCP 服务器
client = MCPClient(["python", "mcp_server.py"])
try:
# 连接服务器
await client.connect()
# 列出所有资源
resources = await client.list_resources()
print(f"可用资源:{json.dumps(resources, indent=2)}")
# 读取特定资源
content = await client.read_resource("file:///data/sales.json")
print(f"资源内容:{content}")
# 列出所有工具
tools = await client.list_tools()
print(f"可用工具:{json.dumps(tools, indent=2)}")
# 调用工具
result = await client.call_tool("query_database", {"sql": "SELECT * FROM users"})
print(f"工具返回:{result}")
finally:
await client.disconnect()
if __name__ == "__main__":
asyncio.run(demo())
四、实现 MCP Server
4.1 基础服务端
# mcp_server.py
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Resource, Tool, TextContent
# 创建服务器实例
server = Server("demo-server")
# 定义可用资源
@server.list_resources()
async def list_resources():
return [
Resource(
uri="file:///data/sales.json",
name="销售数据",
mimeType="application/json",
description="公司销售数据"
),
Resource(
uri="file:///config/settings.json",
name="配置信息",
mimeType="application/json",
description="系统配置"
),
]
# 读取资源
@server.read_resource()
async def read_resource(uri: str):
if uri == "file:///data/sales.json":
return TextContent(
type="text",
text='{"2026-Q1": 1500000, "2026-Q2": 2300000, "2026-Q3": 1800000}'
)
elif uri == "file:///config/settings.json":
return TextContent(
type="text",
text='{"environment": "production", "version": "2.0.1"}'
)
else:
raise ValueError(f"未知资源:{uri}")
# 定义可用工具
@server.list_tools()
async def list_tools():
return [
Tool(
name="query_database",
description="查询数据库",
inputSchema={
"type": "object",
"properties": {
"sql": {"type": "string", "description": "SQL 查询语句"}
},
"required": ["sql"]
}
),
Tool(
name="send_notification",
description="发送通知",
inputSchema={
"type": "object",
"properties": {
"message": {"type": "string", "description": "通知内容"},
"level": {"type": "string", "enum": ["info", "warning", "error"]}
},
"required": ["message"]
}
),
]
# 调用工具
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "query_database":
sql = arguments.get("sql", "")
# 模拟数据库查询
return TextContent(
type="text",
text=f"模拟查询结果:执行了 {sql}"
)
elif name == "send_notification":
message = arguments.get("message", "")
level = arguments.get("level", "info")
print(f"[{level.upper()}] {message}")
return TextContent(
type="text",
text=f"通知已发送:{message}"
)
else:
raise ValueError(f"未知工具:{name}")
# 启动服务器
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
五、实战:连接真实数据源
5.1 连接 SQLite 数据库
# database_server.py
import sqlite3
import json
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Resource, Tool, TextContent
server = Server("database-server")
DB_PATH = "example.db"
def init_db():
"""初始化示例数据库"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT,
created_at TEXT
)
''')
cursor.execute('''
INSERT OR IGNORE INTO users (name, email, created_at) VALUES
('张三', 'zhangsan@example.com', '2026-01-15'),
('李四', 'lisi@example.com', '2026-02-20'),
('王五', 'wangwu@example.com', '2026-03-01')
''')
conn.commit()
conn.close()
@server.list_resources()
async def list_resources():
return [
Resource(
uri="sqlite:///users",
name="用户数据",
description="系统中的用户信息"
),
]
@server.read_resource()
async def read_resource(uri: str):
if uri == "sqlite:///users":
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
rows = cursor.fetchall()
conn.close()
data = [{"id": r[0], "name": r[1], "email": r[2], "created_at": r[3]} for r in rows]
return TextContent(type="text", text=json.dumps(data, indent=2, ensure_ascii=False))
else:
raise ValueError(f"未知资源:{uri}")
@server.list_tools()
async def list_tools():
return [
Tool(
name="query_users",
description="查询用户信息",
inputSchema={
"type": "object",
"properties": {
"user_id": {"type": "integer", "description": "用户 ID"},
"name": {"type": "string", "description": "用户姓名"}
}
}
),
Tool(
name="create_user",
description="创建新用户",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "用户姓名"},
"email": {"type": "string", "description": "邮箱地址"}
},
"required": ["name", "email"]
}
),
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
try:
if name == "query_users":
if "user_id" in arguments:
cursor.execute("SELECT * FROM users WHERE id = ?", (arguments["user_id"],))
elif "name" in arguments:
cursor.execute("SELECT * FROM users WHERE name = ?", (arguments["name"],))
else:
cursor.execute("SELECT * FROM users")
rows = cursor.fetchall()
data = [{"id": r[0], "name": r[1], "email": r[2], "created_at": r[3]} for r in rows]
return TextContent(type="text", text=json.dumps(data, indent=2, ensure_ascii=False))
elif name == "create_user":
name = arguments["name"]
email = arguments["email"]
cursor.execute(
"INSERT INTO users (name, email, created_at) VALUES (?, ?, date('now'))",
(name, email)
)
conn.commit()
user_id = cursor.lastrowid
return TextContent(type="text", text=f"用户创建成功,ID: {user_id}")
else:
raise ValueError(f"未知工具:{name}")
finally:
conn.close()
async def main():
init_db()
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
5.2 连接文件系统
# filesystem_server.py
import os
import json
from pathlib import Path
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Resource, Tool, TextContent
server = Server("filesystem-server")
WATCH_DIR = os.environ.get("MCP_WATCH_DIR", "/tmp/mcp-files")
@server.list_resources()
async def list_resources():
"""列出监视目录下的所有文件"""
resources = []
if os.path.exists(WATCH_DIR):
for root, dirs, files in os.walk(WATCH_DIR):
for file in files:
file_path = os.path.join(root, file)
resources.append(Resource(
uri=f"file://{file_path}",
name=file_path,
description=f"文件:{file}"
))
return resources
@server.read_resource()
async def read_resource(uri: str):
"""读取文件内容"""
if uri.startswith("file://"):
file_path = uri[7:] # 移除 file:// 前缀
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return TextContent(type="text", text=f.read())
else:
raise ValueError(f"文件不存在:{file_path}")
else:
raise ValueError(f"不支持的 URI: {uri}")
@server.list_tools()
async def list_tools():
return [
Tool(
name="list_directory",
description="列出目录内容",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string", "description": "目录路径"}
},
"required": ["path"]
}
),
Tool(
name="write_file",
description="写入文件",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string", "description": "文件路径"},
"content": {"type": "string", "description": "文件内容"}
},
"required": ["path", "content"]
}
),
Tool(
name="delete_file",
description="删除文件",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string", "description": "文件路径"}
},
"required": ["path"]
}
),
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "list_directory":
path = arguments["path"]
if os.path.exists(path) and os.path.isdir(path):
items = os.listdir(path)
return TextContent(type="text", text=json.dumps(items, indent=2))
else:
raise ValueError(f"目录不存在:{path}")
elif name == "write_file":
path = arguments["path"]
content = arguments["content"]
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
return TextContent(type="text", text=f"文件已写入:{path}")
elif name == "delete_file":
path = arguments["path"]
if os.path.exists(path):
os.remove(path)
return TextContent(type="text", text=f"文件已删除:{path}")
else:
raise ValueError(f"文件不存在:{path}")
else:
raise ValueError(f"未知工具:{name}")
async def main():
os.makedirs(WATCH_DIR, exist_ok=True)
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
六、与 Claude 集成
6.1 Claude Desktop 配置
在 ~/.config/claude/claude_desktop_config.json 中添加:
{
"mcpServers": {
"database": {
"command": "python",
"args": ["/path/to/database_server.py"]
},
"filesystem": {
"command": "python",
"args": ["/path/to/filesystem_server.py"],
"env": {
"MCP_WATCH_DIR": "/path/to/watch"
}
}
}
}
6.2 在对话中使用
配置完成后,在 Claude 对话中可以直接使用:
请帮我查询用户名为"张三"的用户信息
Claude 会自动调用 MCP 工具来获取数据。
七、最佳实践
7.1 安全建议
- 最小权限原则:MCP Server 只应暴露必要的数据和操作
- 输入验证:严格验证所有输入参数,防止 SQL 注入等攻击
- 访问控制:对敏感操作添加身份验证
- 日志审计:记录所有工具调用日志
7.2 性能优化
- 连接池:数据库连接应复用,避免频繁创建/销毁
- 缓存策略:对不常变化的数据添加缓存
- 超时设置:为所有操作设置合理的超时时间
7.3 错误处理
@server.call_tool()
async def call_tool(name: str, arguments: dict):
try:
# 业务逻辑
pass
except Exception as e:
# 返回友好的错误信息,不泄露内部细节
return TextContent(
type="text",
text=f"操作失败:{str(e)}"
)
八、总结
MCP 协议为 AI Agent 与外部数据源的安全交互提供了标准化方案。通过本文,你学会了:
- ✅ MCP 协议的核心概念和架构
- ✅ 如何使用 Python 实现 MCP Client 和 Server
- ✅ 如何连接 SQLite 数据库和文件系统
- ✅ 如何与 Claude 集成使用
下一步:
- 尝试实现连接 REST API 的 MCP Server
- 探索 MCP 的远程 SSE 传输模式
- 在生产环境中部署 MCP 服务
参考资源:
作者:墨星 | 技术社区运营专家 掘金主页:[你的掘金主页链接] 知识星球:[你的星球链接]
标签: #AI #Agent #MCP #Claude #LLM #Python #人工智能