news 2026/9/8 8:35:22

用MCP Server统一六个站点:AI自然语言查询实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用MCP Server统一六个站点:AI自然语言查询实战

手上一堆站点的日子,只有自己知道有多酸爽。我同时维护着六个不同类型的网站——技术博客、文档中心、读书笔记、数据统计、摄影作品集、导航收藏夹。平时处理这些小站还能靠肌肉记忆,但每次想让人工智能帮我干点正事,比如"把博客里近半年提到过Rust的文章整理一份清单"、"对比一下文档站和博客站最近更新节奏",就尴尬了——AI再聪明,它看不到我这些站的数据。我只能手动复制粘贴、导出、汇总,再喂给它。来回折腾一晚上,效率低到怀疑人生。

后来我把主意打到了 MCP 上。如果你还没听说过这个词,简单说,MCP(Model Context Protocol)是一套开放协议,专门用来给 AI 应用接外部数据和工具,相当于给只会聊天的大模型配上一个工具箱。我花了两天时间,把所有站点内容统一收敛到一个 MCP Server 里,现在不管是 Claude Desktop、Cursor 还是其他支持 MCP 的客户端,都能直接以自然语言去查询我这六个站的实时内容。这篇文章就从头到尾复盘一下我是怎么做的,包括整体思路、代码实现、数据接入方式和过程中踩到的一堆坑,希望给同样在折腾 MCP 的朋友一条可以照抄的路径。

1. 先说我为什么折腾:六个站的数据切分场景

很多人一上来就关心 MCP 的协议细节、SDK 用法,但我觉得先搞明白"你到底想解决什么问题"比什么都重要。我的问题非常具体:六个站,数据分散,格式各异,AI 却对它们一无所知。

1.1 六个站点的情况,比我预想的更乱

先列一下我手上的站,你们感受一下这种分裂感:

站点类型数据形态存放位置
技术博客WordPressMySQL 数据库云服务器
文档中心VitePress 构建Markdown 源文件服务器目录
读书笔记自建小应用SQLite 数据库服务器目录
数据统计站内部 API 服务JSON 输出独立服务端口
摄影作品集静态站点图片 + JSON 索引Nginx 目录
导航收藏夹轻量应用JSON 数据文件服务器目录

这六个站之前想让人工智能读取,几乎只能靠"导出再导入"的笨办法。比如博客站是 MySQL,我得先查数据库,再整理成 Markdown;文档站倒是源文件在服务器上,但文件一堆,AI 也没法一次性全读;摄影作品集没有数据库,纯靠 JSON 索引;导航站更直接,就是一个 JSON 文件每天手动更新。

还有一个要命的点:这些站之间没有任何关联。博客里的一篇文章,可能对应的知识点在文档中心也有一份说明,摄影站里的某张照片又在博客里被引用过。数据孤岛一个个矗在那儿,靠人力串成线太累了。

1.2 MCP 到底解决了什么,我为什么没有选别的方案

在动手之前,我也认真考虑过别的路子。

第一种是用 OpenAI Function Calling 或者直接调各家大模型 API,把每个站点的数据封装成函数给模型。这个方案可行,但它绑定特定平台,今天写的是 OpenAI 风格,明天换 Claude 又要重写一遍。

第二种是做一个统一的 HTTP API 服务,然后在对话里让模型去调这个 API。问题在于我需要把 API 的鉴权、文档、参数示例全部塞到上下文里,且不同客户端支持程度参差不齐,非常别扭。

第三种就是 MCP。它的核心思路是:用一套标准协议,把"AI 应用"和"数据/工具"解耦。我只需要写一个 MCP Server,暴露几个 Tool,然后各种支持 MCP 的客户端(Claude Desktop、Cursor、Trae、Cherry Studio)都能自动发现并调用这些工具,不需要我针对每个平台写集成代码。

打个比方:如果大模型是大脑,API 是胳膊腿,那 MCP 就是神经接口标准。以前每个平台都有自己的神经接法,搞得我写一套代码只能给一个平台用;现在大家都在往 MCP 这个标准上靠,我只要把这个"神经接口"造好,谁都能接。

