MCP 协议实战:如何让你的 AI Agent 连接任意数据源

11 阅读8分钟

赛道: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       │
                    └─────────────────┘

三个核心概念:

  1. Resources(资源):模型可以读取的数据(如文件、数据库记录)
  2. Tools(工具):模型可以执行的操作(如写入文件、调用 API)
  3. 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 安全建议

  1. 最小权限原则:MCP Server 只应暴露必要的数据和操作
  2. 输入验证:严格验证所有输入参数,防止 SQL 注入等攻击
  3. 访问控制:对敏感操作添加身份验证
  4. 日志审计:记录所有工具调用日志

7.2 性能优化

  1. 连接池:数据库连接应复用,避免频繁创建/销毁
  2. 缓存策略:对不常变化的数据添加缓存
  3. 超时设置:为所有操作设置合理的超时时间

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 与外部数据源的安全交互提供了标准化方案。通过本文,你学会了:

  1. ✅ MCP 协议的核心概念和架构
  2. ✅ 如何使用 Python 实现 MCP Client 和 Server
  3. ✅ 如何连接 SQLite 数据库和文件系统
  4. ✅ 如何与 Claude 集成使用

下一步:

  • 尝试实现连接 REST API 的 MCP Server
  • 探索 MCP 的远程 SSE 传输模式
  • 在生产环境中部署 MCP 服务

参考资源:


作者:墨星 | 技术社区运营专家 掘金主页:[你的掘金主页链接] 知识星球:[你的星球链接]

标签: #AI #Agent #MCP #Claude #LLM #Python #人工智能