MCP协议实战:从零搭建一个AI Agent工具服务器

前言

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

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

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

什么是 MCP

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

MCP 的架构分三层:

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

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

环境准备

bash 复制代码
# Python 3.10+
python --version

# 安装 MCP SDK
pip install mcp

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

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

bash 复制代码
uv init my-mcp-server
cd my-mcp-server
uv add mcp

写第一个 MCP Server

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

python 复制代码
# 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 消息。

测试一下

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

bash 复制代码
python server.py

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

配置到 Claude Desktop

编辑 Claude Desktop 的配置文件:

json 复制代码
{
  "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。

进阶:加点实用功能

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

python 复制代码
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 的文件),有些客户端会截断或者报错。建议加上长度限制:

python 复制代码
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,转载请注明出处。

相关推荐
Do_GH2 小时前
【Linux】09.WSL+SVN部署操作说明
linux·运维·svn
哈德森hh2 小时前
我的 Twitter 自动化运营流程
运维·自动化·twitter
IT策士2 小时前
Django 从 0 到 1 打造完整电商平台:系列总结 + 项目演示与后续扩展
后端·python·django
ElevenS_it1883 小时前
连锁门店IT运维监控实战:200+门店网络设备+POS统一纳管+按区域分组告警路由完整配置(Zabbix Proxy架构)
运维·网络·架构·zabbix
君为先-bey3 小时前
LeMiCa——基于扩散模型的高效视频生成的词典序最小化路径缓存
python·算法·机器学习·扩散模型
dualven_in_csdn3 小时前
mqtt消息及日志查看
linux·运维·服务器
呉師傅3 小时前
东芝e-STUDIO 3525ac提示黄色和品红色墨粉盒在耗尽前被更换。请重新插入之前的墨粉盒并用至耗尽如何操作
运维·windows·电脑
都在酒里3 小时前
Linux字符设备驱动开发(四):进入硬件世界——GPIO子系统与LED设备驱动
linux·运维·驱动开发
L_cl3 小时前
大模型应用开发 9.FastAPI ① 请求与响应
python·fastapi