欢迎光临
我们一直在努力

MCP(Model Context Protocol)完整实战指南:从零搭建AI Agent工具生态

引言:为什么MCP正在改变AI Agent的开发方式

2025年底,Anthropic开源了Model Context Protocol(MCP)规范,到2026年中期,MCP已经成为AI Agent开发生态中最重要的标准化协议之一。它解决了一个核心问题:如何让LLM应用安全、标准化地与外部工具和数据源交互。

在MCP出现之前,每个AI Agent框架都有自己的工具调用约定——OpenAI的function calling、LangChain的Tool抽象、Anthropic的tool use格式互不兼容。开发者想要同时支持多个LLM提供商的工具调用能力,需要写大量的适配代码。MCP的出现就像HTTP协议标准化了Web通信一样,将AI Agent的工具调用抽象为标准化的客户端-服务器架构。

MCP架构示意图 - AI Agent工具生态

本文将从零开始,带你完整实现一个MCP服务器和客户端,涵盖文件操作、数据库查询、API调用等真实场景,并介绍如何将MCP集成到现有的AI工作流中。

MCP核心架构解析

MCP采用经典的客户端-服务器(Client-Server)架构,但针对AI Agent场景做了大量优化:

组件 角色 典型实现
MCP Host 用户交互的入口,如Claude Desktop、IDE插件 Claude Desktop、VS Code、Cursor
MCP Client 与MCP Server建立一对一连接的客户端 Python SDK中的

1
mcp.client
MCP Server 提供标准化工具、资源和提示的轻量服务 自定义Python/TypeScript Server
传输层 通信协议,支持stdio和SSE两种模式 stdin/stdout管道或HTTP SSE

最关键的创新是传输层设计:stdio模式让MCP Server可以作为子进程运行,零网络延迟;SSE模式则支持远程服务器。这意味着既可以在本地用Python脚本快速提供服务,也可以在远端部署专用的工具服务器集群。

MCP协议的三大核心能力

MCP定义了三种核心交互原语:

  • Tools(工具):LLM可以调用的函数,执行后返回结果给模型。类似function calling但标准化了schema定义。
  • Resources(资源):结构化的只读数据,类似文件系统的文件概念,支持URI寻址。
  • Prompts(提示模板):预定义的提示模板,用户可以选择性地加载,简化交互流程。

这三种原语组合起来,几乎可以覆盖所有AI Agent与外部世界的交互场景。

环境准备:安装MCP SDK

首先,我们需要安装MCP的Python SDK。MCP官方推荐使用Python 3.10+:


1
2
3
4
5
6
7
8
9
# 创建虚拟环境
python3 -m venv .venv-mcp
source .venv-mcp/bin/activate

# 安装MCP SDK和依赖
pip install mcp httpx aiosqlite pydantic

# 验证安装
python -c "import mcp; print(mcp.__version__)"

如果你使用Node.js生态,也可以安装TypeScript版本:


1
npm install @modelcontextprotocol/sdk

本文以Python为例,但核心概念同样适用于TypeScript实现。

实战1:构建一个文件系统MCP Server

我们先从最实用的场景开始——构建一个文件操作MCP Server,让AI Agent能够安全地读写文件。这是AI编程助手最基础也最强大的能力之一。

定义工具Schema


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# file_server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import TextContent, Tool
import os
import json

app = Server("file-server")

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="read_file",
            description="读取指定文件的内容,支持文本文件",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "文件路径"},
                    "encoding": {"type": "string", "description": "文件编码,默认utf-8"}
                },
                "required": ["path"]
            }
        ),
        Tool(
            name="write_file",
            description="写入内容到文件,不存在则创建",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "文件路径"},
                    "content": {"type": "string", "description": "文件内容"},
                    "encoding": {"type": "string", "description": "文件编码,默认utf-8"}
                },
                "required": ["path", "content"]
            }
        ),
        Tool(
            name="list_directory",
            description="列出目录内容",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "目录路径"}
                },
                "required": ["path"]
            }
        )
    ]