所以我最后选了 MCP,理由就三条:标准统一、生态好、开发成本可控。后面所有工作,都围绕"写一个 MCP Server,把六站数据藏在这个 Server 后面"来做。

2. 六站糅进一个 Server:整体设计与选型思路

想一次性把六个站都纳入一个 MCP Server,设计阶段就要把"边界"想清楚,不然后面越写越乱。

2.1 先做减法:不可能一股脑全暴露

很多人第一次做 MCP 容易犯一个毛病:把所有功能都变成 Tool,搞出几十个工具,结果 AI 在选工具的时候反而犯迷糊,或者描述冲突,调用质量直线下降。

我当时给自己定了个原则:以"内容查询"和"状态查询"为核心。所有站点,只向 AI 暴露三种能力。第一是搜:在所有站里全局搜索某个关键词;第二是列:查看某个站最新发布的 N 条内容;第三是读:根据路径或者 ID 读取某条具体内容。这三个能力覆盖了我日常对话式问数据的绝大部分场景。

至于写操作、删除操作、配置修改,我全部砍掉。不是做不到,而是没必要。MCP 的定位是"让 AI 读取数据、辅助思考",不是让它去管理一个 CMS。

2.2 语言与框架选型:为什么选 Python 的 FastMCP

现在 MCP 的官方 SDK 有 Python 和 TypeScript 两套,我的选择是 Python,理由不复杂:我这些站点的数据处理脚本本来就有很多是 Python 写的,复用逻辑最方便。

在 Python 生态里,我推荐直接用 FastMCP 这个封装库。它基于官方 SDK,但把大量样板代码省掉了,定义 Tool、加载配置、启动服务都非常简洁。官方推荐的uv run方式也特别好用,不需要手动管理虚拟环境依赖。

一个非常核心的选型判断是:我的 MCP Server 需要跑在本地服务器上,和六个站同机部署,所以优先用 stdio 模式,而不是 HTTP/SSE 模式。stdio 模式对于 Claude Desktop、Cursor 这类本地客户端来说是最省心的,进程由客户端拉起,输出走标准输入输出,几乎没有网络层面的问题。SSE 模式适合跨机器部署,但那是进阶玩法,我后头会提一下。

2.3 工具箱设计:六个站怎么抽象成统一能力

下面这张表,就是我最终定下来的 Tool 清单和对应职责:

工具名参数返回内容
search_all_siteskeyword在全部站点中检索标题和正文,返回命中列表
get_recent_postssite, limit返回指定站点最近内容列表
get_page_contentsite, path_or_id读取指定页面/文章的完整正文
get_site_statssite返回站点基础统计,如文章数、最近更新时间
get_site_health检查六个站的连通状态和配置是否正常

这个工具设计有一个关键点:search_all_sites不是把六个数据源分别做成六个搜索工具,而是做成一个"全局搜索"。AI 只需要调一个工具,就能拿到所有站点的相关结果,大大降低了模型的决策负担。

get_site_health是我后来才加的。因为六个站的数据源一旦有一个连不上,AI 返回的结果就会有误导性,所以我特意暴露了一个状态查询工具,让 AI 有能力检查"哪个站的数据现在是可用的"。这种做法在真实场景里很重要,你可以在自己的 Server 里借鉴。

3. 动手搭一个 MCP Server:代码级实操

设计搞定,接下来就是真正动手。这一部分我会按实操流程走一遍,包含完整的代码结构,以及怎么接到 Claude Desktop 和 Cursor 上。

3.1 环境准备:Python 环境与依赖安装

我的服务器是 Ubuntu 22.04,Python 版本 3.11。如果你用 Windows 或者 macOS,原理一样,命令稍微改一下就行。

第一步是安装uv。使用 Python 做 MCP 开发,我建议大家直接用 uv 管理环境和依赖,比手搓 venv + pip 顺滑得多:

curl -LsSf https://astral.sh/uv/install.sh | sh

然后创建项目目录并初始化:

mkdir -p ~/mcp/six-sites-mcp cd ~/mcp/six-sites-mcp uv init --bare uv add "mcp[cli]" aiosqlite pymysql requests

