Skip to main content
Glama
Surf-Team

FileManager MCP Server

by Surf-Team
README.md
# FileManager MCP Server

MCP-сервер (Streamable HTTP) для **read-only** доступа ИИ-клиентов к файлам проектов.
Проекты, разрешённые каталоги, расширения файлов и ключи доступа описываются в YAML-конфиге —
всё, что не указано в конфиге явно, сервер прочитать не может.

## Инструменты

| Инструмент | Описание |
|---|---|
| `list_projects` | Проекты, доступные предъявленному API-ключу: id, название, описание, корневые папки, читаемые расширения, лимиты |
| `list_files` | Содержимое папки проекта; без `path` — список корней, с `recursive: true` — рекурсивный обход (только файлы) |
| `read_file` | Чтение текстового файла в UTF-8, с постраничной выдачей по строкам (`offset` / `limit`) |
| `find_files` | Поиск файлов по имени: glob (`*`, `?`, `**`) или подстрока |
| `search_content` | Поиск по содержимому файлов (grep) с номерами строк и опциональным контекстом |
| `file_info` | Метаданные файла/папки: тип, размер, даты, расширение, доступность для чтения, число строк |

Инструментов записи нет — сервер физически не умеет изменять файлы.

## Требования

- Node.js 18+
- Доступ на чтение к каталогам, указанным в конфиге

## Установка

```bash
npm install
cp .env.example .env                       # порт, лимиты, значения ключей
cp projects.example.yaml projects.yaml     # описание проектов
# отредактировать оба файла
```

## Конфигурация проектов (projects.yaml)

Путь к файлу задаётся переменной `PROJECTS_CONFIG` (по умолчанию `./projects.yaml`).

```yaml
defaults:                        # необязательно, лимиты по умолчанию для всех проектов
  max_file_size: 1048576
  max_entries: 1000
  max_depth: 5

projects:
  - id: gve-server-data                     # ^[a-z0-9][a-z0-9_-]*$, уникален
    name: Данные игрового сервера
    description: |
      Конфиги, html-диалоги NPC и csv/yaml-таблицы игрового сервера GVE.
    paths:                                  # абсолютные пути, подпапки доступны автоматически
      - /root/Gve-Server-Data/data
    extensions: [.yaml, .yml, .xml, .html, .htm, .md, .json, .txt]
    exclude:                                # необязательно, glob по виртуальному пути
      - "**/secrets/**"
    list_unreadable: false                  # показывать ли файлы с чужими расширениями
    max_file_size: 2097152                  # необязательный override
    api_keys:
      - "${FM_KEY_GVE_DATA}"                # подстановка из окружения (рекомендуется)
```

| Поле | Обязательное | Назначение |
|---|---|---|
| `id` | да | Идентификатор проекта, передаётся в аргументе `project` |
| `name` | нет | Человекочитаемое название (по умолчанию `id`) |
| `description` | нет | Описание для ИИ: что за данные и зачем они нужны |
| `paths` | да | Абсолютные пути к разрешённым корням; должны существовать и не быть вложены друг в друга |
| `extensions` | да | Расширения, доступные для чтения и поиска (`yml` нормализуется в `.yml`) |
| `exclude` | нет | Дополнительные исключения (glob по виртуальному пути) |
| `list_unreadable` | нет | Показывать в листингах файлы с неразрешёнными расширениями (`readable: false`) |
| `max_file_size` / `max_entries` / `max_depth` | нет | Переопределение лимитов проекта |
| `api_keys` | да | Ключи доступа к этому проекту, минимум 24 символа; `${VAR}` берётся из окружения |

Конфиг читается один раз при старте. Любая ошибка валидации (несуществующий путь, короткий ключ,
дублирующийся `id`, вложенные корни) останавливает запуск — сервер никогда не поднимается
с частично корректным списком разрешений. После изменения YAML нужен рестарт.

## Переменные окружения (.env)

