
最近干了一件挺上头的事把我手上六个内容站的文档、博客、FAQ、更新日志全部塞进了一个 MCP server 里。折腾完之后不管是在 Claude Code 里问问题还是在 Cursor 里写代码AI 都能直接搜到六个站的历史内容回答问题时像带了个私人文库不用再跑来跑去切换网站、复制粘贴上下文。先交代一下背景。我平时维护的东西比较杂产品官网、API 文档、技术博客、用户 FAQ、更新日志还有一份内部运维 wiki零零散散分布在六个不同的站点系统里。以前想用 AI 帮忙查资料基本靠人工把链接和正文喂给助手遇到跨站的问题还得自己先脑内拼接好几篇文档。我也试过一次性配六个独立的 MCP 服务结果配置文件长得吓人每加一个站都要重新声明 endpoint、重启服务、维护各自的更新节奏人快被琐事淹没了。后来我换了个思路把六个站的内容当成数据源统一采集、统一索引、对外只暴露一个 MCP server。这篇文章会从方案设计、协议理解、核心实现、坑位排查四个角度完整复盘一遍重点说清楚我为什么这么设计、踩了哪些坑、最后怎么处理。如果你也想给自己的多个站点或知识库做“聚合入口”这篇应该能帮你省不少时间。1. 先把问题讲明白为什么要一个 MCP 装六个站1.1 我面对的真实场景先说这六个站都是什么来头。第一个是产品官网主要放介绍和定价内容更新频率低第二个是 API 文档站接口说明、参数表、错误码都在里面是我最常查的第三个是技术博客大概每周一篇讲架构演进和一些踩坑记录第四个是用户 FAQ客服那边一直在往里面补内容第五个是更新日志每次发版都要记一笔第六个是内部运维 wiki记录服务器拓扑、部署步骤和应急操作手册。这六个站平时都是分开维护的也都各自有搜索功能。但问题在于我想让 AI 帮我回答问题的时候它并不知道这些站里有什么。比如我在 Claude Code 里问“某个接口的错误码 40001 是什么意思”它只能根据训练数据猜或者我手动把文档贴给它。遇到跨站的问题更麻烦比如“上次更新日志里提到的 bug和运维 wiki 里的回滚步骤能不能对上”我得自己打开好几个页面来回对照。说白了我缺的不是文档而是让 AI 能“按需读取这些文档”的能力。1.2 为什么不想配六个独立的 MCP最早我当然也想过每个站配一个 MCP server 不就行了MCP 的生态里确实有很多现成的工具比如让文档站直接暴露成 MCP 服务或者用 Playwright MCP 去抓页面。但实际配下来我发现这条路有几个很现实的问题。第一是配置噪音。我用的代码助手主要是 Claude Code 和 Cursor每个工具都要在配置文件里声明 MCP server 的启动命令或者 endpoint。如果六个站各搞一个服务配置文件里就得写六段万一某个服务挂了启动的时候整条链路都报错排查起来很烦。第二是上下文管理。一个 MCP 工具调用本质上是在对话里插入一段工具返回内容。如果我让 AI 同时挂着六个站的搜索工具它在回答一个问题时可能会把六个站的返回结果全拉进来Token 消耗直接爆炸而且很多还是无关内容。这就像你问一个人“今天吃什么”结果他把六个食堂的菜单全甩给你看着齐全实际很难用。第三是维护成本。六个服务各自要更新、各自要重启、各自要有日志和鉴权。我本来就是一个人维护这些东西时间根本不够分。所以我更希望把“内容获取”这件事收敛到一处做成一个统一的数据层外面只有一个入口。1.3 聚合方案的核心思路最后定的方案是六个站的内容先通过采集程序同步到一个本地知识库然后由一个 MCP server 统一对外提供搜索和读取能力。AI 只需要面对这一个 server我只需要维护一套同步逻辑和一套服务配置。这个方案的本质是把“数据源”和“AI 入口”解耦。数据源那边我可以按每个站的特点设计不同的同步方式比如官网和博客走 sitemap 抓取API 文档直接调生成接口FAQ 导出结构化数据更新日志读 RSS。入口这边MCP server 提供几个固定的工具让 AI 自己决定什么时候搜索、什么时候读全文不用我手动干预。做到这一步之后我的日常变成新增文章时只需要等定时同步跑完AI 就能查到。整个过程不需要改任何 MCP 配置也不需要重启服务。2. 给还不熟 MCP 的朋友补个基础2.1 MCP 到底是什么如果你天天刷技术社区MCPModel Context Protocol这个词肯定不陌生。它本质上是一个开放协议用来解决一个问题怎么让 AI 模型安全、可控地访问外部数据和工具。我用一个生活类比来解释。你可以把 MCP 理解成 AI 世界的“USB-C 接口”。以前各种设备都有各自的充电口现在统一成一个标准你拿一根线就能给不同设备充电。在 AI 生态里不同模型、不同客户端Claude Code、Cursor、Cherry Studio、Dify 这些以前接入数据源的方式五花八门现在大家统一走 MCP 协议一套标准到处用。从架构上说MCP 涉及三个角色宿主Host是运行 AI 的地方比如 Claude Code客户端Client负责跟 MCP server 建立连接服务器Server负责暴露资源、工具和提示词。整体是客户端-服务器模式不是点对点模式所以一个客户端可以同时连多个 server一个 server 也可以被不同客户端复用。2.2 三种原语Resource、Tool、PromptMCP 协议里定义了三种核心原语理解清楚这仨的区别后面设计服务时才不会跑偏。Resource 是只读的数据资源适合暴露文档、配置、数据库查询结果这类“给人或 AI 看”的内容。Tool 是可调用的功能适合执行操作比如查天气、写数据库、调用接口。Prompt 是提示词模板适合封装重复性的提问方式。我在这个项目里的核心诉求是“查内容”所以主要的暴露形式是 Tool而不是 Resource。原因很简单Tool 可以让 AI 先搜索再按需读取主动权在模型手里而如果我把六个站的所有 Resource 一次性全暴露出去客户端在初始化时就要拉取所有的资源列表Token 开销和启动延迟都受不了。实际项目中我建议“默认用 Tool特殊场景才用 Resource”这个取舍后面会细讲。2.3 传输方式怎么选MCP 支持多种传输方式最常见的两种是 stdio 和 HTTP/SSE。stdio 模式是客户端直接启动一个本地进程通过标准输入输出通信。好处是配置简单、延迟低、不需要开端口适合本地开发工具。坏处是服务无法跨机器访问只能跑在本地。HTTP/SSE 模式是客户端通过网络访问远程服务适合团队共享、多客户端复用。坏处是要解决端口、鉴权、跨域一堆问题。我最终选择的是 stdio 模式。原因有三第一我的使用场景全在本地开发机第二六个站的内容同步后落在本地磁盘远程访问意义不大第三stdio 模式配置只有一行命令省心。如果你的场景是团队多人共用或者想在企业内部系统里统一接入那再考虑 HTTP/SSE。这个取舍没有绝对对错看场景。3. 整体架构与数据同步设计3.1 架构分层我的整体架构分成三层内容采集层、索引存储层、MCP 服务层。内容采集层负责对接六个站的原始数据源按各自特点拉取内容索引存储层把拉下来的内容清洗、格式化后写入本地 SQLite 数据库同时生成一个全文索引MCP 服务层读取这个数据库对外提供搜索和内容读取的工具。三层之间用定时任务串联采集完成、索引更新完成之后MCP 服务不需要重启因为它是按需查询数据库的。这个分层看起来简单但每一步都有坑。比如采集层不同站点的技术栈不同有的提供了 sitemap.xml有的没有有的支持 RSS有的只有 HTML 页面必须为每个站单独写适配器。再比如索引层博客和文档的正文格式五花八门有 HTML、有 Markdown、有 JSON清洗时要统一转成纯文本或者统一格式的 Markdown否则后面搜索时质量会很差。3.2 六类内容源怎么统一六个站的同步方式我分别做了处理整理成了一张表内容站原始形态同步方式更新频率产品官网HTML 页面sitemap 抓取 HTML 清洗每周API 文档JSON/OpenAPI直接调文档生成接口每次发版后技术博客RSS HTMLRSS 列表 正文抓取每日用户 FAQ结构化数据导出直接导入 CSV/SQLite每日更新日志RSS MarkdownRSS 仓库文件读取每次发版后运维 wikiMarkdown 文件定时拉取仓库变更每小时这里面最值得说的是 API 文档。它本来就有结构化的 OpenAPI 描述文件我直接把 JSON 解析后存成“接口名 参数 响应 错误码”的记录这样 AI 在回答问题时能精准命中某个接口的定义而不是靠全文匹配去猜。博客和 FAQ 则相反内容是长文本必须做全文索引才能保证搜索召回率。3.3 全文搜索与内容更新策略存储层我选了 SQLite原因是轻量、无外部依赖、单文件方便迁移。全文搜索没有自己写直接用了 SQLite 内置的 FTS5 扩展建虚拟表做索引查询语法也简单一个 MATCH 语句就能搞定。数据量不大的时候FTS5 完全够用如果哪天内容涨到几十万篇再考虑换成 Elasticsearch 或者 Meilisearch 也不迟。更新策略上我做了一个很关键的决定不搞实时同步而是用定时任务做增量更新。每个站记录一个“上次抓取时间”游标每次同步只拉取新增或变更的内容写入时用 URL 或者内容 ID 作为唯一键做 upsert。这样同步任务跑得很快也不会反复抓取旧页面对目标站点的压力也小。实测下来六个站全部增量同步一次大概三十秒放在每天凌晨跑一次完全够用。4. 服务端核心实现Python FastMCP 搭聚合服务4.1 为什么选了 Python 和 FastMCP工具链选择上我对比了一下 TypeScript SDK 和 Python SDK最后选了 Python。原因是我这次的核心逻辑是数据处理和文本清洗用 Python 的处理库写起来更顺手。框架层面用了 FastMCP它把 MCP 协议的细节封装得比较好定义工具就像写普通函数一样加个装饰器就行。有一点要提醒MCP 的 Python SDK 版本迭代很快API 变动也比较频繁我看到过不少教程里还在用旧版的server.tool()写法而新版建议直接创建mcp FastMCP(name)然后用mcp.tool()。建议以官方文档为准别直接抄老代码否则很容易遇到“照着写、跑不通”的情况。4.2 核心工具定义我这个聚合服务只暴露三个工具search_docs用来做全文搜索get_page用来读取单篇完整内容list_recent用来查看最近更新的内容。工具数量刻意控制在三个不是越少越好而是够用就行。工具多了反而会让模型在选择时犯迷糊调用链路也容易出错。核心代码大概是这样的框架from fastmcp import FastMCP import sqlite3 mcp FastMCP(all-in-one-docs) DB_PATH /path/to/docs_index.db def query_db(sql, params()): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row cur conn.execute(sql, params) rows cur.fetchall() conn.close() return rows mcp.tool() def search_docs(keyword: str, limit: int 10) - list[dict]: Search all site contents by keyword, return matched title and snippet. rows query_db( SELECT site, title, url, snippet FROM docs_fts JOIN docs ON docs.id docs_fts.id WHERE docs_fts MATCH ? ORDER BY rank LIMIT ? , (keyword, limit) ) return [dict(r) for r in rows] mcp.tool() def get_page(doc_id: int) - dict: Get full content of a document by its id. rows query_db(SELECT * FROM docs WHERE id ?, (doc_id,)) if rows: return dict(rows[0]) return {error: doc not found} mcp.tool() def list_recent(site: str , limit: int 20) - list[dict]: List recently updated docs, optionally filter by site name. sql SELECT site, title, url, updated_at FROM docs params () if site: sql WHERE site ? params (site,) sql ORDER BY updated_at DESC LIMIT ? rows query_db(sql, params (limit,)) return [dict(r) for r in rows] if __name__ __main__: mcp.run()这里面有个细节search_docs返回的是摘要和 URL而不是全文。这么设计是有意的。AI 在回答问题时往往不需要一口气读完所有文档先看摘要判断哪篇相关再按需调用get_page读全文Token 使用效率高很多。这就像你查资料时先看搜索结果页点进可能相关的链接后再细读而不是把所有网页一次性打印出来。4.3 三个客户端接入配置服务写好后接入客户端就很简单了。我主要在三个工具里用分别是 Claude Code、Cursor 和 Cherry Studio。Claude Code 的配置在项目下的.mcp.json里声明方式如下{ mcpServers: { all-in-one-docs: { command: python, args: [/path/to/mcp_server.py] } } }Cursor 的配置在 Settings - MCP 里同样指向本地命令。Cherry Studio 这类带界面的客户端更简单填 mcp server 的可执行文件路径或者命令行即可。如果走远程 HTTP/SSE 传输那配置里就是把command和args换成url比如{ mcpServers: { all-in-one-docs: { url: http://127.0.0.1:8000/mcp } } }注意像 Claude Code 里用 stdio 模式时command一定要写绝对路径并且最好先手动跑一次确认程序能正常启动。很多人的配置问题都出在这里程序本身没报错但环境变量没加载、Python 路径不对客户端启动时静默失败MCP 工具列表就一直是空的。5. 实操中的坑与排查实录5.1 我踩过的五个典型问题第一个坑是超时。默认情况下MCP 工具调用是有超时时间的尤其是一些客户端对 stdio 进程的首次启动时间卡得很严。我的 server 第一次加载时如果还要初始化数据库连接或者加载模型索引很容易超时。解决方式是把耗时操作放到 lazily 执行服务启动时只做轻量初始化真正查询时才连数据库。第二个坑是文本编码。HTML 清洗时如果没注意字符集中文内容很容易变成乱码。我最终统一在采集阶段把每个页面都转成 UTF-8并且在写入数据库前做了文本规范化把全角半角、空行这些细节都统一掉搜索准确率明显提升。第三个坑是 FTS5 的中文分词。SQLite 内置的 FTS5 默认分词器对中文支持很弱按整句切分搜索“错误码”匹配不到“错误码说明”这种文本。我的解法是用simple分词器配合 unicode61同时在写入时额外存了一份把中文按字和二元组切分的索引字段。虽然没有专业搜索引擎那么精细但对付日常查询够用了。第四个坑是工具返回内容过大。有一次get_page把整个运维 wiki 的长文一次性返回给模型直接导致客户端超时。后来我在返回前做了长度截断并且增加了max_length参数让模型自己决定要读多长问题就解决了。工具返回的内容是会被完整塞进上下文的这个体量必须控制。第五个坑是多个客户端共用同一个 stdio 配置导致端口和进程冲突。因为我的服务是无状态查询 SQLite所以问题不大但如果你在服务里加了缓存或内存状态多进程同时跑就会出奇怪的问题。要共享服务的话还是老老实实部署成 HTTP/SSE 模式。5.2 问题速查表症状可能原因解决办法MCP 工具列表为空进程启动失败或路径错误手动执行 command 验证检查 Python 环境变量调用工具超时首次初始化过慢延迟加载数据库连接轻量化启动中文乱码HTML 编码未统一统一转 UTF-8采集阶段做编码检测搜索召回率低中文分词不生效换 simple 分词器补充字/二元组索引返回内容过大撑爆上下文工具输出未截断增加 max_length 参数限制返回长度多客户端启动多次stdio 各自拉起独立进程接受本地多进程或改为 HTTP 共享服务这张表是我实际排查过程中的浓缩版前三个问题基本每个人都可能遇到尤其是新手刚上手 MCP 时工具列表空白是最常见的劝退场景。5.3 调试 MCP 的三个习惯调试 MCP 服务有个官方工具叫 MCP Inspector界面能让你手动调用工具、查看返回结果。强烈推荐在接入客户端之前先用它把每个工具都调一遍确认输入输出都正常再接入否则出了问题你都不知道是客户端配置的问题还是服务本身的问题。第二个习惯是给采集和服务分别打日志。采集程序记录每次同步的页面数和耗时服务记录每次工具调用的参数和返回大小。出现问题时先看是数据没更新还是服务查询有问题能省去一大半排查时间。第三个习惯是模拟模型的实际调用方式。接入客户端之后不要一上来就问很复杂的问题先试“最近更新了哪些内容”这种简单请求确认链路通了再逐步加大对全文搜索和多步调用的测试。写在最后这个项目做完之后我最大的感受是MCP 的价值不在于“多装几个插件”而在于把数据和工具的组织方式统一起来。我过去是“每个站一个入口”现在是“一个入口管六个站的全部内容”效率提升非常明显。单就配置维护这块我每个月省下来的时间少说也有一个下午更别提每次查资料不用再开六七个标签页了。如果你也想做类似的事我的建议是从小处开始先挑两个内容结构最规范的站做通一遍全流程再把剩下的站逐个加进去。一次贪多很容易被各种小问题淹没信心。另外采集频率一定不要设置太高对目标站点要有基本的尊重否则你同步一次对方服务器报警一次合作就没得谈了。最后分享一个小技巧这个聚合 MCP 的架构其实不止能用在我这种文档站场景。你只要把采集层换成任意数据源——比如数据库表、工单系统、代码仓库里的 README——就能把任意知识域暴露给 AI。我下一步打算把客服的工单历史也接进去让 AI 写回复时可以参考历史解决方案。这个路子值得继续折腾。