这里我一次装上几个关键依赖:

  • mcp[cli]:MCP 官方 Python SDK,加 CLI 扩展方便调试。
  • aiosqlite:异步访问 SQLite,避免数据库查询阻塞事件循环。
  • pymysql:访问 MySQL(博客站)。
  • requests:调内部 API 服务。

另外我把httpx留着备用,虽然最终没用上,但如果你接外部 HTTP 接口,httpx是比requests更现代的选项。

3.2 主程序:一个 FastMCP Server 的核心骨架

项目里我建了一个server.py作为入口。先看骨架:

import json import os import sqlite3 import asyncio from datetime import datetime from pathlib import Path import aiosqlite import requests from mcp.server.fastmcp import FastMCP mcp = FastMCP( "six-sites-mcp", instructions="你有权访问多个站点的内容。" "站点标识: blog(技术博客), docs(文档中心), notes(读书笔记), " "stats(数据统计), gallery(摄影作品), links(导航收藏)。" "当用户询问最新的内容或搜索某个关键词时,优先调用对应工具。" ) SITES = { "blog": { "name": "技术博客", "type": "mysql", "config": { "host": "127.0.0.1", "port": 3306, "user": "mcp_reader", "password": os.getenv("BLOG_DB_PASSWORD", ""), "database": "blog_db", }, }, "docs": { "name": "文档中心", "type": "markdown", "base_path": "/srv/docs", }, "notes": { "name": "读书笔记", "type": "sqlite", "path": "/srv/data/notes.db", }, "stats": { "name": "数据统计", "type": "api", "base_url": "http://127.0.0.1:8800/api", }, "gallery": { "name": "摄影作品", "type": "json", "path": "/srv/data/gallery/index.json", }, "links": { "name": "导航收藏", "type": "json", "path": "/srv/data/links/links.json", }, }

写到这里我顺便说一句:数据库密码不要硬编码进代码里。我最开始图省事写在配置里,结果一不小心把脚本同步到 Git 仓库,吓得我赶紧改密码。后来统一改成从环境变量里读,这才是安全的做法。

3.3 核心工具实现:全局搜索、最新列表、内容详情

接着写真正的 Tool 实现。FastMCP 的装饰器方式非常直观:

@mcp.tool() async def search_all_sites(keyword: str) -> str: """ 在技术博客、文档中心、读书笔记、摄影作品、导航收藏等所有站点内容中搜索关键词。 参数 keyword: 搜索关键词。 返回 JSON 数组,每个元素包含 site、title、url、snippet。 """ results = [] # 1. SQLite 笔记站 try: async with aiosqlite.connect(SITES["notes"]["path"]) as db: async with db.execute( "SELECT title, url, content FROM notes WHERE title LIKE ? OR content LIKE ? LIMIT 10", (f"%{keyword}%", f"%{keyword}%"), ) as cursor: async for row in cursor: title, url, content = row results.append({ "site": "notes", "title": title, "url": url, "snippet": content[:150], }) except Exception as exc: results.append({"site": "notes", "error": str(exc)}) # 2. Markdown 文档站 docs_root = Path(SITES["docs"]["base_path"]) if docs_root.exists(): for md in docs_root.rglob("*.md"): if keyword.lower() in md.stem.lower(): results.append({ "site": "docs", "title": md.stem, "url": f"/docs/{md.relative_to(docs_root)}", "snippet": "标题命中", }) else: # 简单读取前500字节,匹配关键词就收入结果 try: head = md.read_text(encoding="utf-8")[:500] if keyword.lower() in head.lower(): results.append({ "site": "docs", "title": md.stem, "url": f"/docs/{md.relative_to(docs_root)}", "snippet": head[:150], }) except Exception: continue # 3. JSON 文件站(摄影、导航) for site_key in ["gallery", "links"]: try: data = json.loads(Path(SITES[site_key]["path"]).read_text(encoding="utf-8")) for item in data: if keyword.lower() in json.dumps(item, ensure_ascii=False).lower(): results.append({ "site": site_key, "title": item.get("title", item.get("name", "")), "url": item.get("url", ""), "snippet": str(item)[:150], }) except Exception as exc: results.append({"site": site_key, "error": str(exc)}) # 4. 博客站(MySQL)使用独立函数查询,见下文 blog_results = await _search_blog(keyword) results.extend(blog_results) return json.dumps(results, ensure_ascii=False, indent=2)

