Skip to main content
Glama
SkySai1

Open WebUI Knowledge MCP

by SkySai1
README.md
# Open WebUI Knowledge MCP

Компактный MCP для Goose: управление знаниями и поиск в Qdrant **через Open WebUI**.

```text
Goose / MCP client → этот MCP → Open WebUI → Qdrant
                                    ↓
                       настроенные embedding / reranking
```

MCP использует только Open WebUI HTTP API. Open WebUI отвечает за загрузку,
chunking, embeddings, хранение оригиналов, векторов и поиск. MCP возвращает
фрагменты, а не генерирует итоговый ответ. Прямых клиентов Qdrant/Ollama и ML-библиотек
в зависимостях MCP нет.

## Установка

Нужны Python 3.10+, uv и уже запущенный Open WebUI, настроенный на Qdrant.
В каталоге репозитория:

```sh
uv venv --python 3.12
uv pip install --python .venv/bin/python -e '.[dev]'
cp .env.example .env
```

Укажите в `.env` URL и API key **Open WebUI**. Запуск из терминала:

```sh
set -a
. ./.env
set +a
.venv/bin/openwebui-rag-mcp
```

Сервер ожидает MCP-сообщения на stdin; произвольного вывода в stdout нет.
Сам MCP не читает `.env`: пример выше экспортирует его значения в окружение.
Для обычной установки без инструментов разработки замените `'.[dev]'` на `.`.

## Настройка Open WebUI → Qdrant

Эти переменные задаются **процессу/контейнеру Open WebUI**, а не MCP:

```dotenv
VECTOR_DB=qdrant
QDRANT_URI=http://qdrant:6333
QDRANT_API_KEY=
RAG_EMBEDDING_ENGINE=ollama
RAG_OLLAMA_BASE_URL=http://ollama:11434
RAG_EMBEDDING_MODEL=your-installed-embedding-model
```