实现工具处理逻辑


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    safe_root = os.path.expanduser("~/mcp_workspace")
    os.makedirs(safe_root, exist_ok=True)

    if name == "read_file":
        # 安全路径检查
        abs_path = os.path.abspath(os.path.join(safe_root, arguments["path"]))
        if not abs_path.startswith(safe_root):
            return [TextContent(type="text", text="错误:路径越界访问")]
       
        encoding = arguments.get("encoding", "utf-8")
        with open(abs_path, "r", encoding=encoding) as f:
            content = f.read()
        return [TextContent(type="text", text=content)]

    elif name == "write_file":
        abs_path = os.path.abspath(os.path.join(safe_root, arguments["path"]))
        if not abs_path.startswith(safe_root):
            return [TextContent(type="text", text="错误:路径越界访问")]
       
        os.makedirs(os.path.dirname(abs_path), exist_ok=True)
        encoding = arguments.get("encoding", "utf-8")
        with open(abs_path, "w", encoding=encoding) as f:
            f.write(arguments["content"])
        return [TextContent(type="text", text=f"成功写入 {len(arguments['content'])} 字符到 {arguments['path']}")]

    elif name == "list_directory":
        abs_path = os.path.abspath(os.path.join(safe_root, arguments["path"]))
        if not abs_path.startswith(safe_root):
            return [TextContent(type="text", text="错误:路径越界访问")]
       
        entries = os.listdir(abs_path)
        result = "\n".join(f"{'📁' if os.path.isdir(os.path.join(abs_path, e)) else '📄'} {e}" for e in entries)
        return [TextContent(type="text", text=result or "(空目录)")]

    raise ValueError(f"未知工具: {name}")


async def main():
    async with stdio_server() as (read_stream, write_stream):
        await app.run(read_stream, write_stream, app.create_initialization_options())

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

这个文件服务器最关键的实践是沙箱路径隔离:通过将操作限制在

1
~/mcp_workspace

目录内,确保AI Agent即使被注入恶意指令,也无法读取系统关键文件。这是MCP Server设计时最重要的安全考量之一。

实战2:数据库查询MCP Server

第二个实战是构建一个数据库查询工具,让AI Agent能够通过自然语言分析数据库。这里我们用SQLite作为演示,实际生产中可以替换为PostgreSQL或MySQL。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# db_server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import TextContent, Tool
import sqlite3
import json

