我尝试用 MCP 协议把 AI 助手接入公司内部的 MySQL 数据库和 GitHub API,前后折腾了三个版本才跑通生产环境。整个过程踩了不少坑,有的来自协议理解偏差,有的来自工具实现细节。下面这篇实战记录,希望帮你节省几个晚上。
MCP 不是什么新东西,但值得认真学
MCP(Model Context Protocol)是 Anthropic 提出的开放协议,核心解决一个问题:AI 模型如何通过标准化接口调用外部工具。在 MCP 出现之前,每个 AI 框架(LangChain、Semantic Kernel、Vercel AI SDK)都有自己的工具定义格式,开发者对接 N 个模型就要写 N 套工具适配器。MCP 定义了统一的服务端-客户端模型:模型通过 MCP 客户端发现工具列表、调用工具,工具运行在独立的 MCP 服务器里,与模型进程解耦。
从技术角度看,MCP 不是什么革命性设计------它本质上是 JSON-RPC 2.0 加了一层机器可读的工具描述 schema。但它的价值在于标准化:一个 MCP 服务器写好后,可以同时被 Cloudflare Workers AI、本地 Ollama、OpenAI API 等多个客户端复用。
第一版:从"能用"到"跑通"的距离
我的第一个目标是写一个 MCP 服务器,暴露两个工具:query_database(执行 SQL)和 list_github_issues(拉取仓库 Issue 列表)。客户端用 Python 的 mcp 库(pip install mcp)启动并注册。
服务器端代码(Python, 使用 mcp 库)
python
from mcp.server.fastmcp import FastMCP
import sqlite3
import httpx
mcp = FastMCP("ai-tool-server")
@mcp.tool()
async def query_database(sql: str) -> dict:
"""对 MySQL 数据库执行只读查询(SELECT 语句),返回列名和行数据"""
try:
conn = sqlite3.connect("data.db")
cursor = conn.execute(sql)
rows = cursor.fetchall()
columns = [desc[0] for desc in cursor.description]
return {"columns": columns, "rows": rows[:50]} # 限制返回行数
except Exception as e:
return {"error": f"查询失败: {e}"}
finally:
conn.close()
@mcp.tool()
async def list_github_issues(owner: str, repo: str) -> list:
"""获取指定 GitHub 仓库的 open issues"""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://api.github.com/repos/{owner}/{repo}/issues",
params={"state": "open", "per_page": 10}
)
resp.raise_for_status()
issues = resp.json()
return [{"title": i["title"], "url": i["html_url"]} for i in issues]
if __name__ == "__main__":
mcp.run(transport="stdio")
这段代码跑起来后可以用 MCP Inspector 测试:
bash
# 启动服务器
python server.py &
# 使用 MCP Inspector 交互测试(浏览器 UI)
npx @modelcontextprotocol/inspector
# 或直接在终端验证:
python -c "
import asyncio
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.session import ClientSession
async def test():
params = StdioServerParameters(command=['python', 'server.py'])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
tools = await session.list_tools()
print('可用工具:', [t.name for t in tools.tools])
asyncio.run(test())
"
如果返回 {... "result": ...} 说明调用成功。但很快我发现两个问题:
- SQL 注入风险 :直接把用户(AI 模型)传来的 SQL 字符串拼接执行,等于把数据库钥匙交给陌生人。后来我用
sqlite3的参数化查询 + 白名单校验来限制。 - GitHub API 速率限制 :未认证的请求每小时只有 60 次,AI Agent 密集调用时很快就爆 403。解决方案是加上
Authorizationheader 使用个人 token。
客户端集成:用 OpenAI Function Calling 接入 MCP
接下来写客户端,让 OpenAI 模型能自动调用 MCP 服务器里的工具。这里的关键是把 MCP 的工具 schema 转换为 OpenAI Function Calling 的 functions 参数,并把模型的函数调用请求路由回 MCP 服务器。
python
import json
import asyncio
from openai import OpenAI
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.session import ClientSession
async def run():
# 启动 MCP 服务器子进程
server_params = StdioServerParameters(command=["python", "server.py"])
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# 获取工具列表
tools_result = await session.list_tools()
# 转换为 OpenAI tools 格式
openai_tools = []
for tool in tools_result.tools:
openai_tools.append({
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema
}
})
client = OpenAI()
messages = [{"role": "user", "content": "帮我查一下数据库里有没有用户名叫 'admin' 的,再列一下仓库 star 最多的 issue"}]
# 第一次请求,模型可能直接调用工具
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=openai_tools,
tool_choice="auto"
)
msg = response.choices[0].message
# 如果模型请求调用工具,我们去 MCP 服务器执行
if msg.tool_calls:
for tool_call in msg.tool_calls:
tool_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
result = await session.call_tool(tool_name, arguments)
# 把工具返回值回传给模型
messages.append(msg)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result.content)
})
# 第二次请求,模型基于结果生成最终回答
final_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
print(final_response.choices[0].message.content)
asyncio.run(run())
这段代码跑通后,我的 AI 助手可以同时查数据库和读 GitHub Issues 了。但生产环境远远不够------下面是我踩过的 3 个深坑。
踩坑实录
坑 1:工具调用状态管理
MCP 每次工具调用是无状态的,但很多场景需要上下文。比如"帮我查一下上个月销售额最高的产品"需要两次查询:先查日期范围,再查产品。模型如果分别调用两个工具,但第一个工具的结果没传进第二个工具,就会漏掉上下文。我最初以为 MCP 的 tool_call 可以传递 session,后来发现协议本身没有 session 概念。解决方案是在对话层面自行维护上下文,或者把工具融合成更粗粒度的能力:比如提供一个 analyze_sales 工具,内部执行多步查询。
坑 2:错误信息的透明性
MCP 的错误返回格式是 { "code": -32000, "message": "..." },模型看到后很可能直接放弃或重复尝试。例如 SQL 语法错误时,模型收到"查询失败: near 'SELEC': syntax error"后,有概率重新生成同样的 SQL 再试,陷入死循环。我在工具实现中加入了"智能错误提示":对于常见的语法错误,返回"SQL 语法有误,请检查关键字拼写,SELECT 语句示例:SELECT * FROM users LIMIT 5"。模型看到清晰的引导后,大多能自纠正。
坑 3:长工具列表下的选择延迟
当工具数量超过 15 个时,OpenAI 的 function calling 响应时间会明显增加------据社区开发者反映,在大型工具列表场景下响应延迟可能翻倍。MCP 的 tools/list 可以返回所有工具,但模型端要花时间解析和选择。优化方案:对工具按场景分组,把不常用的工具转为"手动触发",或者在 MCP 中实现动态工具路由------让 MCP 服务器根据输入内容只暴露相关工具。目前我采用的是对工具 schema 增加 concurrency 标签,告诉客户端哪些工具可以并行调用。
MCP 适合什么场景?
不要为了用 MCP 而用 MCP。如果你的 AI 系统只对接一个模型、一个框架,直接写工具适配器反而更简单。MCP 的价值在 多模型、多工具、多环境:
- 你的 AI 服务同时支持 OpenAI、Claude、本地模型
- 不同团队开发不同工具,需要统一注册和发现
- 工具需要独立升级、限流、权限控制
我目前的架构是:每个工具一个独立的 MCP 服务器进程,通过 supervisord 管理生命周期。一个中央 MCP 路由器(用 Go 写,基于 JSON-RPC 转发)接收客户端请求,根据工具名路由到对应服务器。这样每个工具可以独立部署、独立扩缩容,而且升级时不影响其他工具。
结尾:工具集成的未来不在于协议,而在于秩序
与其在每个 AI 框架里重复写工具适配器,不如花时间理解 MCP 协议背后的"标准化"哲学。真正的工程价值不在于代码量,而在于集成后的稳定性和可维护性。MCP 不是银弹------它不解决工具内部的错误处理、也不解决模型选择哪个工具的逻辑。但它给混乱的工具集成世界提供了一份"统一字典":所有工具都按同一份 schema 描述自己,所有客户端都按同一套 JSON-RPC 调用。当你需要把同一个工具同时暴露给 Slack AI、公司内部 Copilot 和浏览器插件时,你就会意识到这份秩序的价值。如果你还在手动拼凑不同框架的工具调用接口,不妨试试 MCP。至少,下次新框架出来了,你只需要写一个十几行的适配器,而不是重写整套工具。