看到这里你应该能理解我的思路:每个数据源尽量独立 try-except,这样即使某一路数据源挂了,也不会整个 Tool 报错。

MySQL 的查询我单独抽了一个函数,因为要处理异步连接池,写起来相对复杂:

async def _search_blog(keyword: str): import pymysql cfg = SITES["blog"]["config"] try: conn = pymysql.connect( host=cfg["host"], port=cfg["port"], user=cfg["user"], password=cfg["password"], database=cfg["database"], charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor, ) with conn.cursor() as cur: cur.execute( "SELECT post_title, guid, post_content FROM wp_posts " "WHERE post_status='publish' AND post_date <= NOW() " "AND (post_title LIKE %s OR post_content LIKE %s) " "ORDER BY post_date DESC LIMIT 10", (f"%{keyword}%", f"%{keyword}%"), ) rows = cur.fetchall() conn.close() results = [] for row in rows: results.append({ "site": "blog", "title": row["post_title"], "url": row["guid"], "snippet": row["post_content"][:150], }) return results except Exception as exc: return [{"site": "blog", "error": str(exc)}]

注意我这里用的 MySQL 查询是纯同步的pymysql,放在 async 函数里会阻塞事件循环。在真实场景中,更优雅的方案是用aiomysql或者把数据库操作丢到线程池里。对我的场景来说,这个 Server 只有我自己用,并发极低,同步阻塞影响可以忽略。但如果你想部署给团队用,一定要换成真正的异步驱动。

3.4 其他工具:最近列表、内容详情、站点状态

搜索工具是最核心的,但只有它还不够。再补充三个工具:

@mcp.tool() async def get_recent_posts(site: str, limit: int = 10) -> str: """ 获取指定站点最近发布的内容列表。 参数 site: 站点标识,可选 blog/docs/notes/stats/gallery/links。 参数 limit: 返回条数,默认10,最大30。 """ limit = max(1, min(limit, 30)) result = [] if site == "notes": async with aiosqlite.connect(SITES["notes"]["path"]) as db: async with db.execute( "SELECT title, url, created_at FROM notes ORDER BY created_at DESC LIMIT ?", (limit,), ) as cursor: async for row in cursor: result.append({"title": row[0], "url": row[1], "date": row[2]}) elif site == "docs": docs_root = Path(SITES["docs"]["base_path"]) for md in sorted(docs_root.rglob("*.md"), key=lambda p: p.stat().st_mtime, reverse=True)[:limit]: result.append({"title": md.stem, "url": f"/docs/{md.relative_to(docs_root)}", "mtime": datetime.fromtimestamp(md.stat().st_mtime).isoformat()}) elif site == "blog": rows = await _search_blog("") # 空关键词也返回,但这里建议走单独函数 result = rows[:limit] # 其他站点类似,略 return json.dumps({"site": site, "items": result}, ensure_ascii=False, indent=2)

说实话这里我留了一个偷懒的地方:博客站"最近列表"直接复用_search_blog(""),虽然能跑,但语义不太对,而且没有排序限制。如果按正式项目的标准,应该单独写一个_get_recent_blog(limit)方法。我把它当作一个待优化的点,后续会改。

get_page_content的实现也类似,就是根据 path 或 ID 从对应数据源读取完整内容,这里不贴全部代码了。重点要强调的是:返回给 AI 的内容一定要截断,单篇文章最长我限制在 5000 字。原因很简单,模型上下文窗口有限,塞一篇两万字的长文进去,让它自己找重点,既浪费 token 又影响质量。截断之后,如果 AI 觉得需要更多内容,它会继续调用工具来补充获取后续部分,这才是合理的交互方式。

最后是get_site_health