| Переменная | По умолчанию | Назначение |
|---|---|---|
| `PORT` | `3200` | Порт MCP-сервера |
| `PROJECTS_CONFIG` | `./projects.yaml` | Путь к конфигу проектов |
| `MAX_FILE_SIZE` | `1048576` | Максимальный размер читаемого файла, байт |
| `MAX_ENTRIES` | `1000` | Максимум записей в одном листинге |
| `MAX_DEPTH` | `5` | Максимальная глубина рекурсивного обхода |
| `MAX_LINE_LENGTH` | `2000` | Длинные строки обрезаются до этой длины |
| `SEARCH_MAX_FILES` | `2000` | Максимум файлов, просматриваемых в `search_content` |
| `SEARCH_MAX_MATCHES` | `200` | Максимум совпадений в ответе `search_content` |
| `SEARCH_TIMEOUT_MS` | `5000` | Бюджет времени на один поиск |
| `SEARCH_ALLOW_REGEX` | `true` | Разрешать ли режим `regex: true` в `search_content` |

## Запуск

```bash
npm start        # прод
npm run dev      # с автоперезапуском (--watch)
npm test         # тесты модели безопасности (node --test)
```

- MCP endpoint: `POST http://<host>:<PORT>/mcp` (требует API-ключ)
- Health check: `GET http://<host>:<PORT>/health` — статус и доступность всех корней

## Подключение MCP-клиента

```json
{
  "mcpServers": {
    "filemanager": {
      "url": "http://<host>:<PORT>/mcp",
      "headers": { "Authorization": "Bearer <API_KEY>" }
    }
  }
}
```

Ключ можно передавать и в заголовке `X-API-Key`.

## Модель безопасности

Клиент никогда не видит и не передаёт реальные пути файловой системы. Файлы адресуются
**виртуальным путём** `<корень>/<подпапка>/<файл>`, где `<корень>` — имя (basename) одного из
каталогов в `paths`.

1. **Валидация ввода.** Отклоняются пути с `\0`, обратным слэшем, двоеточием, сегментами `.`/`..`,
   длиной больше 1024 символов.
2. **Канонизация и проверка вложенности.** Путь разрешается через `fs.realpath`, и результат
   обязан находиться внутри канонизированного корня. Это закрывает и `../`-обход, и побег по
   симлинку/junction — в том числе созданному уже после старта сервера. Каждая запись каталога при
   обходе тоже проходит `realpath`; всё, что указывает наружу, молча скрывается.
3. **Allowlist расширений.** Читать и искать можно только в файлах с расширениями из `extensions`.
4. **Неотключаемый deny-list** поверх allowlist: любые файлы и папки, начинающиеся с точки
   (`.env`, `.git`, `.ssh`), `node_modules`, ключевой материал (`*.pem`, `*.key`, `id_rsa*`,
   `*.jks`, …), `shadow`/`passwd`/`authorized_keys`, а также имена с `credentials`, `api_key`,
   `private_key`, `access_token`. Плюс пользовательские `exclude`-глобы.
5. **Никаких `exec`/`spawn`.** Поиск реализован на Node `fs` API, а не через `grep`/`rg`, поэтому
   командной инъекции нет в принципе.
6. **Лимиты** на размер файла, число записей, глубину обхода, число просматриваемых файлов,
   количество совпадений, время поиска, длину строки и размер тела HTTP-запроса (256 КБ).
   Бинарные файлы (NUL в первых 8 КБ) не отдаются.
7. **Изоляция ключей.** Ключ привязан к проектам; чужой проект не виден в `list_projects` и
   сообщает `Unknown project` во всех остальных инструментах. Сравнение ключей —
   `timingSafeEqual` по sha256, с полным проходом по индексу (без утечки по времени).
8. **Без утечки путей.** В ответах и ошибках фигурируют только виртуальные пути. «Вне корня»,
   «запрещено» и «не существует» дают одинаковое сообщение `Path not found`, поэтому API нельзя
   использовать как оракул для разведки файловой системы. Неожиданные ошибки ФС логируются на
   сервере, а клиенту отдаётся обобщённое сообщение.
9. **Аудит-лог** в stdout: ключ (первые 6 символов хеша), инструмент, проект, виртуальный путь,
   статус, время. Содержимое файлов не логируется.

Рекомендации по эксплуатации: запускать под отдельным пользователем с правами только на чтение
нужных каталогов, в Docker монтировать данные как `:ro`, ключи хранить в `.env`
(в YAML — только `${VAR}`).

## Тесты

`npm test` (`node --test`) поднимает временное дерево-песочницу и проверяет модель безопасности:
обход каталогов, абсолютные пути, симлинк наружу, dotfile/ключевой материал, неразрешённое
расширение, превышение размера, бинарный файл, глубину обхода, изоляцию ключей и трансляцию glob.