app = Server("db-server")

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="query_database",
            description="执行数据库查询,返回结果JSON",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "SQL查询语句"},
                    "params": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "查询参数"
                    }
                },
                "required": ["query"]
            }
        ),
        Tool(
            name="get_schema",
            description="获取数据库中所有表的结构",
            inputSchema={
                "type": "object",
                "properties": {}
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    db_path = "~/mcp_workspace/data.db"
    db_path = os.path.expanduser(db_path)
   
    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()

    try:
        if name == "query_database":
            query = arguments["query"]
            params = arguments.get("params", [])
            cursor.execute(query, params)
           
            if query.strip().upper().startswith("SELECT"):
                rows = [dict(row) for row in cursor.fetchall()]
                result = json.dumps(rows, ensure_ascii=False, default=str)
            else:
                conn.commit()
                result = json.dumps({
                    "affected_rows": cursor.rowcount,
                    "last_row_id": cursor.lastrowid
                })
           
            return [TextContent(type="text", text=result)]
       
        elif name == "get_schema":
            cursor.execute("SELECT name, sql FROM sqlite_master WHERE type='table'")
            schema = {}
            for row in cursor.fetchall():
                schema[row["name"]] = row["sql"]
            return [TextContent(type="text", text=json.dumps(schema, ensure_ascii=False))]
   
    finally:
        conn.close()

    raise ValueError(f"未知工具: {name}")

这个Server的核心价值在于:AI Agent不再需要在提示词中嵌入完整的数据结构,而是通过

1
get_schema

工具动态获取表结构,再生成精确的SQL查询。这对于处理大型数据库尤其有效——LLM的上下文窗口再大,也不适合把整个Schema硬编码进提示词里。

实战3:集成外部API的MCP Server

第三个实战是集成第三方API。这里以GitHub API为例,构建一个可以查询仓库信息、Issue、PR的MCP Server:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# github_server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import TextContent, Tool
import httpx
import os

app = Server("github-server")
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="get_repo_info",
            description="获取GitHub仓库信息",
            inputSchema={
                "type": "object",
                "properties": {
                    "owner": {"type": "string"},
                    "repo": {"type": "string"}
                },
                "required": ["owner", "repo"]
            }
        ),
        Tool(
            name="search_code",
            description="在GitHub仓库中搜索代码",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "owner": {"type": "string"},
                    "repo": {"type": "string"},
                    "path": {"type": "string"}
                },
                "required": ["query"]
            }
        ),
        Tool(
            name="list_issues",
            description="列出仓库的Issues",
            inputSchema={
                "type": "object",
                "properties": {
                    "owner": {"type": "string"},
                    "repo": {"type": "string"},
                    "state": {"type": "string", "enum": ["open", "closed", "all"]},
                    "limit": {"type": "integer"}
                },
                "required": ["owner", "repo"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    headers = {"Accept": "application/vnd.github.v3+json"}
    if GITHUB_TOKEN:
        headers["Authorization"] = f"token {GITHUB_TOKEN}"
   
    async with httpx.AsyncClient(headers=headers) as client:
        if name == "get_repo_info":
            url = f"https://api.github.com/repos/{arguments['owner']}/{arguments['repo']}"
            resp = await client.get(url)
            data = resp.json()
            summary = {
                "name": data["full_name"],
                "stars": data["stargazers_count"],
                "forks": data["forks_count"],
                "language": data["language"],
                "description": data["description"],
                "topics": data.get("topics", []),
                "license": data.get("license", {}).get("spdx_id"),
                "updated": data["updated_at"]
            }
            return [TextContent(type="text", text=json.dumps(summary, ensure_ascii=False))]

        elif name == "search_code":
            q = arguments["query"]
            if "owner" in arguments and "repo" in arguments:
                q += f" repo:{arguments['owner']}/{arguments['repo']}"
            if "path" in arguments:
                q += f" path:{arguments['path']}"
           
            resp = await client.get(
                "https://api.github.com/search/code",
                params={"q": q, "per_page": 5}
            )
            items = resp.json().get("items", [])
            results = [{"file": i["path"], "url": i["html_url"]} for i in items]
            return [TextContent(type="text", text=json.dumps(results, ensure_ascii=False))]
   
    raise ValueError(f"未知工具: {name}")

这个Server展示了MCP的一个重要特性:工具的组合性。你可以同时启动文件服务器、数据库服务器和GitHub服务器,在同一个Host中共存。AI Agent会自动判断哪个工具最适合当前任务。

配置与运行MCP Server

MCP Server的配置非常简洁。以Claude Desktop为例,在配置文件中添加:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ~/.config/claude/claude_desktop_config.json
{
  "mcpServers": {
    "file-server": {
      "command": "python3",
      "args": ["/path/to/file_server.py"],
      "env": {}
    },
    "db-server": {
      "command": "python3",
      "args": ["/path/to/db_server.py"],
      "env": {}
    },
    "github-server": {
      "command": "python3",
      "args": ["/path/to/github_server.py"],
      "env": {
        "GITHUB_TOKEN": "ghp_xxxx"
      }
    }
  }
}

如果你在自定义的AI应用中集成MCP,可以使用MCP Client SDK以编程方式连接:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from mcp.client.stdio import stdio_client
from mcp import StdioServerParameters

async def use_mcp_tool():
    # 启动文件服务器作为子进程
    server_params = StdioServerParameters(
        command="python3",
        args=["file_server.py"]
    )
   
    async with stdio_client(server_params) as (read, write):
        # 创建会话
        async with Client(read, write) as client:
            # 获取可用工具列表
            tools = await client.list_tools()
            print(f"可用工具: {[t.name for t in tools]}")
           
            # 调用工具
            result = await client.call_tool(
                "write_file",
                {"path": "hello.txt", "content": "Hello MCP!"}
            )
            print(f"结果: {result.content[0].text}")

import asyncio
asyncio.run(use_mcp_tool())

MCP Server的安全最佳实践

MCP在带来强大能力的同时也引入了新的安全风险,以下是必须遵守的安全原则:

1. 最小权限原则

每个MCP Server只应该拥有完成其功能所需的最小权限。文件服务器不应该能访问网络,GitHub服务器不应该能读写本地文件。在stdio模式下,子进程默认只继承父进程的权限,这是天然的安全隔离。

2. 沙箱路径限制

所有文件操作类Server必须做路径验证,防止路径穿越攻击(Path Traversal)。Python中的

1
os.path.abspath()

结合

1
os.path.commonpath()

是最基本的防护。

3. 速率限制与资源控制

数据库查询工具必须实现超时控制和结果集大小限制,防止AI Agent生成全表扫描这类耗时操作:


1
2
3
4
5
6
7
8
DEFAULT_TIMEOUT = 5  # 查询超时5秒
MAX_ROWS = 1000       # 最多返回1000行

# 在查询工具中添加
cursor.execute(f"SELECT COUNT(*) FROM ({query})", params)
count = cursor.fetchone()[0]
if count > MAX_ROWS:
    query = f"SELECT * FROM ({query}) LIMIT {MAX_ROWS}"

4. 环境变量隔离

敏感API密钥通过

1
env

字段传入,而不是硬编码在代码中。MCP的配置格式天然支持这一点。

5. 审计日志

生产环境的MCP Server应该记录所有工具调用和结果,便于事后审计和调试:


1
2
3
4
5
6
7
8
9
10
11
12
13
import logging
logging.basicConfig(
    filename="mcp_audit.log",
    level=logging.INFO,
    format="%(asctime)s [%(name)s] %(message)s"
)

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    logging.info(f"工具调用: {name}, 参数: {json.dumps(arguments)[:500]}")
    # ... 处理逻辑 ...
    logging.info(f"工具返回: {str(result)[:500]}")
    return result

MCP生态现状与趋势

截至2026年中期,MCP生态已经非常丰富:

  • 官方SDK:Python和TypeScript两个版本都进入稳定期,社区贡献的Rust SDK
    1
    mcp-rs

    也日趋成熟。

  • Claude Desktop:原生支持MCP,也是最大的MCP客户端。
  • VS Code / Cursor:通过MCP插件可以无缝集成自定义工具。
  • Hermes Agent:开源AI Agent框架,通过
    1
    hermes mcp add

    命令即可添加任意MCP Server。

  • LangChain / LangGraph:通过MCP Client Wrapper可以复用MCP生态的工具。
  • 社区MCP Server市场:已有超过1000个公开MCP Server,覆盖数据库、云服务、DevOps、设计工具等各种场景。

MCP生态 - 各种AI工具集成示意图

一个值得关注的趋势是MCP Gateway——在企业内部部署统一的MCP Gateway,对外暴露标准化工具接口,内部对接各种遗留系统和API。这类似于API Gateway在微服务架构中的角色,但针对AI Agent场景做了优化。

总结

MCP正在成为AI Agent开发的事实标准。通过本文的实战,你应该已经掌握了:

  1. MCP的核心架构和三种交互原语(Tools、Resources、Prompts)
  2. 如何编写文件操作、数据库查询、外部API集成三类MCP Server
  3. MCP Server的配置、运行和客户端集成方式
  4. 生产环境中MCP Server的安全最佳实践
  5. MCP生态的现状和发展趋势

下一步,你可以尝试将MCP集成到自己的AI工作流中。无论是个人使用的自动化脚本,还是企业级的AI Agent平台,MCP都提供了清晰、标准化的工具集成方案。随着AI Agent越来越普及,掌握MCP将成为每个AI工程师的基本技能。

参考资源:

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » MCP(Model Context Protocol)完整实战指南:从零搭建AI Agent工具生态
分享到: 更多 (0)