No description
  • Go 88.5%
  • HTML 11.1%
  • Dockerfile 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-17 23:00:44 +03:00
cmd/mcp-fs feat: opaque-токены — REST self-service /api/tokens*, вкладка «Токены» в Web UI и проводка OpaqueLookup в auth middleware (завершение Этапа 12) 2026-09-17 23:00:44 +03:00
docs feat: opaque-токены — REST self-service /api/tokens*, вкладка «Токены» в Web UI и проводка OpaqueLookup в auth middleware (завершение Этапа 12) 2026-09-17 23:00:44 +03:00
internal feat: opaque-токены — REST self-service /api/tokens*, вкладка «Токены» в Web UI и проводка OpaqueLookup в auth middleware (завершение Этапа 12) 2026-09-17 23:00:44 +03:00
.gitignore update: Обновление версии Go 2026-09-01 21:55:17 +03:00
.woodpecker.yml fix: реальная обработка не проверенных ошибок I/O (golangci-lint 0 issues) 2026-09-16 16:41:59 +03:00
config.yml.example feat: opaque-токены — хранилище (internal/tokens), порядок Identify в auth и зафиксированные доки; CLI/REST/WebUI дальше 2026-09-17 19:38:10 +03:00
Dockerfile fix: реальная обработка не проверенных ошибок I/O (golangci-lint 0 issues) 2026-09-16 16:41:59 +03:00
go.mod feat: opaque-токены — CLI one-shot флаги (--token-create/-list/-revoke) и go-simple-args v0.2.0 с Strict 2026-09-17 21:01:12 +03:00
go.sum feat: opaque-токены — CLI one-shot флаги (--token-create/-list/-revoke) и go-simple-args v0.2.0 с Strict 2026-09-17 21:01:12 +03:00
LICENSE first commit 2026-08-12 11:34:19 +03:00
README.md feat: opaque-токены — хранилище (internal/tokens), порядок Identify в auth и зафиксированные доки; CLI/REST/WebUI дальше 2026-09-17 19:38:10 +03:00

mcp-fs

MCP-сервер для работы с файловой системой (filesystem) на Go. Работает в двух режимах:

  • stdio — классический локальный MCP-сервер на stdin/stdout;
  • http — многопользовательский streamable HTTP (MCP) + REST API + встроенный Web UI для управления файлами.

Аутентификация в HTTP-режиме поддерживает три типа credential: JWT (HS256), который выдаёт внешний провайдер (например, OpenWebUI); долгоживущие ключи mct_…, генерируемые самим сервером; и «привнесённые» токены — любое своё значение пользователя. Идентификатор берётся из настраиваемого claim или записи токена и маппится на директорию (подробности: docs/authentication.md).

Возможности

  • Режимы: stdio / http (streamable HTTP).
  • Логирование в JSONL через log/slog: файл или консоль; в stdio — stderr, в http — stdout; уровни error|warn|info|debug|off.
  • JWT (HS256): настраиваемый claim, секрет, режим reject / skip.
  • Opaque-ключи mct_… и «принести свой» токен: генерация через CLI/WebUI/REST, TTL по желанию, отзыв; в хранилище только SHA-256 дайджесты.
  • Привязка идентификатора пользователя к директории:
    • нет маппинга → одна общая директория для всех;
    • известный id → своя директория;
    • аноним/неизвестный id → общая «анонимная» директория.
  • 17 MCP-инструментов: чтение/запись/поиск/управление файлами, архивы (create/extract), конкатенация, загрузка/отправка URL, diff (текст/бинарник).
  • REST API + Web UI (загрузка, скачивание, правка текста, переименование, удаление, каталоги) с теми же правами доступа, что и MCP.
  • Защита от выхода за пределы директории пользователя (включая симлинки).

Сборка

Требуется Go 1.27+.

go build -o mcp-fs ./cmd/mcp-fs
# или с версионированием
go build -ldflags="-s -w \
  -X git.ymnuktech.ru/ymnuk/mcp-fs/internal/version.Version=1.0.0 \
  -X git.ymnuktech.ru/ymnuk/mcp-fs/internal/version.Commit=$(git rev-parse --short HEAD) \
  -X git.ymnuktech.ru/ymnuk/mcp-fs/internal/version.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  -o mcp-fs ./cmd/mcp-fs