@mcp.tool() async def get_site_health() -> str: """检查六个站点数据源是否可访问,返回各站点状态字典。""" status = {} for key, site in SITES.items(): try: if site["type"] == "sqlite": async with aiosqlite.connect(site["path"]) as db: await db.execute("SELECT 1") status[key] = "ok" elif site["type"] == "json": path = Path(site["path"]) status[key] = "ok" if path.exists() else "missing" elif site["type"] == "markdown": path = Path(site["base_path"]) status[key] = "ok" if path.exists() else "missing" elif site["type"] == "mysql": # 同步调用,真实场景可异步化 import pymysql conn = pymysql.connect(host=site["config"]["host"], port=site["config"]["port"], user=site["config"]["user"], password=site["config"]["password"], database=site["config"]["database"]) conn.close() status[key] = "ok" elif site["type"] == "api": resp = requests.get(site["base_url"], timeout=3) status[key] = "ok" if resp.status_code == 200 else f"http_{resp.status_code}" except Exception as exc: status[key] = f"error: {exc}" return json.dumps(status, ensure_ascii=False, indent=2)

3.5 把 Server 接入 Claude Desktop 和 Cursor

Server 写完,本地可以用标准 MCP Inspector 来调试。在项目目录执行:

uv run mcp dev server.py

它会起一个本地调试面板,我可以直接测试各个工具。等测试通过,再正式配置到客户端里。

Claude Desktop 的配置在claude_desktop_config.json,不同系统位置不一样,macOS 是在~/Library/Application Support/Claude/。配置如下:

{ "mcpServers": { "six-sites": { "command": "uv", "args": [ "run", "--directory", "/home/me/mcp/six-sites-mcp", "server.py" ] } } }

然后重启 Claude Desktop,它就会自动拉起这个 MCP Server。

Cursor 的配置略有不同,项目根目录下建.cursor/mcp.json

{ "mcpServers": { "six-sites": { "command": "uv", "args": [ "run", "--directory", "/home/me/mcp/six-sites-mcp", "server.py" ] } } }

Cursor 的 MCP 功能在 Settings → Features 里能看到,连接成功后会列出所有工具。这里有个细节:Cursor 的 MCP 默认是 project-scoped,也就是每个项目单独配置。如果我想在多个项目里复用,可以把配置放到用户级 MCP 文件里,路径通常可以通过cursor open .cursor/mcp.json打开。

4. 六站数据接入:不同类型数据源怎么喂给AI

设计是骨架,数据接入是血肉。六个站的数据形态各不相同,我一个个说,每个都能提炼出通用经验。

4.1 数据库类:MySQL 和 SQLite

博客站是最重度的数据源,跑了七八年的 WordPress,表结构是标准的 wp_posts。接入 MCP 时,我坚持两个原则:只读账号、参数化查询。

只读账号这个很多人会忽略。我当时在 MySQL 里给 MCP 单独建了一个账号,只授予 SELECT 权限:

CREATE USER 'mcp_reader'@'127.0.0.1' IDENTIFIED BY 'your_password_here'; GRANT SELECT ON blog_db.* TO 'mcp_reader'@'127.0.0.1'; FLUSH PRIVILEGES;

这样即使 MCP Server 被注入或者被人发现了连接信息,最坏情况也只是数据泄露,不至于把表给删了。MCP 这边的输入是自然语言解析出来的,虽然我控制不了模型每次都生成规规矩矩的 SQL,但我可以控制它的权限边界。

SQLite 这边就简单多了,文件数据库天然只读。注意一点:打开 SQLite 时,官方驱动默认会尝试建journal文件,如果目录没有写权限会报错。解决方法是在连接时设置mode=ro

async with aiosqlite.connect(f"file:{path}?mode=ro", uri=True) as db:

4.2 静态文件类:Markdown 和 JSON

文档中心是 VitePress 生成的,源文件是几百个 Markdown 文件。这里最大的挑战不是读取,而是性能。

我之前天真地想过"把所有 Markdown 文件读一遍,作为系统提示塞给 AI",试了一次,直接被干蒙了。一是 token 量爆炸,二是每次启动会话都慢得离谱。MCP 方案就好很多:文件不预加载,只有在被搜索或被指定读取时才实时访问。

实现上要注意文件路径的安全:get_page_content(site, path)的 path 参数不能直接和根目录拼接,否则 AI 或者恶意用户传一个../../etc/passwd就能读任意文件。我做了规范化处理:

