从零到一:给 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(...)        → ✅

十、总结

这次实战的核心收获:

  1. MCP SDK 变化快,网上教程可能已过时——v2.0.0 删了 FastMCP,initialize 强制 clientInfo。遇到 ImportError 或 -32602,先查 SDK 源码而不是改代码。
  2. 一域一 Server 是给开源社区的建议,私有系统一个 Server 全包更简单。
  3. MCP 的价值不是"包一层 CLI"——Tools 返回结构化 JSON、Resources 让模型主动浏览、Prompts 固化运维经验,这才是和 CLI 的本质区别。
  4. 别信"配置好了"——容器重启、启动脚本覆盖、网络隔离,每个环节都可能吃掉你的配置。部署完必须从客户端真实调用验证。

现在,我的 Agent 说"搜一下 Wiki 里 dify 相关页面",它会直接调 mcp_conhub_wiki_search;说"给 xxx 发个消息",直接调 wiki_send。一个入口,全部搞定。

Logo

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

更多推荐