从零到一:给 AI Agent 开发一个统一 MCP 服务器(实战全记录)
从零到一:给 AI Agent 开发一个统一 MCP 服务器(实战全记录)
把 Wiki、Dify、GitLab 等所有内部系统接入一个 MCP 服务器,让所有 Agent 只连一个入口。本文记录完整开发过程:架构设计、SDK 踩坑、代码实现、Docker 部署、配置持久化。
一、需求:一个 MCP 服务器,接入所有系统
我维护着一个多 Agent 系统,里面跑着几十个 AI Agent。它们要操作各种内部系统:Wiki 知识库、Dify AI 平台、GitLab 代码仓库、Jenkins CI、LDAP 用户管理……
没有 MCP 之前,Agent 是这样干活的:
# 搜 Wiki
terminal("wiki search dify")
# 然后解析一坨文本
# 查 Dify 知识库
# 没有封装,Agent 根本不会用
每个 Agent 都要自己记命令、记凭据、解析文本。新系统接入 = 改所有 Agent。凭据散落在每个 Agent 的配置里。
老板的需求一句话:
我们需要一个 MCP 服务器,把所有的系统都接入,你要操作什么直接调 MCP 就行,不要再乱去找乱搞。
二、架构:一个 Server 全包,还是多个?
MCP 官方推荐"一域一 Server"——Wiki 一个、Dify 一个、GitLab 一个,各自独立。
但那是给开源社区分工用的。我们是私有系统、自己维护,一个 Server 全包更合理:
- Agent 端只配一条连接,不用记多个地址
- 凭据集中在 Server 端,Agent 零凭据
- 加新系统 = Server 加一个工具,全局生效
关键架构决策:
Agent (Hermes) = MCP Client
│ Streamable HTTP
▼
ConHub MCP Server (独立 Docker 容器, 192.168.1.111:9100)
│
├── Wiki.js (GraphQL)
├── Dify (REST)
├── GitLab (REST) ← 预留
└── Jenkins (REST) ← 预留
三、SDK 踩坑:MCP SDK v2.0.0 没有 FastMCP 了
这是第一个大坑。网上教程全是老 API:
# ❌ 网上教程:FastMCP(v2.0.0 已删除!)
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("conhub")
装了最新版 mcp==2.0.0 之后:
ModuleNotFoundError: No module named 'mcp.server.fastmcp'
查 SDK 源码发现,v2.0.0 用 MCPServer 替代了 FastMCP:
# ✅ v2.0.0 正确姿势
from mcp.server.mcpserver.server import MCPServer
mcp = MCPServer(name="conhub", version="1.0.0")
@mcp.tool()
def wiki_search(query: str, limit: int = 10) -> str:
return json.dumps(wiki.search(query, limit), ensure_ascii=False)
@mcp.resource("wiki://pages/{path}")
def wiki_page(path: str) -> str:
return wiki.get_content(path)
@mcp.prompt()
def investigate_agent_failure(agent_name: str) -> str:
return f"排查 Agent {agent_name} 故障:..."
if __name__ == "__main__":
mcp.run() # stdio
# 或 HTTP:
# mcp.run(transport="streamable-http", host="0.0.0.0", port=9100)
四、协议踩坑:initialize 必须带 clientInfo
写完 Server 用 JSON-RPC 测试,所有请求返回 -32602 Invalid request parameters:
{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{}}}
查 SDK v2.0.0 源码(Pydantic 模型)发现,v2.0.0 的 initialize 强制要求 clientInfo 字段:
{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18",
"capabilities":{},
"clientInfo":{"name":"my-agent","version":"1.0"}}}
加上就好了。这是新 SDK 的破坏性变更,老教程里根本没有这个字段。
五、业务系统踩坑:Wiki.js GraphQL 的脾气
我最初直接用官方的 GraphQL variables 写法:
query($q: String!, $l: Int) {
pages { search(q: $q, limit: $l) { results { id title path } } }
}
返回 400。逐个排查,Wiki.js 的 GraphQL schema 和你想的不一样:
| 踩坑 | 错误写法 | 正确写法 |
|---|---|---|
| 搜索参数名 | search(q: "...") |
search(query: "...") |
| 没有 limit 参数 | search(query:"x", limit:3) |
search(query:"x"),结果自己截取 |
| 总数字段 | totalCount |
totalHits |
| variables 兼容性 | 部分版本不支持 | 直接内联字符串最稳 |
# ✅ 最终写法:内联字符串
q = '{pages{search(query:"%s"){results{id title path}totalHits}}}' % query.replace('"', '\\"')
六、完整代码:17 个工具,Wiki 全功能 + Dify
最终实现了 17 个工具,Wiki 侧对齐 CLI 全部功能:
Wiki 查询(5 个): wiki_search wiki_get wiki_list wiki_comments wiki_inbox
Wiki 写操作(7 个): wiki_create wiki_update wiki_title wiki_move wiki_reset wiki_delete wiki_send
Dify(5 个): dify_kb_list dify_doc_search dify_doc_add dify_doc_rm dify_chat
核心是 backend 层,每个系统一个文件封装 API:
# backends/wiki.py — Wiki.js GraphQL 封装
class WikiBackend:
def _gql(self, query, variables=None):
r = requests.post(self.url, json={"query": query, "variables": variables or {}},
headers={"Authorization": f"Bearer {self.token}"}, timeout=20)
if r.status_code != 200:
raise Exception(f"GraphQL {r.status_code}: {r.text[:200]}")
data = r.json()
if "errors" in data:
raise Exception(data["errors"][0].get("message", "GraphQL error"))
return data.get("data", {})
def create(self, path, title, content, desc="", tags=None):
"""创建页面(对齐 wiki create 命令)"""
data = self._gql(self.MUT_CREATE, {"c": content, "t": title, "l": "en",
"pa": path, "d": desc, "e": "markdown", "p": True, "pv": False, "tg": tags or []})
res = data["pages"]["create"]["responseResult"]
return {"succeeded": res["succeeded"], "message": res["message"]}
def update(self, path, content, title=None):
"""更新页面(对齐 wiki push)"""
p = self.get(path) # 先查 id
data = self._gql(self.MUT_UPDATE, {"i": p["id"], "c": content, "t": title or p["title"],
"e": "markdown", "pub": True, "priv": False, "l": "en", "pa": path,
"d": p.get("description", ""), "tg": p.get("tags", [])})
return data["pages"]["update"]["responseResult"]
# create / update / title / move / reset / delete / comments / send / inbox
# 全部对齐 wiki CLI 的 21 个命令
七、Docker 部署:三个坑
坑 1:Docker Hub 拉不动
网络环境拉 python:3.13-slim 超时。解法:用本地已有的 hermes-agent 镜像做 base,把 venv 整个 COPY 进去,不联网装包:
FROM hermes-agent:latest
COPY conhub-mcp-venv/ /opt/conhub-mcp-venv/ # 本地 venv 直接拷
COPY conhub-mcp/ /opt/conhub-mcp/
CMD ["/opt/conhub-mcp-venv/bin/python3", "server.py"]
坑 2:镜像自带的 ENTRYPOINT 劫持启动
hermes-agent 镜像有自带 ENTRYPOINT 会启动 Web UI(端口 8648),docker run 时容器起来跑的是 Web UI 而不是我的 Server。解法:用 --entrypoint 覆盖:
docker run -d --name conhub-mcp \
--network host \
--entrypoint /opt/conhub-mcp-venv/bin/python3 \
-e "WIKI_TOKEN=$(cat /tmp/wiki_apikey.txt)" \
conhub-mcp:latest /opt/conhub-mcp/server.py
坑 3:必须 --network host
Agent 们分散在不同的 Docker 网络(ceo_default、deputy-ops_default……),容器间默认不通。--network host 让 Server 直接绑宿主机 IP:端口,所有 Agent 容器都能连:
docker update --restart unless-stopped conhub-mcp # 崩溃自动重启
八、配置持久化:最隐蔽的一个坑
MCP Server 部署好了,Agent 端 config.yaml 也加了:
mcp_servers:
conhub:
url: "http://192.168.1.111:9100/mcp"
transport: "streamable_http"
timeout: 120
enabled: true
容器一重启,全部 MCP 配置消失。 排查发现:容器启动脚本 startup.sh 每次启动都执行:
cp -f /repo/_src/config.yaml ~/.hermes/config.yaml
运行时改的 config.yaml 每次启动都被源文件覆盖! 解法:SSH 到宿主机,改源文件 /home/yy/myagentlab/_src/config.yaml,写入完整 mcp_servers 块,然后 diff 两个文件确认一致。
九、验证:MCP 工具直接可用
部署完成后,Agent 端直接调 MCP 工具:
wiki_search("conhub") → [{"id": "1913", "title": "ConHub MCP 服务器设计方案"}]
dify_kb_list() → [4 个知识库]
wiki_create(...) → {"succeeded": true} ✅ 写操作也通
wiki_update(...) → {"succeeded": true} ✅
wiki_comments(...) → ✅
wiki_delete(...) → ✅
十、总结
这次实战的核心收获:
- MCP SDK 变化快,网上教程可能已过时——v2.0.0 删了 FastMCP,initialize 强制 clientInfo。遇到 ImportError 或 -32602,先查 SDK 源码而不是改代码。
- 一域一 Server 是给开源社区的建议,私有系统一个 Server 全包更简单。
- MCP 的价值不是"包一层 CLI"——Tools 返回结构化 JSON、Resources 让模型主动浏览、Prompts 固化运维经验,这才是和 CLI 的本质区别。
- 别信"配置好了"——容器重启、启动脚本覆盖、网络隔离,每个环节都可能吃掉你的配置。部署完必须从客户端真实调用验证。
现在,我的 Agent 说"搜一下 Wiki 里 dify 相关页面",它会直接调 mcp_conhub_wiki_search;说"给 xxx 发个消息",直接调 wiki_send。一个入口,全部搞定。
更多推荐


所有评论(0)