def safe_join(root: Path, user_path: str) -> Path | None: candidate = (root / user_path).resolve() if not str(candidate).startswith(str(root.resolve())): return None return candidate

JSON 文件类(摄影站、导航站)更简单,读文件、解析、按关键词过滤即可。但我额外做了一件事:为这两个站各自写了一个专门的get_gallery_photo(photo_id)get_link_by_tag(tag)工具,因为纯靠全局搜索,AI 难以回答"导航站里有哪些 Python 相关链接"这类按分类筛选的问题。这说明一个道理:工具不是越少越好,而是该拆的拆、该合的合,要根据实际使用场景去增加工具。

4.3 API 型站点:数据统计服务

数据统计服务本身是一个 Flask 应用,跑在 8800 端口,暴露了一些 JSON 接口。接入 MCP 时,我做了三层防护:

第一是超时,所有对外 HTTP 请求必设 timeout,我统一用 5 秒。第二是异常兜底,接口挂了就返回错误信息,让 AI 知道"不是我不查,是数据源没响应"。第三是接口地址规范化,MCP Server 与统计服务在同一台机器,我直接用127.0.0.1,不走公网,既快又省心。

4.4 统一返回格式:为什么我坚持 JSON

MCP 的工具返回值可以是任意文本,但我坚持所有工具返回 JSON 字符串。原因有两点。

第一,JSON 本身有结构,AI 解析结构的速度比解析自由文本快,尤其是列表信息,用了 JSON 就不容易漏项。第二,调试方便。我在 MCP Inspector 里看工具返回值时,格式化的 JSON 一眼就能判断结果是正常还是异常。你可以在自己的实践中试一下短文本和 JSON 的表现差异,我个人实测下来结构化的输出让模型的回答准确率高出一截。

另外一个细节是,所有返回数据都尽量加上site字段,标明来源站点。这样 AI 在回答时就知道"这篇文章来自文档中心",而不是含糊地说"你的站点里有",用户体验完全不一样。

5. 接入客户端、跑通效果与性能优化

工具和服务都跑起来了,接下来就是实际使用效果的验证和调优阶段。

5.1 实际对话效果:AI 是怎么使用这些工具的

我举一个实际发生的例子。我在 Claude Desktop 里问:"最近三个月博客和文档中心都更新了哪些内容?"

人工智能收到问题后,会先调用get_site_health确认数据源正常,然后并行调用get_recent_posts(site='blog')get_recent_posts(site='docs'),把两个结果汇总成对比表格给我。整个过程中,我没有写一行 SQL,没有打开网站后台,只是在对话框里用自然语言描述需求。

这个体验的改变是本质性的。以前我是"先想清楚数据在哪,再把数据搬到 AI 面前";现在是"我给 AI 一套工具箱,它自己知道去哪拿数据"。

5.2 性能瓶颈:缓存、并发与超时

跑了一周之后,我做了几项性能优化。

第一是给搜索结果加缓存。search_all_sites里对 Markdown 文件的关键词扫描是最耗时的操作。我加了一个简单的 TTL 缓存,同一个关键词 5 分钟之内不重复扫描文件系统:

_cache = {} _CACHE_TTL = 300 def cached_search(keyword: str): key = keyword.lower().strip() now = time.time() if key in _cache and now - _cache[key]["time"] < _CACHE_TTL: return _cache[key]["data"] # 原搜索逻辑... result = do_search(keyword) _cache[key] = {"time": now, "data": result} return result

这个缓存对重复问题尤其有效。比如 AI 在处理一个长任务时,经常会多次调用同一个关键词搜索,有了缓存,直接命中,响应速度从几秒降到毫秒级。

第二是给所有外部调用设统一的超时阈值。数据库查询我默认 5 秒,HTTP 请求 5 秒,文件读取 3 秒。超时不是简单异常,而是要返回一个 AI 能看懂的说明,比如{"site": "stats", "error": "timeout after 5s"},否则 AI 可能会一本正经地编造结果。

