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, возвращающий полные фрагменты, проще написать, но гораздо хуже использовать: десять результатов по 1200 символов — это бо́льшая часть окна контекста, потраченная прежде, чем агент решил, какой из них ему нужен. Поэтому search возвращает сниппеты — достаточно для первичной сортировки, — а fetch по запросу расширяет выбранный результат. Агент платит за детали только там, где сам решил, что они стоят того.

Сниппеты центрируются на совпадении, а не на начале фрагмента. Возврат первых N символов постоянно подводит: подходящее предложение обычно находится в середине. Агент видит постороннее вступление и либо отбрасывает удачное совпадение, либо загружает всё, чтобы разобраться. Окно сниппета выбирается так, чтобы охватить как можно больше вхождений терминов запроса.

Любой лимит принудительно обрезается на стороне сервера. Вывод инструмента попадает прямиком в окно контекста, поэтому инструмент без ограничений — это отказ в обслуживании для того, кто его вызывает. Запрос 10 000 результатов — именно тот случай, ради которого существует лимит; ограничения применяются принудительно, а не по доверию. Когда вывод обрезается, ответ об этом сообщает, чтобы агент мог сузить запрос, а не предполагать, что увидел всё.

Пустые результаты объясняют сами себя. Голый пустой список — это тупик. В ответе сообщается, сколько фрагментов и документов существует, что позволяет отличить «запрос промахнулся» от «ничего не проиндексировано» — две ситуации с разными следующими шагами.

Устаревшие идентификаторы — ожидаемый результат, а не ошибка. Идентификаторы фрагментов меняются при редактировании документа, поэтому идентификатор из начала длинной сессии может устареть. fetch прямо сообщает об этом и предлагает агенту выполнить поиск заново.

При склейке фрагментов перекрытие удаляется. Фрагменты перекрываются, чтобы ни один отрывок не разрывался на границе, но если вернуть это перекрытие, агент прочитает одни и те же предложения дважды и может принять повторение за акцент. Фрагменты содержат абсолютные смещения, поэтому перекрытие удаляется по позиции, а не по совпадению строк.

BM25, а не эмбеддинги. Для запросов, похожих на ключевые слова, которые агент отправляет при навигации по корпусу, о котором он уже что-то знает, лексический поиск силён. И у него есть свойство, которое важнее всего в цикле агента: быстро и никогда незаметно не стоит денег. Семантический поиск — полезное дополнение, а не обязательное условие полезности.

Лёгкий стемминг, а не настоящий стеммер. Множественное число и типичные глагольные окончания сводятся так, что tastes соответствует taste. Полноценная реализация Портера — это сотня строк и поверхность для поддержки, а её длинный хвост (operationaloper) на коротких запросах с равной вероятностью навредит или поможет. Индексация и запросы используют один токенизатор, потому что любое расхождение между ними молча снижает полноту поиска.

Безопасность

Серверу задаётся корневой каталог, и он никогда не читает за его пределами. Это важнее, чем кажется: аргументы инструментов приходят из выходных данных модели, поэтому идентификатор документа — это недоверенный ввод, а ../../.ssh/id_rsa — то, что рано или поздно запросит сбитый с толку или враждебный агент.

Каждый путь, пересекающий границу, проходит через единую проверку ограничения, которая разрешает симлинки до сравнения: симлинк внутри корня, указывающий наружу, обходит проверку префикса, выполненную по неразрешённому пути. Выглядящие как абсолютные аргументы интерпретируются относительно корня, а не как настоящие абсолютные пути. URI ресурсов обрабатываются так же, как аргументы инструментов.

Файлы не в UTF-8, слишком большие файлы и каталоги зависимостей (.git, node_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 запускает установленный консольный скрипт как подпроцесс и общается с ним по JSON-RPC через stdio, как это делает хост. Это покрывает упаковку, точку входа и транспорт — включая классический сбой, когда что-то пишет в 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