`qdrant` и `ollama` в примере — имена сервисов в одной контейнерной сети; замените
адреса на доступные Open WebUI. Проверьте сохранённые настройки Documents в Admin
Panel: часть параметров Open WebUI хранится в его БД. Модель выбирает администратор.
Это настройка backend для баз, которыми управляет Open WebUI; существующие
произвольные коллекции Qdrant автоматически базами Open WebUI не становятся.
[Описание переменных Open WebUI](https://docs.openwebui.com/reference/env-configuration/).

Hybrid search и reranker настраиваются в Open WebUI. MCP сохраняет выбранный режим
и передаёт `k`/`k_reranker` для конкретного поиска. Совместимость reranker с Ollama
зависит от возможностей Open WebUI и выбранного адаптера; MCP не эмулирует `/rerank`.
Для первого запуска можно использовать обычный vector search без reranker.

В Open WebUI разрешите API keys и создайте ключ пользователя с нужными правами
на Knowledge/Files/Retrieval API. Если включены ограничения endpoints, разрешите
используемые ниже пути. `rag_health` проверяет только авторизованный Knowledge API,
а не фактическую доступность Qdrant или моделей.

## Окружение MCP

| Переменная | По умолчанию | Значение |
|---|---|---|
| `OPENWEBUI_URL` | `http://localhost:3000` | Базовый URL; поддерживается path prefix |
| `OPENWEBUI_API_KEY` | обязательно | Bearer token Open WebUI |
| `OPENWEBUI_TIMEOUT` | `120` | Timeout каждой HTTP-операции, секунды |
| `OPENWEBUI_VERIFY_TLS` | `true` | Проверять TLS, строго `true`/`false` |
| `OPENWEBUI_MAX_PAGES` | `1000` | Предел пагинации; превышение возвращает ошибку |
| `RAG_TOP_K` | `8` | Максимум фрагментов, 1–100 |
| `RAG_MAX_CHUNK_CHARS` | `7000` | Лимит текста фрагмента в выдаче |
| `RAG_MAX_FILE_CHARS` | `50000` | Лимит извлечённого текста файла в выдаче |
| `RAG_MAX_INPUT_CHARS` | `1000000` | Лимит входного текста/query в символах |

Усечение всегда обозначается `truncated`; у файла есть `total_chars`.
`RAG_SCORE_THRESHOLD` старого прототипа удалён: MCP не может одинаково трактовать
оценки всех режимов retrieval. Threshold задаётся в Open WebUI.
Невалидная конфигурация завершает запуск с кодом 2 и сообщением в stderr.
Недоступный Open WebUI при старте логируется; MCP остаётся запущен и может
восстановиться при следующем вызове.

## Goose

Добавьте stdio extension через интерфейс Goose или объедините этот блок со своим
`~/.config/goose/config.yaml`. Замените путь и ключ своими значениями:

```yaml
extensions:
  openwebui_knowledge:
    name: openwebui_knowledge
    type: stdio
    enabled: true
    cmd: /absolute/path/to/GooseMCPopenwebui/.venv/bin/openwebui-rag-mcp
    args: []
    timeout: 600
    envs:
      OPENWEBUI_URL: http://localhost:3000
      OPENWEBUI_API_KEY: replace-with-your-key
      OPENWEBUI_TIMEOUT: "120"
      OPENWEBUI_VERIFY_TLS: "true"
      RAG_TOP_K: "8"
```

Timeout Goose учитывает, что запись состоит из нескольких HTTP-запросов.
Используйте абсолютный путь: запуск уже установленного пакета не требует PyPI.
[Формат конфигурации Goose](https://github.com/aaif-goose/goose/blob/main/documentation/docs/guides/config-files.md).

## Tools

`knowledge_id` — ID базы Open WebUI; `file_id` — ID документа. Это разные сущности.

| Tool | Назначение |
|---|---|
| `rag_health` | Проверить доступ к Knowledge API |
| `knowledge_list` | Все доступные базы, кратко и с пагинацией |
| `knowledge_create(name, description="")` | Создать базу |
| `knowledge_get(knowledge_id)` | Сведения о базе и краткий список её файлов |
| `knowledge_add(knowledge_id, text, title="knowledge.txt", source=null, metadata=null)` | Загрузить UTF-8 `.txt`, проверить обработку и прикрепить к базе |
| `knowledge_update(knowledge_id, file_id, text)` | Изменить текст общего файла, подтвердить чтением, обновить индекс базы |
| `knowledge_delete(knowledge_id, file_id)` | Отсоединить файл от базы с `delete_file=false` |
| `knowledge_get_file(file_id)` | Извлечённый текст файла |
| `knowledge_search(query, knowledge_ids=null, top_k=null)` | Найти релевантные фрагменты |

Старые имена `rag_list_knowledge`, `rag_search`, `rag_get_file` сохранены как aliases
с теми же входными параметрами. Формат результатов обновлён; это не полная обратная
совместимость старого прототипа.

Пример последовательности arguments:

```json
{"name":"Рабочие заметки","description":"Решения команды"}
```

Из ответа `knowledge_create` возьмите `knowledge_id`:

```json
{"knowledge_id":"<id-базы>","text":"Согласовали выпуск в пятницу.","title":"Решение","metadata":{"project":"demo"}}
```

Из ответа `knowledge_add` возьмите `file_id` для чтения, обновления или удаления.
Поиск:

```json
{"query":"Когда выпуск?","knowledge_ids":["<id-базы>"],"top_k":5}
```

`knowledge_ids=null` ищет во всех доступных базах, `[]` — ни в одной. MCP отправляет
один retrieval-запрос для выбранного набора и сохраняет порядок Open WebUI.
Каждый результат содержит `text`, `truncated`, `chunk_id`, `file_id`, `knowledge_id`,
`title`, `source`, `metadata`, `score`, `distance`. Отсутствующие upstream поля — `null`.
Score/distance сохраняются без преобразований: поле `distances` в разных режимах
Open WebUI может содержать разные типы оценок. Отдельные vector/reranker scores
и достоверное число chunks API не гарантирует. Embeddings из ответов исключаются.

## Контракт API и ограничения MVP

Контракт сверялся с официальным исходным кодом Open WebUI `main` 22.09.2026:
[Knowledge API](https://github.com/open-webui/open-webui/blob/main/backend/open_webui/routers/knowledge.py),
[Files API](https://github.com/open-webui/open-webui/blob/main/backend/open_webui/routers/files.py),
[Retrieval API](https://github.com/open-webui/open-webui/blob/main/backend/open_webui/routers/retrieval.py).
23.09.2026 дополнительно проверены установленный Open WebUI 0.11.3 и
[контракт Retrieval API этого тега](https://github.com/open-webui/open-webui/blob/v0.11.3/backend/open_webui/routers/retrieval.py).

| Операция | HTTP API |
|---|---|
| Базы | `GET /api/v1/knowledge/`, `POST /api/v1/knowledge/create` |
| База / файлы | `GET /api/v1/knowledge/{id}`, `GET /api/v1/knowledge/{id}/files` |
| Загрузка | `POST /api/v1/files/?process=true&process_in_background=false` (multipart) |
| Обработка | `GET /api/v1/files/{id}/process/status` |
| Привязка / переиндексация / удаление связи | `POST /api/v1/knowledge/{id}/file/{add,update,remove}` |
| Извлечённый текст | `GET /api/v1/files/{id}/data/content` |
| Изменение текста | `POST /api/v1/files/{id}/data/content/update` |
| Поиск по одной базе | `POST /api/v1/retrieval/query/doc` с `collection_name`, `query`, `k`, `k_reranker` |
| Поиск по нескольким базам | `POST /api/v1/retrieval/query/collection` с `collection_names`, `query`, `k`, `k_reranker` |

Список баз поддерживает `items/total`, `data/total` и старый плоский массив.
Файлы читаются из вложенного `files` старых ответов либо из отдельного paginated API.
Retrieval поддерживает одну вложенную строку результатов и плоский массив.
Это совместимость форматов, а не обещание поддержки любого релиза. Для мутаций
нужны перечисленные endpoints и поддержка `delete_file=false` установленной версией.

### Причина HTTP 400 на проверенном Open WebUI 0.11.3

Ошибка устранена на проверенном экземпляре: пользователь переключил Open WebUI
на встроенную модель reranker и восстановил доступ API key. Итоговая live-проверка
23.09.2026 через MCP прошла без 400/403 и без предупреждений о пропущенных оценках:

| Сценарий | top_k | Получено фрагментов | Результат |
|---|---|---|---|
| «НПА России» | 8 | 8 | У всех есть числовая оценка в `distance` |
| «Тестовая база знаний», alias `rag_search` | 1 | 1 | Контрольная фраза найдена |
| Обе указанные базы, общий запрос | 3 | 3 | У всех есть числовая оценка в `distance` |
| Все 3 доступные базы, `knowledge_ids=null` | 3 | 3 | Контрольная фраза найдена |

Оценки возвращены в upstream-поле `distances`; отдельное поле `score` отсутствует
и сохраняется как `null`. Это не отсутствие оценок. Порядок выдачи сохраняется,
лимиты соблюдаются. Проверка подтверждает выполнение поиска, но не является
оценкой качества релевантности на эталонном наборе. Новые документы при этом
не загружались; использован ранее созданный проверочный файл.

Предшествующая диагностика установила следующую цепочку сбоя:

1. Open WebUI не мог соединиться с настроенным внешним reranker на
   `host.docker.internal:11435/v1/rerank`. В логах есть ошибка установления соединения;
   на хосте порт 11435 не слушается.
2. [ExternalReranker.predict в 0.11.3](https://github.com/open-webui/open-webui/blob/v0.11.3/backend/open_webui/retrieval/models/external.py)
   перехватывает исключение и возвращает `None` вместо оценок.
3. [RerankCompressor в 0.11.3](https://github.com/open-webui/open-webui/blob/v0.11.3/backend/open_webui/retrieval/utils.py)
   при этом возвращает исходные документы. У части документов нет `score`.
   `merge_and_sort_query_results` сравнивает `None` с `float` и вызывает TypeError;
   retrieval handler преобразует его в HTTP 400.

Запрос MCP соответствует `QueryCollectionsForm`: `collection_names: list[str]`,
`query: str`, `k: int`, `k_reranker: int`. Необязательные `hybrid`, `r`,
`hybrid_bm25_weight`, `enable_enriched_texts` не передаются, поэтому настройки выбирает
Open WebUI. Контракт проверен по
[официальному router 0.11.3](https://github.com/open-webui/open-webui/blob/v0.11.3/backend/open_webui/routers/retrieval.py);
общая [документация API](https://docs.openwebui.com/reference/api-endpoints/)
не заменяет контракт конкретного релиза.

Первоначальный обход через `/query/doc` подтвердил получение фрагментов, но **не
исправность reranking**: отсутствие общего этапа сортировки позволяет вернуть 200
с отсутствующими оценками. MCP сохраняет этот штатный endpoint для одной базы,
но теперь добавляет `warnings`, если у возвращаемого фрагмента отсутствуют и score,
и distance. Это предупреждение о непроверяемом ранжировании, а не доказательство
ошибки reranker для любого ответа без метрик. Оценки и порядок не изменяются.
Несколько баз используют один глобальный запрос; при HTTP 400 возвращаются
`isError=true`, `http_status`, `endpoint` и безопасная подсказка проверить зависимости.

Если используется внешний reranker, первопричина устраняется администратором Open WebUI:

- В Admin Panel → Settings → Documents проверить external reranker URL/model/key
  и доступность **из контейнера Open WebUI**. `host.docker.internal` обозначает хост;
  сервис должен быть запущен и принимать соединения из Docker.
- Восстановить существующий reranker на нужном порту либо указать его действующий
  адрес. Не подставлять URL embeddings или chat API: нужен совместимый rerank endpoint.
- Согласно [официальному адаптеру](https://github.com/open-webui/open-webui/blob/v0.11.3/backend/open_webui/retrieval/models/external.py),
  Open WebUI отправляет POST с `model`, `query`, `documents` (массив строк), `top_n`.
  Ответ должен содержать `results` с `index` и числовым `relevance_score` для документов.
- Повторить `knowledge_search` с двумя `knowledge_ids` и с `knowledge_ids=null`.
  Успешное чтение файлов или `rag_health` не проверяет reranker.

Во время диагностики API key получал 401 на административный `GET /api/v1/retrieval/config`.
MCP не обходит это ограничение, не меняет глобальные настройки, не выключает
hybrid/reranking автоматически и не запускает модели. В этой установке пользователь
выбрал встроенный reranker; восстановление прежнего внешнего сервиса больше
не является условием работы поиска.
Изменение настроек reranker само по себе не требует переиндексации документов.

До переключения reranker live-проверка через MCP при `top_k=8` вернула восемь фрагментов,
пять без обеих метрик; MCP добавил предупреждение. Запрос по двум базам вернул
явную ошибку 400 с новой диагностикой. Локально проходят 86 тестов, включая
восстановление поиска через MCP SDK после ошибки, и ruff check/format.

Фактически проверено: список 35 документов «НПА России», получение полного текста
одного файла (24 768 символов), два поисковых фрагмента через `knowledge_search`;
загрузка `mcp-check-20260923.txt` в «Тестовая база знаний», чтение 156 символов обратно
и нахождение контрольной фразы через alias `rag_search`. Проверочный файл сохранён
в тестовой базе, его `file_id`: `cd8a48cf-03d3-478a-aaf0-a4397d5acdf1`.
Live update/delete и запуск из Goose в этой проверке не выполнялись.

- Записи неатомарны. Ошибка возвращается с MCP `isError=true`, `ok=false`, `stage`,
  известным `file_id` и `outcome=partial_or_unknown`. При timeout запрос мог завершиться:
  сначала проверьте Open WebUI, затем решайте, повторять ли операцию. Автоповторов нет.
- При сбое привязки загруженный файл сохраняется в Open WebUI; его можно проверить
  и прикрепить через UI. MCP не удаляет его автоматически.
- Update меняет **общий файл**. Другие базы, использующие его, могут измениться;
  поведение обновления их индексов зависит от версии Open WebUI. Индекс указанной базы
  обновляется отдельным вызовом. Исходный скачиваемый файл и извлечённый текст могут
  отличаться после редактирования — MCP читает именно извлечённый текст.
- Delete удаляет связь и поручает удаление векторов Open WebUI. Сам файл и другие
  базы сохраняются. Некоторые ошибки очистки/поиска Open WebUI скрывает внутри
  успешного HTTP-ответа; MCP не может независимо подтвердить состояние Qdrant.
- Source/title/metadata передаются как metadata загрузки (Open WebUI сохраняет их
  в `file.meta.data`); попадание произвольных полей в retrieval metadata зависит от backend.
  Изменение metadata, metadata-filter, удаление всей базы и внешние read-only Knowledge
  Sources не входят в CRUD MVP.
- Конкурирующие записи сериализуются внутри одного MCP-процесса. Транзакций между
  несколькими MCP или пользователями Open WebUI нет.

## Разработка и проверки

```sh
.venv/bin/pytest -q
.venv/bin/ruff check src/openwebui_rag_mcp tests
.venv/bin/ruff format --check src/openwebui_rag_mcp tests
```

Все HTTP-вызовы тестов mock'аются через httpx. Отдельный тест запускает реальный
stdio subprocess, выполняет MCP initialize/list_tools/call_tool и проверяет
восстановление после ошибки tool. Qdrant/Ollama/Open WebUI для unit-тестов не нужны.
План и оценка исходного кода — [TODO.md](TODO.md), инструкции — [AGENTS.md](AGENTS.md).

TDQS

C2.6/5.0

Scored across 12 tools

Disambiguation2/5

Several tools are exact aliases (rag_get_file/knowledge_get_file, rag_list_knowledge/knowledge_list, rag_search/knowledge_search), so an agent must arbitrarily choose between duplicate entry points. The remaining tools are mostly distinct, but the duplicated surface creates real selection ambiguity.

Naming Consistency3/5

The knowledge_* tools follow a mostly consistent snake_case verb_noun pattern, but rag_* aliases introduce a second prefix convention. Names like knowledge_get and knowledge_get_file are also close enough to be mildly confusing.

Tool Count4/5

Twelve tools is within a reasonable range, but three are exact aliases padding the count. The effective surface is about nine unique operations, which is well-scoped for this domain.

Completeness3/5

File operations are well covered with add, get, update, delete, and search, and knowledge bases support list, create, and get. However, knowledge bases lack update and delete operations, leaving an obvious lifecycle gap.

Maintenance

ActivityMaintained
ResponsivenessNo issues