Быстрый старт

Локально (stdio)

./mcp-fs --mode stdio --storage-root ./data

Подключение через любой MCP-клиент, поддерживающий stdio. В stdio-режиме JWT не используется — файлы размещаются в общей/анонимной директории.

Многопользовательский HTTP

cp config.yml.example config.yml
# задайте jwt.secret и storage.*
./mcp-fs --config config.yml
  • MCP-эндпоинт: POST /mcp (stdlib MCP-клиентам указать URL http://host:8091/mcp)
  • Web UI: http://host:8091/
  • REST API: http://host:8091/api/...

HTTP-эндпоинты принимают заголовок Authorization: Bearer <token> (или jwt.header) с любым типом credential (JWT, ключ mct_…, привнесённый), но токен не обязателен: без него запрос идёт как анонимный. В режиме unauthorized: reject переданный невалидный токен отклоняется с 401; в skip подписанный/неподписанный JWT доверяется без проверки подписи. Незарегистрированный/протухший ключ mct_… → всегда 401 (fail-closed, независимо от режима).

Конфигурация

Парсинг — библиотека go-simple-args. Приоритет: CLI > env > config.yml > defaults. Путь к файлу: --config config.yml или CONFIG_FILE=....

Раздел Ключ CLI / env Default Описание
mode --mode / MODE stdio stdio или http
log file --log-file / LOG_FILE пусто путь JSONL-лога; пусто = консоль
log level --log-level / LOG_LEVEL info error|warn|info|debug|off
http addr --http-addr / HTTP_ADDR :8091 адрес прослушивания
http mcp_path --http-mcp-path / HTTP_MCP_PATH /mcp путь MCP-эндпоинта
http max_upload_bytes --http-max-upload-bytes / HTTP_MAX_UPLOAD_BYTES 104857600 лимит upload (100 MiB)
http max_text_file_size --http-max-text-file-size / HTTP_MAX_TEXT_FILE_SIZE 10485760 лимит текста (10 MiB)
mcp default_lines --mcp-default-lines / MCP_DEFAULT_LINES 0 строк по умолчанию для read_file (0 = все)
jwt secret --jwt-secret / JWT_SECRET пусто HS256 секрет
jwt claim --jwt-claim / JWT_CLAIM sub поле JWT — id пользователя
jwt header --jwt-header / JWT_HEADER Authorization заголовок с токеном
jwt unauthorized --jwt-unauthorized / JWT_UNAUTHORIZED reject reject = проверять подпись переданного токена (401 если невалиден); skip = доверять claim без проверки. Без токена всегда аноним (см. storage.anonymous_enabled)
tokens dir --token-dir / TOKEN_DIR пусто каталог opaque-токенов (<sha256>.json, 0600); пусто = фича выключена, JWT работает как раньше
storage root --storage-root / STORAGE_ROOT ./data общая директория
storage anonymous --storage-anonymous / STORAGE_ANONYMOUS пусто директория анонимов
storage anonymous_enabled --storage-anonymous-enabled / STORAGE_ANONYMOUS_ENABLED true разрешить анонимов (нет токена / ненамапленный id); false = блокировать
storage users — (только YAML) — map id -> директория
archive max_input_bytes --archive-max-input-bytes / ARCHIVE_MAX_INPUT_BYTES 5368709120 лимит входа archive_create (5 GiB)
archive max_uncompressed_bytes --archive-max-uncompressed-bytes / ARCHIVE_MAX_UNCOMPRESSED_BYTES 10737418240 лимит распаковки (10 GiB)
archive max_files --archive-max-files / ARCHIVE_MAX_FILES 10000 макс. файлов в архиве
archive timeout_seconds --archive-timeout-seconds / ARCHIVE_TIMEOUT_SECONDS 300 таймаут операции архивации
archive compression_level --archive-compression-level / ARCHIVE_COMPRESSION_LEVEL 6 уровень сжатия (1-9)
concat max_result_bytes --concat-max-result-bytes / CONCAT_MAX_RESULT_BYTES 10737418240 лимит результата конкатенации (10 GiB)
download max_bytes --download-max-bytes / DOWNLOAD_MAX_BYTES 10737418240 лимит загрузки (10 GiB)
download timeout_seconds --download-timeout-seconds / DOWNLOAD_TIMEOUT_SECONDS 600 таймаут загрузки
download block_private_ips --download-block-private-ips / DOWNLOAD_BLOCK_PRIVATE_IPS true блокировать private/loopback/link-local/multicast
download allowed_domains — (только YAML) пусто allowlist доменов (обходит проверку private)
download user_agent --download-user-agent / DOWNLOAD_USER_AGENT mcp-fs-downloader/1.0 User-Agent на загрузке
upload max_bytes --upload-max-bytes / UPLOAD_MAX_BYTES 10737418240 лимит отправки (10 GiB)
upload timeout_seconds --upload-timeout-seconds / UPLOAD_TIMEOUT_SECONDS 300 таймаут отправки
upload allowed_target_hosts — (только YAML) пусто allowlist целевых хостов
diff max_text_size --diff-max-text-size / DIFF_MAX_TEXT_SIZE 104857600 лимит текста для diff (100 MiB)
diff max_binary_size --diff-max-binary-size / DIFF_MAX_BINARY_SIZE 1073741824 лимит бинарника (1 GiB)

Полный пример — config.yml.example.

JWT из OpenWebUI

  1. Войдите в OpenWebUI как нужный пользователь.
  2. Извлеките JWT (например, из localStorage браузера: ключ token / id_token, или из cookie).
  3. Укажите в config.yml тот же секрет, которым OpenWebUI подписывает токены, и claim, который соответствует id пользователя (обычно sub).
  4. В Web UI вставьте токен один раз — он сохранится в localStorage (mcp_fs_jwt). При истечении (ответ 401) токен удаляется автоматически, и UI запросит его заново.

Opaque-токены: ключи mct_… и «принести свой»

Дополняют JWT долгоживущими credential'ами без срока действия по умолчанию. Единственный формат запроса — Authorization: Bearer <значение>; тип определяется сервером (зарегистрированный дайджест → владелец, префикс mct_ без записи в store → 401).

Хранение (tokens.dir, пусто = фича выключена): один JSON-файл на токен <sha256hex>.json с {name, userID, createdAt, expiresAt}; права 0600, запись tmp+rename. Сырые значения не хранятся — только SHA-256 дайджесты.

CLI (one-shot флаги, выполняются до запуска и завершают процесс)

# создать для alice (raw печатается один раз — сохраните его); TTL в секундах, 0 = бессрочно
mcp-fs --token-create alice [--token-name ci-deployer] [--token-ttl 3600]
# список записей: name/created/expires (raw не показывается); "all" = все пользователи
mcp-fs --token-list [userID|all]
# отзыв по имени, либо все токены пользователя при отсутствии имени
mcp-fs --token-revoke <userID> [--token-name ci-deployer]

REST self-service (только свои записи)

  • POST /api/tokens {name?, ttl?} → 201 {raw, token{…}} — raw один раз;
  • POST /api/tokens/import {raw, name?, ttl?} → 201 без эха raw. Валидация: 12–512 печатных ASCII без пробелов, энтропия Шеннона ≥3.0 бит/символ, дайджест не занят никем (иначе 409 «this token value is already registered»); имя — [a-zA-Z0-9_.-]{1,64}, уникально в рамках владельца;
  • GET /api/tokens → свои {name, createdAt, expiresAt[]};
  • DELETE /api/tokens/{name} → 204 (нет своего токена — 404).

Без credential на self-service — 401 «authentication required»; при пустом tokens.dir все token-маршруты отвечают 503 «opaque tokens are not enabled».

Web UI: поле логина принимает любой тип credential; вкладка «Токены» — создание (модалка «скопируйте сейчас»), регистрация своего raw, список и отзыв. Полный справочник ошибок и порядок Identify — docs/authentication.md.

REST API

24 эндпоинта; все маршруты — под аутентификацией (JWT или opaque-токен), ошибки — {"error": "..."}. Маршруты /api/tokens* self-service: работают только со своими токенами.

Метод Путь Описание
GET /api/root текущий root и identity
GET /api/list?path= список каталога
GET /api/tree?path= рекурсивное дерево
GET /api/file?path= текстовый контент файла
POST /api/file запись текстового файла {path, content}
GET /api/download?path= скачивание файла
POST /api/upload?path= multipart-загрузка файлов
POST /api/dir создать каталог {path}
POST /api/move переименовать/переместить {from, to}
DELETE /api/file?path= удалить файл
DELETE /api/dir?path= удалить каталог (рекурсивно)
POST /api/edit последовательные текстовые замены {path, edits[]}
GET /api/search?path=&pattern=&max_depth= regex-поиск имён файлов
GET /api/info?path= метаданные файла/каталога
POST /api/archive/create создать архив, вернуть как файл {archive, sources[], format, …}
POST /api/archive/extract распаковать архив из storage {archive, destination, …}
POST /api/concat конкатенация файлов, вернуть результат как файл {sources[], destination, …}
POST /api/download_url скачать из URL, вернуть как файл {url, destination, …}
POST /api/upload_url отправить файл из storage на внешний URL {source, url, …}
POST /api/diff сравнение двух файлов {file1, file2, mode} → JSON
POST /api/tokens создать ключ mct_… для текущего пользователя {name?, ttl?} (raw — один раз)
POST /api/tokens/import зарегистрировать свой raw-токен на себя {raw, name?, ttl?}
GET /api/tokens список своих токенов [{name, createdAt, expiresAt}]
DELETE /api/tokens/{name} отзыв своего токена по имени → 204

MCP-инструменты

list_directory, directory_tree, read_file, write_file, edit_file, create_directory, move_file, delete_file, delete_directory, search_files, get_file_info, archive_create, archive_extract, concat_files, download_url, upload_url, diff_files.

  • read_file определяет тип по содержимому/MIME: текст (txt, md, csv, json, yaml, …) возвращается строкой, изображения — ImageContent, аудио — AudioContent, остальные бинарные — embedded-ресурсом. Для текстовых файлов доступно чтение по частям: offset (1-based номер первой строки) и limit (макс. число строк от offset; если не задан — берётся mcp.default_lines из конфига, 0 = все строки).
  • delete_directory удаляет только пустой каталог; для удаления непустого каталога (рекурсивно) передавайте recursive: true.
  • archive_create / archive_extract — архивы tar, tar.gz, tar.bz2, tar.xz, tar.br, zip, одиночные gz/bz2 (формат по расширению или format). Экстракция защищена от path-escape, симлинков/хардлинков и zip-bomb (лимиты файлов/размера). Bzip2 — чистый Go (dsnet/compress), компрессия медленнее нативного bzip2.
  • concat_files — конкатенация файлов (стриминг, опциональный separator).
  • download_url / upload_url — загрузка из внешнего http(s) и отправка файла на внешний http(s) (POST/PUT). SSRF-защита (блокировка private/loopback/link-local/multicast) + лимиты размера/таймаута. Серверный JWT во внешние сервисы не передаётся.
  • diff_files — сравнение двух файлов: mode=text (unified diff, по умолчанию) или mode=binary (SHA256-хэши).

Пути в инструментах указываются внутри директории пользователя, например /docs/note.txt (не абсолютные пути хоста).

Docker

docker build -t mcp-fs .
docker run -p 8091:8091 -v "$PWD/data:/app/data" mcp-fs --config /app/config.yml

CI

Woodpecker-пайплайн см. в .woodpecker.yml: lint/test, сборка бинарника, Docker-образ, release.

Документация

  • ROADMAP.md — план с декомпозицией и отметками выполнения.
  • security.md — модель защиты путей, остаточные риски.
  • config.yml.example — пример конфигурации.

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

  • Каждый запрос (в т.ч. каждый MCP-вызов в stateless-режиме) проверяется на валидность JWT.
  • Все пути проверяются на выход за root двухуровнево: лексически (filepath.Rel, запрет ../абсолютных путей) и через EvalSymlinks (симлинки наружу блокируются даже при создании новых файлов). Подробно — docs/security.md.
  • Не выставляйте сервер без jwt.secret в открытый интернет.