- Go 88.5%
- HTML 11.1%
- Dockerfile 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| cmd/mcp-fs | ||
| docs | ||
| internal | ||
| .gitignore | ||
| .woodpecker.yml | ||
| config.yml.example | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| README.md | ||
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-клиентам указать URLhttp://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
- Войдите в OpenWebUI как нужный пользователь.
- Извлеките JWT (например, из localStorage браузера: ключ
token/id_token, или из cookie). - Укажите в
config.ymlтот же секрет, которым OpenWebUI подписывает токены, и claim, который соответствует id пользователя (обычноsub). - В 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в открытый интернет.