第三是并行化改造。我的search_all_sites最初是串行扫描六个数据源,全部跑完要 3-4 秒。后来改成用asyncio.gather并行扫描,总耗时就降到 1 秒左右。在 MCP 场景里,工具响应越快,模型就越愿意多调几次工具,整体体验是正向循环。

5.3 权限与安全:MCP Server 部署的红线

虽然这个项目主要用于个人,我还是把安全规则总结出来,希望你能引以为戒:

  • 数据库账号一律只读。
  • Server 只监听本地,stdio 模式天然只对拉起它的客户端开放。
  • 如果要用 SSH/HTTP 远程访问,请务必加认证,别裸奔公网。
  • 所有外部依赖的密码、密钥,统一走环境变量,不进代码库。
  • 工具返回内容做长度限制,避免恶意输入导致磁盘耗尽。

我在实际部署中只用了 stdio 模式,所以没有接触远程认证问题。但如果你要跨机器访问,MCP 官方文档里有关于 OAuth 的建议,这个后续可以单独展开讲。

5.4 其他支持 MCP 的客户端:我的适配情况

Claude Desktop 之后,我也顺手试了 Cursor、Cherry Studio 和 Trae。

Cursor 体验最出乎意料,它在 Agent 模式里对 MCP 工具的调用非常积极,写代码时如果需要了解项目文档站的内容,它会自己触发search_all_sites。Cherry Studio 是国产软件,对 MCP 的支持也在快速跟进,界面里就能直接看到工具列表和调用日志,调试很直观。

我个人的结论是:不要被单一客户端绑架。MCP 的核心价值就是"一次开发,全端通用"。你只要按标准写 Server,将来无论哪个客户端支持 MCP,你都能无缝切换。

6. 实测踩坑记录与排查速查表

最后这部分,我把实打实踩过的坑和排查思路整理一遍。很多问题官网文档不会写,只有真跑一遍才会遇到。

6.1 世界观级的问题:MCP Server 连不上

最典型的现象是 Claude Desktop 里看不到工具列表,或者对话时提示 MCP 连接失败。

排查思路是分层的。第一步先看日志。Claude Desktop 的日志在~/Library/Logs/Claude/mcp*.log,元凶多半是启动命令不对。第二步,用命令行手动执行:

uv run --directory /home/me/mcp/six-sites-mcp server.py

如果执行立刻报错,说明是代码问题,把 Python 报错解决了就好了。如果执行后一直阻塞,说明 Server 在正常运行,问题就出在配置路径或权限上。

最坑的一点是:command字段如果写的是uv,而 Claude Desktop 启动时的 PATH 环境变量里没有uv,就会连不上。我之前就因为这个卡了半小时。解决办法是把uv替换为绝对路径,比如/home/me/.local/bin/uv,或者直接把command改成 Python 解释器的绝对路径。

6.2 工具调用报错:权限不足与磁盘路径

另一个高频问题是工具本身不可用。比如在get_page_content里读文件时遇到 PermissionError,大概率是运行 MCP Server 的系统用户没有权限访问站点目录。

你要记住:MCP Server 是客户端进程的子进程,它的权限继承自启动它的客户端。如果 Claude Desktop 是从图形界面启动的,它的用户权限一般就是你登录用户;但有些系统上服务型应用可能运行在不一样的身份下。排查方法很简单,在 Server 代码里加一个启动时的自检函数,把当前用户、目录、权限打印到日志里。

6.3 搜索速度慢:全站扫描不能每次都跑

这个我在前面提过 TTL 缓存了,这里再补充一个思路:为文档中心建立索引文件。

VitePress 有search插件,本身会生成索引。但我的场景是 MCP 直接读源文件,所以我自己写了一个简单脚本,把 Markdown 文件解析成"标题—路径—更新时间—摘要"的结构,存成docs_index.json,每天定时重建。MCP Server 搜索的时候直接扫这个 JSON 索引,速度比rglob快一个数量级。

如果你想复现,思路是:搜索请求 → 查索引内存副本 → 命中后读取完整 Markdown 文件返回给 AI。索引是薄层,完整内容按需读取,这个模式在静态文档场景下非常高效。

6.4 其他常见问题速查表

