Skip to main content
Glama
BigTolly

mcp-local-access

by BigTolly
README.md
**Languages:** [Русский](README.md) | [English](README-EN.md) 

# MCP (Local Access)

MCP-сервер позволяет облачным сервисам с поддержкой MCP, например Notion AI.
Предоставлять доступ к **одной папке** на вашем компьютере: читать файлы,
искать по ним, править, создавать и удалять, а также запускать разрешённые команды.

MCP сервер — это «руки», облачный сервис — «мозг».
Вместо копирования кода в чат и обратно, MCP позволяет работать с файлами на ПК.

**Зачем это нужно**

- Чат с ИИ видит реальный проект, а не пересказ: точные файлы, точные строки.
- Правки применяются сразу в файлы, с диффом в ответе.
- Рамки задаёте вы: одна корневая папка, список запрещённых путей, белый список
  команд, при желании — режим «только чтение».

**Статус.** Версия разработана для Windows и MacOS, но реально протестирована
только на Windows; ветки для macOS/Linux написаны, но не проверены.

![Popup](./copy-url-add-mcp.png)

---

## Содержание

1. [Как это работает](#1-как-это-работает)
2. [Структура проекта](#2-структура-проекта)
3. [Требования](#3-требования)
4. [Установка](#4-установка)
5. [Настройка](#5-настройка)
6. [Запуск и публикация](#6-запуск-и-публикация)
7. [Подключение клиента](#7-подключение-клиента)
8. [Настройка через JSON на примере accio.com](#8-настройка-через-json-на-примере-acciocom)
9. [Инструменты](#9-инструменты)
   - [Диалоговое окно](#Диалоговое-окно)
   - [Управление браузером (Browser Control)](#управление-браузером-browser-control)
10. [Rules.md — краткое описание вашего проекта](#10-rulesmd--краткое-описание-вашего-проекта)
11. [Права доступа](#11-права-доступа)
12. [Лимиты](#12-лимиты)
13. [Безопасность](#13-безопасность)
14. [Диагностика](#14-диагностика)
15. [FAQ](#15-faq)

---

## 1. Как это работает

```
Notion AI / claude.ai / chatgpt
        │  HTTPS + Bearer-токен
        ▼
cloudflared-туннель  или  свой web сервер с TLS
        │  HTTP внутри локальной сети
        ▼
server.py  (слушает host:port, отдаёт /mcp)
        │  все пути проверяются: песочница + права
        ▼
rootDir + явно разрешённые внешние папки Read/Write
```

Стек: Python + MCP SDK для Python (FastMCP) + uvicorn, транспорт Streamable HTTP
на пути `/mcp`, авторизация Bearer-токеном. Сервер ничего не знает о способе
публикации: он просто слушает `host:port`, а как этот порт оказался в интернете —
дело туннеля или прокси.

## 2. Структура проекта

| Путь | Что это |
| --- | --- |
| `server.py` | MCP-сервер: конфиг, песочница путей, права, регистрация инструментов, авторизация |
| `dialog-box.py` | диалог Ask me: интерфейс, звук и ожидание ответа пользователя |
| `config.json` | файл настроек, создайте его из `config.example.json` |
| `config.example.json` | шаблон настроек |
| `.env` | файл хранит токен `MCP_TOKEN`, который генерируется при первом запуске `server.py` |
| `requirements.txt` | четыре зависимости: `mcp`, `uvicorn`, `python-dotenv`, `websockets` |
| `README.md` | этот файл |
| `Rules.md` | базовые инструкции для ИИ, чтобы ИИ знал минимальный контекст по вашему проекту |
| `logs/` (внутри `rootDir`) | `cmd-NNN.log` — вывод команд, `audit.log` — журнал действий, `processes.json` — реестр процессов |

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

- Python 3.10 или новее (`python --version`).
- Способ отдать локальный порт наружу по HTTPS — одно из двух:
  - **cloudflared**, если своего веб сервера нет:
    - Windows: `winget install Cloudflare.cloudflared`
    - macOS: `brew install cloudflared`
  - **свой обратный прокси с TLS** (на базе Nginx, Caddy, Traefik) перед этим портом.

При первом запуске `server.py`, сам генерирует токен, установка для cloudflare
для этой задачи не нужна, он сам сам cloudflared.

## 4. Установка

```bash
cd local-access
python -m venv .venv

# Windows
.venv\Scripts\activate

# macOS / Linux
source .venv/bin/activate

pip install -r requirements.txt
```

## 5. Настройка

Все настройки лежат в `config.json`. Скопируйте шаблон и поправьте:

```bash

# Windows
copy config.example.json config.json

# macOS / Linux
cp config.example.json config.json
```

```json
{
  "rootDir": "C:/usr/mcp/local-access",
  "host": "0.0.0.0",
  "port": 8000,
  "readOnly": false,
  "jsonResponse": false,
  "rulesFile": "Rules.md",
  "logDir": "logs",
  "audit": true,
  "maxCommandTimeout": 600,
  "permissions": { "...": "см. раздел 11" }
}
```

| Ключ | Значение |
| --- | --- |
| `rootDir` | Основная папка файловой песочницы. Всё вне неё закрыто, кроме явно подключённых абсолютными правилами `Read`/`Write` папок. Обязательный параметр. |
| `host` | Интерфейс для прослушивания. По умолчанию `0.0.0.0` — годится и для туннеля на этой машине, и для прокси на другом хосте. `127.0.0.1` — принимать только локальные подключения. |
| `port` | Локальный порт; туннель или прокси смотрит сюда. По умолчанию 8000. |
| `readOnly` | `true` отключает `edit_file`, `multi_edit`, `create`, `delete`, `move` и `run_command`. |
| `jsonResponse` | `true` нужен только клиенту, который не умеет SSE-ответы. |
| `rulesFile` | Путь к файлу-путеводителю для модели. Необязательный, см. раздел 10. |
| `logDir` | Папка внутри `rootDir` для логов команд и журнала действий. По умолчанию `logs`. |
| `audit` | `false` выключает `logs/audit.log`. По умолчанию `true`. |
| `maxCommandTimeout` | Верхняя граница `timeout` для `run_command`, в секундах. По умолчанию 600. |
| `permissions` | Правила по путям и командам, см. раздел 11. |
| `browser` | Мост к расширению Browser MCP, см. «Браузер (Browser MCP)» в разделе 9. Необязательный. |

### Несколько файлов настроек

По умолчанию читается `config.json`. Другой файл задаётся ключом `--config`
(короткая форма `-c`). Путь указывать не нужно: файл всегда ищется в папке, где
лежит `server.py`.

```bat
rem start.cmd
start "MCP Server" cmd /k python C:\usr\mcp\local-access\server.py --config new.json
```

- Порядок выбора: `--config`, затем переменная `CONFIG_FILE`, затем `config.json`.
- Файл, указанный через `--config`, обязан существовать — иначе сервер не
  стартует с ошибкой `Config file not found`. Отсутствие `config.json` по
  умолчанию, как и раньше, не ошибка.
- Активный файл настроек всегда закрыт для модели, как бы он ни назывался:
  правило `*(**/config.json)` не покрыло бы `new.json`. Неактивные запасные
  конфиги защищайте сами, например `"*(**/new.json)"` в `deny`.
- Имя активного файла сервер печатает при старте в строке `config`.

В `.env` — только секретный токен:

```bash
# Windows
copy .env.example .env

# macOS / Linux
cp .env.example .env
```

Оставьте `MCP_TOKEN` пустым: при первом запуске сервер сгенерирует токен и
запишет его в эту же строку. Чтобы сменить токен, очистите значение и
перезапустите сервер.

## 6. Запуск и публикация

Нужны два терминала.

**Терминал 1 — сервер:**

```bash
python server.py
```

Он печатает эндпоинт, токен и действующие правила:

```
  local-mcp v0.1
  root      : C:\usr\mcp\my-project
  endpoint  : http://0.0.0.0:8000/mcp
  token     : 3f8c1d...
  read-only : no
  rules     : C:\usr\mcp\local-mcp-v0.1\Rules.md
  logs      : C:\usr\mcp\my-project\logs (audit.log: last 300 lines)
  processes : adopted 1, cleaned 4 old log(s)
  config    : C:\usr\mcp\local-mcp-v0.1\config.json
  perms     : defaultMode allow
    allow : List(**), Read(**), Edit(**), Create(**), Delete(**)
    deny  : *(**/.env), *(**/.env.*), *(**/config.json), *(**/secrets/**)
    run   : Run(npm run dev), Run(npm run build), Run(git status)
  browser   : ws://localhost:9009 - press 'Connect' in the Browser MCP extension

  WARNING: the server is listening on the whole local network.
  For a tighter setup set "host": "127.0.0.1" in config.json.
```

### Вариант A — быстрый туннель cloudflared

**Терминал 2:**

```bash
cloudflared tunnel --url http://localhost:8000 --protocol http2
```

После запуска в терминале можно увидеть публичный адрес:

Requesting new quick Tunnel on trycloudflare.com...
+--------------------------------------------------------------------------------------------+
|  Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):  |
|  https://verbal-univ-silent-bracelet.trycloudflare.com                                     |
+--------------------------------------------------------------------------------------------+

В данном случае это `https://verbal-univ-silent-bracelet.trycloudflare.com`.
Обратите внимание, что после каждого запуска url адрес меняется.


- `--protocol http2` заставляет туннель работать по TCP вместо QUIC/UDP. Без
  этого флага соединение может отваливаться с `timeout: no recent network
  activity`, а запросы — падать с ошибкой Cloudflare 1033.
- Туннель не нужно перезапускать вместе с сервером: он пробрасывает
  `localhost:8000`, и пока порт тот же, публичный адрес продолжает работать.
  Перезапуск нужен, только если вы поменяли `port` или туннель сам умер.
- Адрес быстрого туннеля меняется при каждом старте cloudflared — значит,
  URL в клиенте придётся обновлять.

### Вариант B — использование веб сервер перед сервером (постоянный адрес)

Для этого варианта нужно иметь:
- статический внешний IP-адрес
- веб сервер с TLS, на котором можно настроить обратный прокси
- домен, при этом для mcp можно использовать домен третьего уровня
- получить сертификат для работы https


В `config.json`:

- `"host": "0.0.0.0"` — значение по умолчанию и то, что нужно прокси: на
  `127.0.0.1` сервер доступен только с этой машины.
- `"port": 8000` — или любой свободный порт, на который смотрит прокси.
- После правки файла перезапустите `server.py` и проверьте строку `endpoint`.

На хосте с прокси нужен только `location /mcp` — публичный путь это дело прокси,
сервер всегда отдаёт `/mcp`.
Пример настройки обратного прокси на Nginx:

```nginx
server {
    listen 443 ssl;
    server_name mcp.example.com;

    location /mcp {
        proxy_pass http://192.168.1.50:8000/mcp;   # машина, где запущен server.py
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_buffering off;              # SSE должен стримиться, а не буферизоваться
        proxy_request_buffering off;
        proxy_read_timeout 3600s;
        chunked_transfer_encoding on;
    }
}
```

- Ключевые строки — `proxy_buffering off` и `proxy_request_buffering off`:
  транспорт стримит SSE, и буферизующий прокси подвешивает каждый ответ до
  таймаута.
- Заголовок `Authorization` проходит без изменений, токен доезжает как есть.
- Сервер сам переписывает заголовок `Host`, так что защита SDK от DNS-rebinding
  не требует дополнительных заголовков (ошибки 421 не будет).
- TLS терминируется на прокси, до сервера идёт обычный HTTP внутри локальной сети.

Адрес после этого постоянный: `https://mcp.example.com/mcp` вводится в клиент
один раз и переживает перезапуски и сервера, и прокси.

## 7. Подключение клиента

**Notion** (Settings → Connections → добавить MCP-сервер):

- URL: `https://ваш-адрес/mcp` — суффикс `/mcp` обязателен
- Аутентификация: Bearer-токен, префикс `Bearer`, сам токен — из терминала 1

## 8. Настройка через JSON на примере accio.com

Некоторые сервисы позволяют добавить MCP-сервер не по полям формы, а одним
блоком JSON. Например, в accio.com: Settings → MCP → секция **Custom** → кнопка
**Add custom MCP** (или «+ Add») → диалог **Add Custom MCP Server**. В диалоге
есть четыре вкладки Configuration Mode: `JSON | Stdio (Local) | HTTP | SSE`.
Откройте вкладку **JSON** и вставьте этот текст:

```json
{
  "mcpServers": {
    "Local Access": {
      "type": "http",
      "url": "https://your-host.example.com/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_TOKEN"
      }
    }
  }
}
```

Затем замените значения по умолчанию на свои:

- `url` — адрес вашего туннеля или домена, суффикс `/mcp` обязателен
- `Authorization` — префикс `Bearer` и ваш токен (`MCP_TOKEN` из `.env`, сервер
  печатает его при запуске)

Ключ `Local Access` — это просто имя подключения в интерфейсе, его можно
поменять. Тот же JSON подходит любому клиенту, который принимает конфигурацию
в формате `mcpServers`.

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

Пути внутри проекта задаются относительно `rootDir`. Для явно разрешённых
внешних папок доступны абсолютные пути и ссылки из проекта. Остальные внешние
пути и ссылки блокируются. См. «Внешние папки и сетевые пути» в разделе 11.

### Путеводитель

| Инструмент | Что делает |
| --- | --- |
| `project_guide()` | Возвращает `Rules.md`: контекст проекта и правила работы |

Тот же текст автоматически дописывается к первому результату инструмента в
диалоге, так что обычно вызывать его не нужно.

### Файлы

| Инструмент | Что делает |
| --- | --- |
| `list_files(path, recursive, max_depth)` | Листинг папки; `recursive` включает вложенные |
| `read_file(path, offset, limit)` | Файл с номерами строк, постранично для больших файлов |
| `search_text(pattern, path, glob, regex, case_sensitive, context, max_results)` | Поиск по проекту, отдаёт `путь:строка: текст` |
| `file_info(path)` | Размер, число строк, время изменения, текст или бинарник |
| `edit_file(path, old_string, new_string, replace_all)` | Точная замена, возвращает дифф |
| `multi_edit(path, edits, regex)` | Несколько замен в одном файле, всё или ничего |
| `create(path, type, content, overwrite)` | Новый файл или папка |
| `move(source, destination, overwrite)` | Переименование или перемещение |
| `delete(path, recursive)` | Безвозвратное удаление файла или папки |

- Шумные папки (`node_modules`, `.git`, `dist`, `build`, `.venv`, `venv`,
  `__pycache__`, `.next`, `.idea`) при листинге и поиске пропускаются всегда.
- У `delete` нет корзины: непустая папка требует `recursive=true`, сам корень
  удалить нельзя, а папка, внутри которой лежит запрещённый правилами файл,
  не удаляется целиком.
- `move` требует прав `Delete` на источник и `Create` на приёмник, поэтому
  защищённый файл нельзя вынести из-под защиты переносом.
- Один файл не стоит править двумя вызовами одновременно: `edit_file` и
  `multi_edit` читают и пишут файл целиком, параллельные вызовы затирают друг
  друга.

### Команды

| Инструмент | Что делает |
| --- | --- |
| `run_command(command, cwd, timeout, background, wait_for, idle_timeout, restart)` | Запускает команду внутри `rootDir` |
| `list_processes()` | Всё, что запущено в этой сессии: живое и завершённое |
| `check_process(id, lines, wait, wait_for)` | Состояние и свежий вывод одного процесса |
| `stop_process(id, stop_all)` | Убивает процесс вместе с детьми |

- Команды идут через системный шелл, поэтому `&&`, `||`, `|` и `;` работают.
  Каждое звено цепочки проверяется по правилам `Run(...)` отдельно, команда без
  подходящего правила отклоняется.
- Команда должна быть одной строкой: перевод строки — разделитель команд, так что
  многострочная строка выполнилась бы лишь частично. Склеивайте шаги через `&&`
  или положите их в скрипт.
- `background=false` (по умолчанию) — для команд, которые завершаются: вызов
  возвращает код выхода и вывод, процесс после этого гарантированно мёртв. По
  таймауту убивается всё дерево процессов.
- `background=true` — для долгоживущих команд вроде `npm run dev`. Процесс живёт
  после вызова; когда вернуть управление, решают `wait_for` (регулярное
  выражение) или `idle_timeout`, например `wait_for: "ready in|listening on"`.
- Вывод стримится в `logs/cmd-NNN.log`, а не держится в памяти, так что шумная
  сборка не разорвёт ответ. Длинный вывод возвращается началом и хвостом.
- Каждый запущенный процесс отслеживается, и короткий блок `[processes]`
  дописывается к **любому** результату инструмента, пока что-то работает или
  только что завершилось:

```
[processes]
  #1 running   npm run dev  (pid 24188, 96s, log logs/cmd-001.log)
  #2 exited (exit 0)  npm run build  (log logs/cmd-002.log)
```

  Так модель остаётся в курсе терминала: MCP-сервер не может сам прислать
  уведомление внутрь хода модели, поэтому состояние заново подкладывается
  контекстом при следующем вызове.
- Запуск команды, которая уже работает, отклоняется со ссылкой на её номер;
  `restart=true` сначала останавливает старый процесс.
- Вывод принудительно переводится в UTF-8 (`chcp 65001` на Windows,
  `PYTHONIOENCODING`, `NO_COLOR`), чтобы логи не превращались в кракозябры.
- Процессы убиваются деревом (`taskkill /T` на Windows, целая группа процессов
  на macOS/Linux, сначала `SIGTERM`, потом `SIGKILL`), потому что порт держит не
  `npm`, а его дочерний `node`.
- При выходе сервера все запущенные процессы останавливаются, так что `Ctrl+C`
  не оставляет висящих dev-серверов.
- Реестр процессов дублируется в `logs/processes.json`. Жёсткое убийство сервера
  (закрытая консоль, диспетчер задач) не выполняет очистку и оставляет детей
  живыми, поэтому при старте каждая запись сверяется с ОС: живой pid
  подхватывается обратно в реестр и показывается как `running (adopted)`,
  остальное считается завершённым, а их `cmd-NNN.log` удаляются. Номера
  продолжают расти, поэтому имена логов не конфликтуют. Подхваченный процесс
  можно смотреть и останавливать как обычно — теряется только точный код
  выхода, потому что сервер больше не владеет его хэндлом.

### Диалоговое окно

Инструмент `show-dialog-box()` открывает диалоговое окно **Ask me**, которое позволяет входить в зацикленный диалог с агентом.

![Popup](./dialog-box.png)

**Сценарий работы**

1. При работе с MCP Local Access в инструкциях агента добавляется правило: после завершения задачи или уточняющего вопроса в чате вызвать `show-dialog-box`.
2. Введите следующий запрос в появившемся окне и нажмите **OK**. Пустой ответ - завершают диалог.
3. Если пользователь не ответил в течение 50 сек (таймаут можно изменить), то инструмент перезапускает себя, если этого не делать, то инструмент завершится по таймауту.

**Настройка**

В файле `config.json` есть параметры, которые настраивают работу с этим инструментом, пример:

```json
"dialog": {
  "width": 500,
  "height": 300,
  "sound": true,
  "autoPrompt": true,
  "waitTimeoutSeconds": 50
}
```
Настройки задают начальную ширину и высоту окна, время перезапуска инструмента (менять нужно осознанно), включить звук при вызове инструмента, нужно ли вызывать инструмент после ответа.

**Автоматическое указание агенту**

Инструкция передаётся в MCP-поле `instructions` и дополнительно с первым
результатом инструмента; повторно — при изменении файла правил или после
длительного перерыва.

Параметр принимает только JSON `true`/`false`. Если параметр
отсутствует, указания включены.

**Ожидание и таймауты**

Когда время одного вызова истекает, инструмент возвращает `Call show-dialog-box again to continue waiting for the same window.` Агент должен вызвать его снова: продолжится ожидание **того же
окна**, без дубликата и без потери набранного текста. Если ответ уже отправлен, следующий вызов заберёт сохранённый результат. Одновременно ожидающий второй вызов получает сообщение, что ожидание уже идёт.

### Управление браузером (Browser Control)

Инструменты MCP Local Access `browser_*` управляют **вашим** браузером, используя расширение Browser Control.
`server.py` поднимает WebSocket на `ws://localhost:9009`, к которому подключается расширение.
Браузер могут использовать облачные агенты и работать с сайтами под вашей авторизацией.

**Как включить**

1. Установите в Chrome расширение Browser Control.
2. В браузере откройте нужную вкладку, нажмите на иконку Browser Control и кнопку **Connect this tab**.
   Либо агент может сам открыть Chrome и подключить свою вкладку без дополнительных действий со стороны пользователя.

| Инструмент | Что делает |
| --- | --- |
| `browser_status()` | Показывает, слушает ли сервер порт, подключена ли вкладка и есть ли агентская сессия; выводит расширенную диагностику |
| `browser_launch(url)` | Запускает Chrome с вашим профилем и подключает вкладку агента без Connect |
| `browser_close(force)` | Закрывает вкладку агента; Chrome, запущенный агентом, завершает по PID |
| `browser_navigate(url)` | Открывает URL, возвращает снимок страницы |
| `browser_go_back()` / `browser_go_forward()` | Назад / вперёд по истории |
| `browser_snapshot()` | ARIA-снимок страницы с `ref` элементов |
| `browser_click(element, ref)` | Клик по элементу |
| `browser_hover(element, ref)` | Наведение на элемент |
| `browser_drag(start_element, start_ref, end_element, end_ref)` | Перетаскивание |
| `browser_type(element, ref, text, submit)` | Ввод текста, `submit=true` — Enter после |
| `browser_select_option(element, ref, values)` | Выбор в выпадающем списке |
| `browser_press_key(key)` | Нажатие клавиши, например `Enter`, `ArrowLeft` |
| `browser_wait(time)` | Пауза в секундах, отклоняет отрицательные, бесконечные и превышающие 60 секунд значения |
| `browser_screenshot()` | Скриншот вкладки (PNG) |
| `browser_get_console_logs()` | Возвращает логи и структурированную информацию о состоянии их сбора |
| `browser_cdp(method, params)` | Одна команда Chrome DevTools Protocol для подключённой вкладки (`DOM.getDocument`, `Performance.getMetrics`, ...). `Browser.*`, `Target.*` и отключение Runtime/Log/Page запрещены, как и куки/хранилище всего браузера и `Network.loadNetworkResource` |
| `browser_evaluate(expression, await_promise)` | JavaScript в странице, как в консоли DevTools; возвращает значение или исключение |
| `browser_cdp_events(action, domain, limit)` | Подписка на события домена CDP (`subscribe/unsubscribe/read/clear`) |
| `browser_diagnostics()` | Реальная проверка debugger, подписок, ошибок и потерь сбора |
| `browser_console(limit, level, clear)` | Console API, исключения и браузерные Log-сообщения; очистка только явно |
| `browser_network(action, request_id, limit, part, max_chars)` | `start/stop/clear/list/detail/body`; сеть выключена по умолчанию |
| `browser_capture(mode, scale)` | `viewport/full_page`, `original/reduced`; PNG веб-страницы, не всего окна Chrome |

Сценарий для модели: `browser_snapshot` → взять `ref` нужного элемента → `browser_click` / `browser_type`. Действия возвращают свежий снимок страницы.

В `readOnly` у `browser_cdp` разрешены только читающие методы (`get*`, `describe*`, `query*`, `search*`, `resolve*`); `browser_evaluate` и подписки запрещены. В журнал аудита записываются только имя метода и длина выражения, но не параметры и не код.

Сервер отказывает в методах, которые выходят за пределы вкладки (`CDP_DENIED_DOMAINS` / `CDP_DENIED_METHODS` в `server.py`): `Browser.*`, `Target.*`, `Network.getAllCookies`, `Storage.getCookies/setCookies/clearCookies`, `Network.clearBrowserCookies/clearBrowserCache`, `Storage.clearDataFor*`, `Network.loadNetworkResource`, `Network.getCookies` с `urls`, `Page.close/crash`, а также `Page.navigate` на `javascript:` и, по умолчанию, на `file://` (если `browser.allowFileUrls` не установлен в `true`). Browser Control дополнительно ограничивает `Network.setCookie(s)/deleteCookies` и методы с `origin`/`securityOrigin`/`storageKey` сайтом текущей вкладки.


**Настройка MCP Local Access** (необязательно; ниже приведены значения по умолчанию):

```json
"browser": {
  "enabled": true,
  "host": "localhost",
  "port": 9009,
  "timeoutSeconds": 30,
  "allowFileUrls": false,
  "chromePath": "",
  "userDataDir": "",
  "profileDirectory": "",
  "launchArgs": [],
  "launchTimeoutSeconds": 30,
  "closeTimeoutSeconds": 10
}
```

| Ключ | Значение |
| --- | --- |
| `enabled` | `false` отключает мост: порт не слушается, инструменты отвечают отказом |
| `host` | Интерфейс WebSocket. `localhost` — только эта машина; наружу не открывайте |
| `port` | Порт, к которому подключается расширение. Browser MCP знает только `9009`; в Browser Control порт меняется в popup |
| `timeoutSeconds` | Сколько ждать ответа расширения на одну команду |
| `allowFileUrls` | По умолчанию `false`: сервер запрещает переходы на `file://` через `browser_navigate` / `browser_cdp Page.navigate`. Для разрешения доступа необходимо установить `browser.allowFileUrls` в `true` в `config.json`, **а также** установить флажок «Allow file:// pages» во всплывающем окне Browser Control и разрешить расширению «Доступ к URL файлов» в настройках Chrome. Эти разрешения дополняют, а не заменяют друг друга. Такой доступ позволяет агенту читать **любой доступный браузеру файл компьютера** в обход `rootDir` и правил Deny |
| `chromePath` | `browser_launch`: путь к `chrome.exe` / `Google Chrome`. Пусто: ищется сам (реестр App Paths, Program Files, LocalAppData; `/Applications`; `google-chrome`/`chromium` в PATH) |
| `userDataDir` | `browser_launch`: `--user-data-dir`. Пусто: ваш обычный профиль (сайты остаются залогинены) |
| `profileDirectory` | `browser_launch`: `--profile-directory`, например `Profile 1`. Пусто: профиль по умолчанию |
| `launchArgs` | `browser_launch`: дополнительные флаги Chrome (список строк) |
| `launchTimeoutSeconds` | Сколько секунд `browser_launch` ждёт подключения расширения (минимум 5) |
| `closeTimeoutSeconds` | Сколько `browser_close` ждёт, пока Chrome закроется сам после закрытия последней вкладки, прежде чем завершать его по PID |

**Важно**

- В режиме `readOnly` доступны чтение страницы, снимки и чтение диагностики.
  Действия браузера, очистка журналов и включение/выключение сетевого сбора запрещены.
- В `logs/audit.log` записываются переходы, клики и события ввода; сам введённый текст не записывается (это может быть пароль) — только его длина.
- Браузер не входит в песочницу `rootDir`: агент видит всё, что открыто в подключённой вкладке, и действует от вашего имени.
- Для `browser_navigate` и `Page.navigate` сервер всегда запрещает переходы на `javascript:`, а на `file://` — только при `browser.allowFileUrls=false` (значение по умолчанию). Разрешение на сервере не отменяет отдельные ограничения Browser Control и Chrome.


### Агентский браузер: `browser_launch` / `browser_close`

Установить расширение Browser Control может только пользователь.
Агент может сам открыть Chrome и подключить свою вкладку.
Если `userDataDir` пуст, Chrome запускается с вашим профилем и может открывать сайты под вашей учётной записью.

Варианты запуска Chrome:
- Chrome не запущен: агент запускает его, а после работы закрывает свою вкладку. Если других открытых вкладок нет, агент завершает Chrome по PID.
- Chrome уже открыт: новая **вкладка** открывается в последнем активном окне, а по завершении работы агент закрывает только свою вкладку.

Для работы с браузером вкладка агента не обязана быть активной.
Агент может получать снимки страницы, нажимать на элементы, переходить по ссылкам и делать скриншоты даже в свёрнутом окне.
Страница при этом «спит», и её обновления происходят медленнее.

Сбор ограничен основной CDP-целью выбранной вкладки; дочерние CDP-сессии не агрегируются.
Журналы хранятся в памяти и теряются при перезапуске worker, отключении или смене вкладки.

Большие снимки ограничены 20 000 пикселей по стороне и бюджетом 32 млн пикселей
с учётом DPR/масштаба; возможны ограничения конкретного Chrome, sticky и lazy loading.
Перед выпуском требуется фактическая проверка в Chrome: mock-тесты её не заменяют.

## 10. Rules.md — краткое описание вашего проекта

`Rules.md` — (опционально) необязательный файл, кратко опишите свой проект,
или можете указать путь к файлам документации по вашему проекту.
Смысл простой — не объяснять модели детали о проекте в каждом новом диалоге.

- Название файла и путь к этому файлу, можно изменить в переменной `rulesFile`
  в `config.json`. Если переменная не задана, то берется файл `Rules.md`,
  расположенный в папке указанной в `rootDir`.
- Файл перечитывается при изменении, перезапуск не нужен: новый текст приходит со
  следующим результатом инструмента и в следующей сессии клиента.

## 11. Права доступа

Правила живут в блоке `permissions` файла `config.json` и записываются в стиле
Claude Code: `Инструмент(glob-путь)`.

```json
{
  "permissions": {
    "defaultMode": "allow",
    "allow": ["List(**)", "Read(**)", "Edit(**)", "Create(**)", "Delete(**)"],
    "deny": [
      "*(**/.env)",
      "*(**/.env.*)",
      "*(**/config.json)",
      "*(**/secrets/**)",
      "*(**/*.pem)",
      "*(**/*.key)",
      "Edit(**/.git/**)",
      "Create(**/.git/**)",
      "Delete(**/.git/**)"
    ]
  }
}
```

- Глаголы: `List`, `Read`, `Edit`, `Create`, `Delete`, `Run`, внешний `Write` или `*` для всех
  путевых глаголов. Соответствие инструментам: `List` и `Read` вместе определяют,
  что показывает `list_files`; `Read` покрывает ещё `search_text` и `file_info`;
  `Edit` покрывает `multi_edit`; `Create` — `create` и приёмник `move`; `Delete` —
  `delete` и источник `move`.
- `Run` стоит особняком: его аргумент — шаблон команды, а не путь, поэтому
  `*(**)` его никогда не выдаёт. См. «Правила для команд» ниже.
- `defaultMode: "allow"` — внутри `rootDir` разрешено всё, что не запрещено (чёрный список). Внешние папки всегда требуют явного абсолютного `Read`/`Write`.
- `defaultMode: "deny"` — работают только пути из `allow` (белый список).
- **`deny` всегда сильнее `allow`.**
- Обычные шаблоны задаются относительно `rootDir`, разделитель — `/`, поддерживаются
  `*` (один сегмент), `**` (любая глубина) и `?` (один символ).
- Шаблоны привязаны к корню, поэтому `.env` совпадает **только** с файлом в
  корне. Чтобы накрыть все подпапки, нужен префикс `**/` — `**/.env`.
- `secrets/**` скрывает и саму папку `secrets`.
- Запрещённые элементы не показываются в `list_files`, вместо них выводится
  число скрытых записей.
- Если блока `permissions` нет, действуют встроенные значения: разрешено всё,
  кроме `**/.env`, `**/.env.*`, `**/secrets/**`, `**/*.pem`, `**/*.key`, плюс
  запрет записи и удаления внутри `**/.git/**`.
- Правила читаются при старте — после правки перезапустите сервер.
- `*(**/config.json)` доступ к файлу закрыт для модели, чтобы не переписала
  собственные права.


### Внешние папки и сетевые пути

Чтобы подключить конкретную папку вне `rootDir`, владелец добавляет в
`permissions.allow` абсолютное правило `Read(папка)` или `Write(папка)`.
Корень проекта менять не нужно. Это разрешение папки и всего её содержимого,
а не всего диска.

```json
{
  "permissions": {
    "defaultMode": "allow",
    "allow": [
      "List(**)", "Read(**)", "Edit(**)", "Create(**)", "Delete(**)",
      "Read(J:/src/**)",
      "Write(//debian/share/work)"
    ],
    "deny": [
      "*(**/.env)", "*(**/.env.*)", "*(**/config.json)",
      "*(**/secrets/**)", "*(**/*.pem)", "*(**/*.key)",
      "Delete(//debian/share/work/important/**)"
    ]
  }
}
```

| Внешнее правило | Листинг, поиск, сведения и чтение | Изменение и создание | Удаление и перемещение |
| --- | --- | --- | --- |
| `Read(J:/src)` | Да | Нет | Нет |
| `Write(J:/src)` | Да | Да | Да, с проверкой прав источника и назначения |

- `Read(J:/src)` и `Read(J:/src/**)` равнозначны: включают саму папку и любую
  глубину вложенности. То же для `Write`. Другие шаблоны в разрешении внешней
  папки запрещены: укажите конкретный абсолютный путь.
- Поддерживаются дисковые пути Windows (`J:/src`, `J:/Просто так`), UNC
  (`//debian/share/src`) и абсолютные пути Linux/macOS (`/mnt/share/src`).
  Используйте `/`; в JSON обратный слеш требует экранирования, например
  `"Read(J:\\src)"` в виде JSON должен содержать два обратных слеша.
  На Linux UNC не подключает Samba автоматически: сначала смонтируйте ресурс
  средствами ОС и разрешите его локальный путь.
- `Read` с относительным шаблоном сохраняет старое значение внутри проекта.
  `Write` в `allow` используется только с абсолютной внешней папкой.
- Широкие относительные `Edit(**)`, `*(**)` и `defaultMode: "allow"` не дают
  внешних прав и не повышают внешний `Read` до `Write`.
- Можно обращаться через `src/main.py`, если `rootDir/src` — ссылка на
  разрешённую папку, либо напрямую через `J:/src/main.py` или
  `//debian/share/src/main.py`. Абсолютные пути внутри самого `rootDir`
  по-прежнему не принимаются: используйте относительные.
- Проверяется конечная цель ссылок. Выход в неразрешённую папку, соседний
  `src-other`, неполный UNC, `J:src`, device-пути и Windows alternate data
  streams не разрешаются. Листинг и поиск пропускают запрещённые папки и
  не зацикливаются на ссылках.
- `deny` проверяется по пути ссылки, конечной цели и пути относительно
  подключённой папки. Запреты вроде `*(**/.env)` и `*(**/secrets/**)` действуют
  также снаружи; ссылка с невинным именем не открывает запрещённую цель.
  Запрет папки действует и на её содержимое. `Write(...)` в `deny` блокирует
  все файловые операции; для запрета только удаления используйте `Delete(...)`.
- При вложенных разрешениях наиболее конкретная папка определяет уровень:
  `Write(J:/src)` плюс `Read(J:/src/vendor)` делает `vendor` доступной только
  для чтения. На одной и той же папке `Write` повышает `Read`.
- `readOnly: true` блокирует все инструменты изменения даже при `Write`.
  Сам `rootDir` и корни подключённых папок нельзя удалить или переместить.
  Изменение файла через символьную ссылку сохраняет ссылку; удаление обычной
  символьной ссылки удаляет ссылку, а не её цель.
- Доступ зависит и от Windows/SMB: учётная запись сервера должна видеть ресурс.
  UNC обычно надёжнее буквы диска, привязанной к пользовательскому сеансу.
  При старте реальный путь разрешения фиксируется; подключите ресурс заранее.
  Если диск переподключён на другой ресурс или изменена цель ссылки-разрешения,
  перезапустите сервер и перепроверьте настройки.
- `Read`/`Write` не выдают `Run`. Внешний `cwd` команды требует `Write` и
  отдельного разрешения `Run`. Однако команда, запущенная даже внутри проекта,
  не ограничена файловой песочницей и может обращаться к другим путям.
- Эти проверки не заменяют изоляцию ОС: локальный процесс, одновременно
  меняющий ссылки, и разрешённые на стороне Samba переходы за пределы share
  требуют отдельных ограничений ОС/SMB.

После изменения кода или правил перезапустите сервер. `config.example.json`
по умолчанию не открывает никаких внешних папок; реальные пути добавляет владелец
в свой `config.json`.

Проверка реализации без чтения рабочего конфига и секретов:

```bash
python -m unittest discover -s tests -v
```

Тесты используют временные папки; UNC и сопоставление сетевого диска дополнительно
проверяются моделированием, без подключения к реальному Samba-ресурсу.

### Правила для команд

Команды — всегда белый список. `Run(...)` принимает шаблон команды, где `*`
означает любой текст; всё остальное сравнивается буквально, пробелы
сжимаются, регистр игнорируется. Если ни одного правила `Run` нет,
`run_command` отказывает во всём.

```json
{
  "permissions": {
    "allow": [
      "Run(npm run dev)",
      "Run(npm run build)",
      "Run(npm install)",
      "Run(git status)",
      "Run(git diff*)",
      "Run(python *)",
      "Run(pytest*)"
    ],
    "deny": ["Run(*rm -rf*)"]
  }
}
```

- Цепочка разбивается по `&&`, `||`, `|`, `;` и переводам строк, и каждый сегмент
  должен сам совпасть с правилом из `allow`. Для `npm run build && git status`
  нужны оба правила; `npm run build && rm -rf /` упадёт на втором сегменте.
- Разбиение учитывает кавычки, поэтому `git commit -m "fix | bug"` — один сегмент.
- `deny` и здесь сильнее, а `defaultMode: "allow"` на команды **не**
  распространяется.

**Насколько это обеспечивает безопасность.** Не сильно, и это осознанный компромисс.
Белый список спасает от случайностей и опечаток, а не от целенаправленной атаки: одного
разрешённого `python *` или `npm run *` достаточно, чтобы сделать всё, что может
обычная программа, включая выход за `rootDir` — песочница путей ограничивает
только файловые инструменты, но никогда не дочерний процесс. Держите список
коротким и конкретным и относитесь к запуску команд как к «я доверяю этому
клиенту свою учётную запись», а не как к песочнице.

Пример белого списка: читать только `src` и Markdown, писать только в
`src/generated`:

```json
{
  "permissions": {
    "defaultMode": "deny",
    "allow": ["List(src/**)", "Read(src/**)", "Read(*.md)", "Create(src/generated/**)", "Edit(src/generated/**)"]
  }
}
```

## 12. Лимиты

Зашиты в `server.py` (константы в начале файла), меняются правкой кода:

| Лимит | Значение |
| --- | --- |
| Размер одного ответа | 100 000 символов, дальше обрезка |
| Чтение/запись содержимого файла | 1 МБ |
| Записей в листинге | 1000 |
| Таймаут синхронной команды | 60 с по умолчанию, максимум — `maxCommandTimeout` (600 с) |
| Ожидание фоновой команды | `idle_timeout` 15 с по умолчанию |
| Одновременно фоновых процессов | 8 |
| Вывод команды в одном ответе | 20 000 символов |
| Завершённый процесс в реестре | 10 минут |
| `audit.log` | последние 300 строк |
| `Rules.md` | 20 000 символов |

## 13. Безопасность

- Любой, у кого есть публичный URL **и** токен, может читать, менять и удалять
  внутри `rootDir` и явно подключённых внешних папок — в пределах разрешённых операций и правил `deny`.
- `run_command` — самая широкая дыра: дочерний процесс не связан песочницей
  путей, поэтому разрешённые `python *`, `node *` или `npm run *` могут добраться
  до всего, до чего дотягивается ваша учётная запись. Держите список `Run`
  коротким.
- Останавливайте фоновые процессы, когда закончили (`stop_process(stop_all=true)`);
  остановка сервера по `Ctrl+C` убивает их тоже. Жёсткое убийство — нет, но при
  следующем старте живые процессы подхватываются обратно.
- Выключайте туннель, когда он не нужен.
- Токен меняется так: очистить `MCP_TOKEN` в `.env` и перезапустить сервер.
- `rootDir` держите настолько узким, насколько позволяет задача: родительская
  папка втягивает в песочницу и сам этот проект вместе с его `.env`.

**Если нужно строже:** включите подтверждение вызовов на стороне клиента, сузьте
`rootDir`, поставьте `"readOnly": true` или уберите все правила `Run`.

## 14. Диагностика

| Симптом | Причина |
| --- | --- |
| `rootDir is not set` | Нет `config.json` или в нём не задан `rootDir` |
| `Config file not found` | Файла из `--config` нет в папке `server.py` |
| `browser : NOT started - port 9009 is busy` | Порт занят другим Browser MCP (`npx @browsermcp/mcp`); закройте его |
| `No connection to the browser extension` | Во вкладке не нажат **Connect** в расширении Browser MCP |
| `the active config file is protected` | Модель обратилась к активному файлу настроек — он закрыт всегда |
| 401 от клиента | Токен не совпадает или заголовок называется не `Authorization` |
| 421 `Invalid Host header` | Старый `server.py` без перезаписи `Host` |
| 406 Not Acceptable | Клиент не отправил заголовок `Accept`, показанный в разделе 8 |
| Cloudflare 1033 | Туннель потерял соединение; перезапустите с `--protocol http2` |
| Клиент не видит инструментов | В URL нет суффикса `/mcp` |
| Новый инструмент не появился | Клиент закэшировал старый список; переподключите коннектор |
| Ответы подвешиваются до таймаута | Прокси буферизует SSE: `proxy_buffering off` |
| `Permission denied` | Путь совпал с правилом `deny` в `config.json` |
| `Permission denied: Run(...)` | Ни одно правило `Run` не подошло команде или звену цепочки |
| `Already running as #N` | Та же команда ещё жива: посмотрите её, остановите или передайте `restart=true` |
| `Timed out after Ns` | Долгоживущая команда запущена без `background=true` |
| `command must be a single line` | Переводы строк разделяют команды; склейте шаги через `&&` |
| `Absolute paths inside rootDir are not allowed` | Для проекта используйте относительный путь |
| `Path escapes the allowed root directory or external grants` | Внешняя цель не разрешена абсолютным `Read`/`Write` или ссылка вышла из разрешённой папки |
| `external directory is read-only` | Для изменения внешней папки нужно явное `Write`, внутренних `Edit(**)` недостаточно |

## 15. FAQ

**А что если я хочу несколько rootDir?**

Один процесс — один основной корень. Для конкретных внешних папок добавьте
абсолютные `Read`/`Write` из раздела 11. Другие варианты:

- Указать общую родительскую папку и сузить доступ правами, например
  `List(project-a/**)`, `Read(project-a/**)`, `Edit(project-a/**)` и то же для
  `project-b`. Просто и работает, но всё лежит в одной песочнице.
- Запустить второй экземпляр сервера со своим конфигом и портом: конфиг
  задаётся ключом `--config` (файл ищется рядом с `server.py`) или переменной
  `CONFIG_FILE`, а порт — в самом конфиге.

  ```bash
  python server.py --config config-b.json
  CONFIG_FILE=/path/to/config-b.json python server.py     # macOS / Linux
  set CONFIG_FILE=C:\path\to\config-b.json && python server.py   # Windows
  ```

  Мост к браузеру может работать только в одном экземпляре (порт `9009` один):
  в остальных поставьте `"browser": { "enabled": false }`.

  Каждому экземпляру нужен свой публичный адрес (второй туннель или второй
  `location` в Nginx) и своё подключение в клиенте. Токен берётся из `.env`
  рядом с `server.py`, так что для разных токенов нужны разные копии проекта.

**Обязателен ли cloudflared?**

Нет. Нужен любой способ отдать порт наружу по HTTPS: свой Nginx/Caddy/Traefik,
именованный туннель Cloudflare, любой другой туннель. Сервер о нём ничего не
знает.

**Можно ли обойтись без публикации наружу?**

Notion и claude.ai работают через интернет, им нужен публичный HTTPS-адрес.
Локальные клиенты (Cursor и другие, запущенные на этой же машине) могут
подключаться прямо к `http://127.0.0.1:8000/mcp` — тогда поставьте
`"host": "127.0.0.1"`.

**Может ли модель выйти за пределы rootDir?**

По умолчанию файловыми инструментами — нет. Владелец может явно разрешить
конкретные внешние папки абсолютными `Read`/`Write`; другие внешние пути
и ссылки остаются закрыты. Через `run_command` — да: дочерний процесс
песочницей не ограничен. Если это неприемлемо, не давайте правил `Run` или
включите `readOnly`.

**Как вообще запретить любые изменения?**

`"readOnly": true` — остаются только чтение, поиск и листинг. Промежуточный
вариант: оставить `Edit`/`Create` на рабочую папку и запретить остальное через
`deny`.

**Нужен ли Rules.md?**

Нет, сервер работает и без него. Но с ним не приходится в каждом новом диалоге
объяснять, что это за проект и как в нём принято работать. См. раздел 10.

**Работает ли это на macOS и Linux?**

Код есть (`/bin/sh`, `start_new_session`, убийство группы процессов), но проверен
он только на Windows. Считайте первый запуск на macOS/Linux тестовым.

**Модель удалила нужный файл — как откатить?**

Никак средствами сервера: корзины нет, удаление безвозвратное. Держите проект под
git, а в `logs/audit.log` можно посмотреть, что именно произошло.
Рекомендуется настроить запуск бекапа проекта перед каждой новой задачей.

**Почему клиент спрашивает подтверждение на каждый вызов?**

Это поведение клиента, сервер на него не влияет. В Notion подтверждения
настраиваются в свойствах подключения; права сервера работают независимо от
того, что вы нажали.

**Токен утёк. Что делать?**

Очистить значение `MCP_TOKEN` в `.env`, перезапустить сервер (он сгенерирует
новый токен), вписать новый токен в клиента. Старый перестаёт работать сразу.

**Почему `logs/` внутри rootDir, а не рядом с сервером?**

Чтобы модель могла сама прочитать вывод команды через `read_file`. Побочный
эффект: добавьте `logs/` в `.gitignore`.