前言

最近在搞 AI Agent 项目的时候,发现一个很头疼的问题:大模型能力再强,它也没法直接操作数据库、调用 API、读写文件。你得自己写一堆胶水代码把工具和模型串起来,而且换个模型又得重写一遍。

Anthropic 去年搞了个 MCP(Model Context Protocol),说白了就是给 AI Agent 定了一套标准的"工具调用协议"。你实现一次 MCP Server,所有支持 MCP 的客户端(Claude Desktop、Cursor、Continue 等)都能直接用。

今天就从零开始,手把手搭一个能用的 MCP Server。

MCP架构概览

什么是 MCP

先简单说下 MCP 的核心思路。它借鉴了 LSP(Language Server Protocol)的设计哲学:协议标准化,实现一次,到处可用

MCP 的架构分三层:

  • Host(宿主):比如 Claude Desktop、Cursor 这些客户端
  • Client(客户端):Host 内部的 MCP 客户端,负责和 Server 通信
  • Server(服务器):你写的工具服务,提供具体能力

通信方式有两种:stdio(本地进程)和 HTTP+SSE(远程服务)。今天先搞最常用的 stdio 方式。

环境准备

# Python 3.10+
python --version

# 安装 MCP SDK
pip install mcp

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

MCP SDK 的版本迭代很快,建议用最新的。如果你用的是 uv 包管理器:

uv init my-mcp-server
cd my-mcp-server
uv add mcp

写第一个 MCP Server

直接上代码。我们做一个简单的"文件工具服务器",提供读取文件和列出目录两个能力。

# server.py
import os
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

# 创建 Server 实例
server = Server("file-tools")

# 定义工具列表
@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="read_file",
            description="读取指定路径的文件内容",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "文件的绝对路径"
                    }
                },
                "required": ["path"]
            }
        ),
        Tool(
            name="list_directory",
            description="列出指定目录下的文件和子目录",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "目录的绝对路径"
                    }
                },
                "required": ["path"]
            }
        )
    ]

# 处理工具调用
@server.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "read_file":
        file_path = arguments["path"]
        if not os.path.exists(file_path):
            return [TextContent(type="text", text=f"错误:文件不存在 {file_path}")]
        with open(file_path, "r", encoding="utf-8") as f:
            content = f.read()
        return [TextContent(type="text", text=content)]

    elif name == "list_directory":
        dir_path = arguments["path"]
        if not os.path.isdir(dir_path):
            return [TextContent(type="text", text=f"错误:目录不存在 {dir_path}")]
        entries = os.listdir(dir_path)
        result = "\n".join(entries)
        return [TextContent(type="text", text=result)]

    else:
        return [TextContent(type="text", text=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__":
    import asyncio
    asyncio.run(main())

代码结构

看起来代码不多对吧?这就是 MCP 的好处——协议帮你处理了通信、序列化、错误处理这些脏活,你只需要关注业务逻辑。

代码拆解

几个关键点解释一下:

Server 实例Server("file-tools") 里的名字会显示在客户端里,用户看到的就是这个。

@server.list_tools():装饰器注册工具列表。每个 Tool 需要 name、description 和 inputSchema(JSON Schema 格式)。inputSchema 很重要,LLM 靠它来理解怎么调用你的工具。

@server.call_tool():实际执行逻辑。name 是工具名,arguments 是参数。返回值必须是 TextContent 列表。

stdio_server:用标准输入输出通信,适合本地运行。客户端启动你的进程后,通过 stdin/stdout 交换 JSON-RPC 消息。

测试一下

先手动跑看看有没有语法错误:

python server.py

程序会阻塞等待输入,这是正常的——stdio 模式下它在等客户端发消息。Ctrl+C 退出就行。

配置到 Claude Desktop

编辑 Claude Desktop 的配置文件:

{
  "mcpServers": {
    "file-tools": {
      "command": "python",
      "args": ["/absolute/path/to/server.py"]
    }
  }
}

配置文件位置:

  • macOS:~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows:%APPDATA%\Claude\claude_desktop_config.json

重启 Claude Desktop,你会看到工具图标出现了。试着让它读个文件或者列个目录,它会自动调用你的 MCP Server。

进阶:加点实用功能

光读文件太基础了。再加一个"代码搜索"工具,支持正则表达式搜索文件内容:

import re

@server.list_tools()
async def list_tools():
    return [
        # ... 前面的工具保留
        Tool(
            name="search_in_files",
            description="在指定目录下搜索匹配正则表达式的文件内容",
            inputSchema={
                "type": "object",
                "properties": {
                    "directory": {
                        "type": "string",
                        "description": "搜索的根目录"
                    },
                    "pattern": {
                        "type": "string",
                        "description": "正则表达式"
                    },
                    "file_extension": {
                        "type": "string",
                        "description": "限定文件扩展名,如 .py、.js",
                        "default": ""
                    }
                },
                "required": ["directory", "pattern"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict):
    # ... 前面的逻辑保留

    if name == "search_in_files":
        directory = arguments["directory"]
        pattern = arguments["pattern"]
        ext = arguments.get("file_extension", "")

        results = []
        for root, dirs, files in os.walk(directory):
            for file in files:
                if ext and not file.endswith(ext):
                    continue
                filepath = os.path.join(root, file)
                try:
                    with open(filepath, "r", encoding="utf-8") as f:
                        for i, line in enumerate(f, 1):
                            if re.search(pattern, line):
                                results.append(f"{filepath}:{i}: {line.strip()}")
                except (UnicodeDecodeError, PermissionError):
                    pass

        if not results:
            return [TextContent(type="text", text="没有找到匹配的内容")]
        return [TextContent(type="text", text="\n".join(results[:50]))]

这个工具在实际开发中很实用——你可以让 AI 帮你在项目里搜代码、找引用、定位 bug。

实际使用效果

踩坑记录

说几个我实际开发中遇到的坑:

1. JSON Schema 要写对

inputSchema 必须是合法的 JSON Schema。我一开始偷懒没写 required 字段,结果 LLM 调用工具时经常漏参数。写清楚 required 能显著提高调用准确率。

2. 错误处理别偷懒

工具执行出错时不要抛异常,返回一个 TextContent 告诉客户端错误信息。否则整个 MCP 连接可能断掉。

3. 返回内容别太长

如果你的工具返回了超长文本(比如读了一个几 MB 的文件),有些客户端会截断或者报错。建议加上长度限制:

content = f.read()
if len(content) > 10000:
    content = content[:10000] + "\n... (内容过长,已截断)"

4. Windows 路径注意转义

Windows 上的反斜杠路径在 JSON 里需要转义。建议在 inputSchema 的 description 里提示用户用正斜杠。

总结

MCP 这个协议设计得确实优雅。你只需要关心两个函数:list_tools 告诉客户端"我能做什么",call_tool 实际去"做"。通信、协议、序列化这些全帮你搞定了。

如果你想让 AI Agent 能操作更多东西——数据库、浏览器、文件系统、第三方 API——写个 MCP Server 就行,所有支持 MCP 的客户端都能直接用。

下一步可以研究的东西:

  • Streamable HTTP:远程部署 MCP Server,支持多客户端
  • Resources:除了 Tools,MCP 还支持 Resources(提供上下文数据)和 Prompts(模板提示词)
  • 安全机制:生产环境下的认证和权限控制

完整代码已上传 GitHub,欢迎 Star。


本文首发于 CSDN,转载请注明出处。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