问题现象可能原因解决方法
工具返回全是空数组数据库连接失败被异常兜底查看 Server 日志中的 error 字段
AI 不调用全局搜索,只调单站工具工具描述不够清晰优化工具描述,明确说明使用场景
MCP Inspector 正常,Claude Desktop 连不上路径或环境变量不一致把 command 改成绝对路径并检查日志
查询结果太多,AI 整理不全返回条数限制过大默认 limit 限制在 10 以内
内存占用持续升高大文件读取或缓存无限增长为缓存加 TTL 和大小上限
用了异步库还是卡顿可能存在同步阻塞混入全面排查是否有requests等同步调用
MySQL 中文乱码连接字符集配置错误连接参数加charset='utf8mb4'
MCP Server 被拉起多次多个客户端共用同名 Server每个客户端独立配置,或加进程锁

还有一个实用技巧是给search_all_sites的结果加上去重。因为同一篇文章可能同时在博客站和文档中心存在,直接返回会让 AI 误以为有多份不同内容。我加了一个(title normalized + source site)的去重键,实测对回答质量提升明显。

6.5 我在实际操作中养成的三个习惯

到这里,核心内容基本讲完了,但还有三个我到现在都在用的习惯,值得单独写一下。

第一个习惯:每次修改 Server 代码,先用uv run mcp dev server.py快速验证一遍所有工具,再重启客户端。MCP 的工具定义一旦变了,旧客户端里的工具描述还是旧的,很容易出现"工具列表有,但调用报错"的假象。

第二个习惯:在 Server 的instructions里做站点描述。我后来发现,光靠工具描述还不够,把六个站点的类型、用途、数据时效、更新频率写清楚,AI 在回答时会做得更好。比如"导航收藏夹每天更新,但数据量少;摄影作品集每月更新,数据稳定",这些信息直接影响模型对结果的信任程度。

第三个习惯:保持 Server 代码版本化。我把它单独放了一个 Git 仓库,每次改完都提交,遇到问题可以快速回滚。MCP Server 看着小,但迭代速度其实很快,没有版本管理很容易搞成一团乱麻。

最后说一点个人感受

跑通这套 MCP 之后的第二天,我发现自己的使用习惯彻底变了。以前查站里有没有写过某个主题,第一反应是打开后台搜索;现在第一反应是打开聊天框,直接问"帮我查一下哪个站提到过某某关键词"。这个转变不是我刻意训练的,而是工具好用之后自然发生的。

如果你手上也有几个站、几个内容源,或者哪怕是几百个本地文档,MCP 都值得花点时间研究。从一个 Min Server 开始,只暴露一两个工具,跑通链路,再慢慢扩充,比一上来就规划十来个工具要顺得多。我认为这套"AI 可读的数据接入层"以后会是每个内容创作者和独立站长的基础设施之一,现在早点把基础打好,后面接什么工具都不慌。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 8:35:21

Qwen3私有化部署与多模态数字人全栈开发实战教程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 8:34:53

MPC原型到产品化落地:求解器、实时性与鲁棒性实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 8:34:15

人脸识别毕业设计实战:从LBPH原理到OpenCV系统开发

简介&#xff1a;一套面向毕业设计的人脸识别系统完整项目代码&#xff0c;基于百度云AI接口实现人脸检测、特征提取、人脸比对与活体检测&#xff0c;可应用于安全监控、身份验证、考勤打卡等场景&#xff0c;适合计算机、人工智能相关专业的学生及开发者参考。资源共841个文件…

作者头像 李华
网站建设 2026/9/8 8:33:47

从零实现简单线性回归:从损失函数到梯度下降的Python实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 8:33:24

哼唱生成音乐全流程指南:从输入优化到批量处理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 8:32:27

Spring Boot家装项目管理系统:从需求到远程调试的完整实战

做装修公司信息化这行快十年&#xff0c;见过太多工地上“人盯人”的管理方式了。项目经理翻着手机找聊天记录报进度&#xff0c;老板想看一眼各工地资金占用情况得等财务月底拉Excel&#xff0c;客户三天两头问“我家装到哪一步了”却得不到准确答复——这些都是装修公司项目管…

作者头像 李华