Skip to main content
Glama
mmorrisj
by mmorrisj

corpus-mcp

一个 MCP 服务器,让智能体可以对文档目录进行关键词搜索。指向一个文件夹即可使用——无需下载模型、无需 API 密钥、无需 GPU、无需并行运行向量数据库。唯一依赖:MCP SDK。

pip install -e .
corpus-mcp --root ./docs serve

有趣的部分不在于检索,而在于工具设计:智能体实际上能用搜索工具做什么,以及是什么让一个工具可用,而不是成为上下文窗口的焚化炉。


十秒试用

$ make demo
1. reference/glossary.md  (score 1.973, f700ededcfdd:0)
   # Glossary

   **Extraction** — the process of dissolving soluble compounds out of ground
   coffee. Under-extraction tastes sour and thin; over-extraction tastes bitter …

2. guides/brewing.md  (score 1.774, 71c6f092dbcb:0)
   # Pour-over brewing
   …

那个查询是 "why does my coffee taste sour"。文档说的是 tastes,查询说的是 taste,而真正解答问题的术语条目排在第一位。这两点都是有意为之;详见下文。

工具

工具

用途

search(query, limit, snippet_chars)

按相关性排序的段落片段,简短且以匹配为中心,每个片段带 chunk_id

fetch(chunk_id, context_chunks)

某一个片段的完整文本及其相邻片段

list_sources(limit)

已索引的内容,以及每个文档的大小

文档还作为 MCP 资源 暴露在 corpus://<relative-path>

值得商榷的设计决策

Search 和 fetch 是独立的工具。 一个直接返回完整块的 search 写起来更简单,但用起来糟糕得多:十条各 1200 字符的结果,在智能体决定想要哪一条之前,就已经消耗了大半上下文窗口。因此 search 返回片段——足以初步筛选——而 fetch 按需展开选中的结果。智能体只在自己认为值得的地方为细节付费。

片段以匹配为中心,而不是块的开头。 返回前 N 个字符的做法经常失败,因为匹配的句子通常在中部:智能体看到一段无关的前言,要么放弃一个好结果,要么为了弄清楚而抓取全部内容。片段窗口的选择会尽量覆盖尽可能多的查询词出现位置。

所有限制都在服务端强制实施。 工具输出直接进入上下文窗口,所以无界工具就是对调用它的东西进行拒绝服务攻击。调用方要求 10000 条结果正是上限存在要应对的情况,因此限制是被强制执行的,而不是被信任的。当输出被截断时,响应会明确说明,这样智能体可以缩小查询范围,而不是假设自己看到了全部内容。

空结果会自我解释。 一个光秃秃的空列表就是死胡同。响应会报告存在多少个块和文档,从而区分“你的查询没命中”和“什么都没索引”——这两种情况的下一步不同。

过期标识符是预期结果,而不是错误。 文档被编辑时块 id 会变化,因此长会话中较早出现的 id 可能失效。fetch 会明确说明这一点,并告诉智能体重新搜索。

块合并时会去除重叠。 块之间有重叠是为了不让任何段落被边界切断,但把重叠交还给智能体意味着它会读到重复的句子,并可能把重复当作强调。块携带绝对偏移量,因此重叠是按位置而不是按字符串匹配去除的。

使用 BM25,而非嵌入。 对于智能体在浏览它已经有所了解的语料库时发出的关键词式查询,词法检索足够强,而且它具备在智能体循环中最关键的性质:快速,且永远不会悄悄花钱。语义搜索是有价值的补充,而不是让这个东西有用的前提。

轻度词干化,而非真正的词干分析器。 复数和常见动词词尾会被归并,使 tastes 能匹配 taste。完整的 Porter 实现有上百行代码,也是一个维护面,而且它的长尾(operationaloper)在短查询上很可能弊大于利。索引和查询共用同一个分词器,因为两者之间的任何分歧都会悄悄损害召回率。

安全

服务器指向一个根目录,并且永远不会读取它之外的内容。这比看起来更重要:工具参数来自模型输出,因此文档标识符是不可信输入,../../.ssh/id_rsa 是一个困惑或有对抗性的智能体最终会要求的东西。

每一条跨越边界的路径都要经过一次隔离检查,该检查在比较之前先解析符号链接——根目录内指向外部的符号链接会绕过对未解析路径的前缀检查。看起来像绝对路径的参数会被解释为相对于根目录,而不是真正的绝对路径。资源 URI 与工具参数受到同等对待。

非 UTF-8 文件、超大文件和厂商目录(.gitnode_modules 等)会被跳过,而不是作为噪声编入索引。

连接到客户端

Claude Desktop 或任何 MCP 主机都会以子进程方式启动服务器:

{
  "mcpServers": {
    "my-docs": {
      "command": "corpus-mcp",
      "args": ["--root", "/absolute/path/to/docs", "serve"]
    }
  }
}

语料库在磁盘上发生变化时会重新读取,因此会话期间编辑的文件无需重启就能被搜索到——重新索引按修改时间增量进行,而不是每次调用都重建。

开发

make install   # server plus dev tools
make demo      # one query against the example corpus
make test      # 89 tests, no network required
make smoke     # launch the installed server as a subprocess and exercise it
make lint

两层测试,因为它们捕获不同的失败:

  • tests/test_server.py 在进程内用真实的 MCP 客户端驱动真实的服务器。 被测试的是线路行为——工具模式、结构化结果、错误形状——而不是底层的 Python 函数。一个函数正确但工具表面错误的服务器仍然是坏的,只有这一层能捕获这类问题。

  • scripts/stdio_smoke.py 将已安装的控制台脚本作为子进程启动,并通过 stdio 与它进行 JSON-RPC 通信,就像主机那样。这覆盖了打包、入口点和传输层——包括经典的“有东西写到 stdout 并破坏协议流”的失败。

局限

  • 仅限词法检索。 与文档没有任何共同词汇的查询将无法找到它。在同一工具表面之后添加嵌入后端是显而易见的下一步。

  • 仅限文本格式——.md.txt.rst.csv.json.yaml 等。不支持 PDF 或 DOCX 提取。

  • 整个索引驻留在内存中,语料库变化时会完整重建。对于为此构建的数千文档场景没问题;百万级语料库需要一个按文件更新的真正索引。

  • 仅支持英语。 停用词表和词尾归并都基于英语。

  • 除根目录外没有访问控制。 根目录下的每个文件对服务器连接的任何对象都可见。

许可证

MIT。由 Aion Innovations 构建。

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.

  • Agentic search over your Dewey document collections from any MCP-compatible client.

  • Serve a folder of Markdown notes as an MCP server: hybrid search, reading, and sourced answers.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mmorrisj/corpus_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server