MCP GitHub Server
by LeonidYasin
README.md
# MCP GitHub Server
[](pyproject.toml)
[](TOOLS.md)
[](pyproject.toml)
Расширяемый MCP HTTP-сервер с модульной архитектурой и автоматическим обнаружением инструментов. Изначально вырос из обёртки над GitHub API, сейчас — универсальный набор инструментов для агента.
Сервер реализует **MCP Streamable HTTP transport** (JSON-RPC 2.0 поверх `POST /mcp`).
---
## 📌 Source of truth
| Что | Где источник правды |
|-----|---------------------|
| **Версия** | `pyproject.toml` (single-source; читается динамически в коде) — **0.4.3** |
| **Список инструментов** | рантайм-реестр: `list_my_tools`; полный каталог — [`TOOLS.md`](TOOLS.md) — **110** |
| **Схема инструмента** | `describe_tool(name)` |
| **Документация** | ревизия **5** от **2026-09-27** |
> **Ревизия документации: 5 · 2026-09-27.** Инкрементируется при каждом значимом изменении README/ROADMAP/TOOLS/SANDBOX. При расхождении доков и рантайма — прав рантайм (код), доки приводим к нему. Ревизия 4: добавлен раздел «Продакшен (Gunicorn + Caddy)». Ревизия 5: добавлен подраздел «Пошаговый деплой на Ubuntu VPS», а также Windows/Waitress и примечание про PEP 668/venv.
---
## Возможности
**Всего инструментов: 110** (полный каталог — [`TOOLS.md`](TOOLS.md)). Инструменты разложены по подпакетам `mcp_server/tools/`. Каждый подпакет — независимая категория, которую можно включать и отключать отдельно.
### 🐙 `github/` — GitHub API (82)
Файлы, коммиты, ветки, PR (включая review-threads), issues, releases, tags, gists, Actions/workflows, search, security-алерты.
| Группа | Инструменты |
|--------|-------------|
| Файлы | `get_file_contents`, `create_or_update_file`, `create_or_update_file_with_sha`, `create_or_update_binary_file`, `delete_file`, `move_file`, `replace_in_file`, `read_file_chunk`, `read_full_file`, `grep_file`, `list_directory` |
| Коммиты | `list_commits`, `get_commit_status`, `get_commit_diff`, `get_file_blame` |
| Ветки / сравнение | `list_branches`, `get_branch`, `create_branch`, `delete_branch`, `compare_branches`, `merge_branches` |
| PR | `create_pull_request`, `list_pull_requests`, `get_pull_request`, `update_pull_request`, `merge_pull_request`, `close_pull_request`, `add_pr_comment`, `request_pr_review`, `get_review_threads`, `resolve_review_thread`, `unresolve_review_thread` |
| Issues / метки | `create_issue`, `list_issues`, `get_issue`, `close_issue`, `add_issue_comment`, `add_labels` |
| Releases / Tags | `list_releases`, `create_release`, `get_latest_release`, `list_tags`, `create_tag`, `delete_tag` |
| Gists | `create_gist`, `list_gists`, `get_gist`, `update_gist` |
| Actions / Workflows | `dispatch_workflow`, `rerun_workflow`, `rerun_failed_jobs`, `cancel_workflow`, `list_workflows`, `list_workflow_runs`, `get_workflow_by_file`, `list_artifacts`, `download_artifact`, `get_workflow_run_status`, `get_workflow_run_steps`, `get_workflow_logs_preview`, `get_run_logs_by_step`, `get_step_logs_via_checks`, `read_run_logs_offset`, `grep_run_logs`, `grep_workflow_logs` |
| Search | `search_code`, `search_commits`, `search_issues`, `search_repositories` |
| Security | `list_dependabot_alerts`, `list_code_scanning_alerts`, `list_secret_scanning_alerts` |
| Repo info / admin | `get_repo_info`, `get_repo_languages`, `get_repo_topics`, `list_repo_contributors`, `update_repo_info`, `get_repo_tree` |
| Batch | `push_multiple_files`, `read_multiple_files` |
> **Большие файлы.** `get_file_contents` отдаёт файл целиком — для больших файлов клиент может обрезать ответ (`[truncated]`). Используйте `read_file_chunk(owner, repo, path, ref, offset, limit)`: жёстко ограниченный кусок строк (≤ 32 KB). Поиск по файлу — `grep_file(...)`. Файл целиком без угадывания чанков — `read_full_file(...)` (сам подбирает размер, склеивает части, ставит `(конец файла)` либо предупреждение о бюджете `max_bytes`).
>
> **Прямые ссылки на файл.** `get_file_contents`/`read_full_file` в начале ответа возвращают `🔗` (blob) и `📄` (raw) ссылки.
### 🏗️ `build/` — сборка и отладка (13)
`watch_build`, `auto_fix_build`, `get_android_build_error`, `get_ios_build_error`, `get_run_logs_by_step`, `get_step_logs_via_checks`, `get_latest_workflow_error`, `get_workflow_run_logs`, `get_full_workflow_logs`, `get_workflow_by_file`, `list_workflow_runs`, `get_latest_run_id`, `get_workflow_run_steps`
### 🌐 `web/` — веб (4)
`web_fetch`, `web_search` (DuckDuckGo HTML, без API-ключа), `rss_read`, `html_to_markdown`
### 🧰 `utils/` — утилиты и данные (15)
`base64_encode`, `base64_decode`, `hash_text`, `json_format`, `json_query`, `uuid_generate`, `timestamp_now`, `date_convert`, `regex_test`, `text_diff`, `csv_parse`, `csv_generate`, `yaml_to_json`, `json_to_yaml`, `markdown_to_html`
### 🧭 `meta/` — мета-инструменты (2)
`list_my_tools`, `describe_tool`
### 🧠 `synapse/` — находимость людей (`ENABLE_SYNAPSE=1`)
`publish_profile`, `search_people`, `propose_contact`, `save_note`, `search_notes`, `index_github`. Батч 7a — keyword-MVP; батч 7b (эмбеддинг-матчинг, item-модель offer/want) — в работе. Протокол — [`docs/synapse-protocol.md`](docs/synapse-protocol.md).
### 🔒 `localfs/` — локальные файлы (4, по умолчанию выключено)
`read_local_file`, `write_local_file`, `list_local_dir`, `search_in_files`. Флаг `ENABLE_LOCAL_TOOLS=1`, корень `LOCAL_TOOLS_ROOT`. См. [`SANDBOX.md`](SANDBOX.md).
### 🔒 `localgit/` — git в workspace (6, по умолчанию выключено)
`git_status`, `git_log`, `git_diff`, `git_commit`, `git_push`, `git_pull`. Тот же флаг.
### 🔒 `shell/` — sandboxed shell / python (2, по умолчанию выключено)
`run_command`, `run_python`. Флаг `ENABLE_LOCAL_SHELL=1`, выполнение только внутри `LOCAL_TOOLS_ROOT`. См. [`SANDBOX.md`](SANDBOX.md).
---
## Установка
```bash
git clone https://github.com/LeonidYasin/mcp-server.git
cd mcp-server
pip install -e .
```
Либо вручную: `pip install flask httpx python-dotenv flask-cors`.
## Запуск
### Локально / для разработки
```bash
python -m mcp_server.server
```
Сервер слушает `http://0.0.0.0:3001`, MCP-эндпоинт — `POST /mcp`. Health-check — `GET /health` (показывает `tool_count`, список инструментов и диагностику последнего запроса).
> ⚠️ **Это dev-режим.** Внутри используется встроенный сервер Werkzeug, который сам Flask не рекомендует для продакшена: он однопоточный, не масштабируется и не рассчитан на публичный доступ. Для локальной работы и отладки — ок, для боевого сервера — нет (см. раздел «Продакшен» ниже).
Токен GitHub передаётся заголовком `Authorization: Bearer <token>`.
### Продакшен (Gunicorn + Caddy)
Для боевого развёртывания используется классическая слоёная схема:
```
Интернет ──HTTPS(443)──▶ Caddy ──HTTP(127.0.0.1:3001)──▶ Gunicorn ──WSGI──▶ Flask app
```
- **Caddy** — принимает HTTPS, проверяет ключ клиента, проксирует на loopback.
- **Gunicorn** — production WSGI-сервер: несколько воркеров, устойчивость к падениям, корректная обработка сигналов.
- **Flask app** — сам код (`mcp_server.server:app`), без изменений.
#### 1. Установка зависимостей
```bash
pip install -e ".[prod]" # gunicorn (Linux/macOS)
# или, если extra ещё не заведён в pyproject.toml:
pip install -e . gunicorn
```
#### 2. Запуск через Gunicorn
```bash
# слушаем ТОЛЬКО локальный интерфейс — из интернета напрямую недоступно
gunicorn mcp_server.server:app \
--bind 127.0.0.1:3001 \
--workers 2 \
--timeout 120 \
--access-logfile - \
--error-logfile -
```
Почему `--bind 127.0.0.1`: сервер не должен торчать в интернет сам по себе. Снаружи его закрывает Caddy, который терминирует TLS и аутентификацию. `--workers 2` — достаточно для одного пользователя/агента; при росте нагрузки увеличивается до `2 × CPU + 1`.
#### 3. Caddy (TLS + аутентификация по ключу)
Caddyfile (минимальный рабочий конфиг):
```caddy
mcp.example.com {
# ключ клиента Claude передаётся в отдельном заголовке,
# чтобы не конфликтовать с Authorization: Bearer <github_token>,
# который уходит дальше в GitHub API
@authorized header X-API-Key "<СЕКРЕТНЫЙ_КЛЮЧ_КЛИЕНТА>"
handle @authorized {
reverse_proxy 127.0.0.1:3001
}
handle {
respond "Unauthorized" 401
}
}
```
`mcp.example.com` Caddy сам получит и будет продлевать TLS-сертификат Let's Encrypt. Запросы без правильного `X-API-Key` отсекаются на входе и до приложения не доходят.
#### 4. systemd-юнит (автозапуск)
`/etc/systemd/system/mcp-server.service`:
```ini
[Unit]
Description=MCP GitHub Server
After=network.target
[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/mcp-server
ExecStart=/opt/mcp-server/.venv/bin/gunicorn mcp_server.server:app \
--bind 127.0.0.1:3001 --workers 2 --timeout 120
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-server
sudo systemctl status mcp-server
```
#### 5. Клиент (например, Claude как кастомный коннектор)
- **URL:** `https://mcp.example.com/mcp`
- **Request header:** `X-API-Key: <СЕКРЕТНЫЙ_КЛЮЧ_КЛИЕНТА>`
- GitHub-токен по-прежнему передаётся в `Authorization: Bearer <github_token>` и доходит до приложения как есть — Caddy его не трогает.
> **Почему не запускать `python -m mcp_server.server` в проде.** Этот путь вызывает `app.run(host="0.0.0.0", port=3001)` (см. `mcp_server/server.py`) — сервер слушает все интерфейсы и становится доступен из глобального интернета без TLS и без аутентификации. Gunicorn за Caddy эту дыру закрывает.
#### 6. Windows: Gunicorn не подходит — используйте Waitress
**Gunicorn не работает на Windows.** Он зависит от POSIX-модуля `fcntl`, которого в Windows нет:
```
ModuleNotFoundError: No module named 'fcntl'
```
Для Windows production-запуска есть **Waitress** — чистый Python WSGI-сервер, многопоточный, без POSIX-зависимостей и с официальной поддержкой Windows.
```bash
pip install waitress
waitress-serve --host=127.0.0.1 --port=3001 --threads=4 mcp_server.server:app
```
Остальная схема не меняется: Waitress так же слушает `127.0.0.1:3001` за reverse proxy (Caddy/Nginx/IIS), а Caddy терминирует TLS и проверяет `X-API-Key`.
| Платформа | WSGI-сервер | Причина |
|-----------|-------------|---------|
| Linux / macOS | **Gunicorn** | стандарт де-факто, multi-process |
| Windows | **Waitress** | чистый Python, multi-thread, без `fcntl` |
> **Для локального тестирования PR на Windows** production-сервер не обязателен: dev-режим (`python -m mcp_server.server`) полностью достаточен, чтобы убедиться, что код работает. Waitress/Gunicorn нужны только для реального боевого запуска.
#### 7. Пошаговый деплой на Ubuntu VPS (проверено вживую)
Ниже — последовательность, реально отработанная на Ubuntu 24.04 VPS (2026-09-27): Gunicorn + Caddy + DuckDNS, снаружи проверено `curl`.
**Разделение ролей.** Команды помечены: `# root` — под root (или через sudo), `$ mcp` — под сервисным пользователем. Сервисный пользователь запускает приложение, но не имеет прав на systemd/apt/Caddy.
**Шаг 1. Сервисный пользователь (под root).**
Пользователь создаётся **с домашней папкой и shell**, но **без sudo**. Это важно: у него должны работать локальные инструменты (`localfs`, `localgit`, `shell`, работа с БД), поэтому нужен `$HOME` и `/bin/bash`. Ограничение только одно — нет прав за пределами `$HOME` и нет sudo.
```bash
# root
adduser --disabled-password --gecos "" mcp
# опционально: убедиться, что НЕ в группе sudo
deluser mcp sudo 2>/dev/null || true
```
**Шаг 2. Установка venv-пакетов (под root).**
Ubuntu 24.04 блокирует `pip install` в системный Python (PEP 668, `externally-managed-environment`). Поэтому ставим через venv:
```bash
# root
apt install -y python3-venv python3-full
```
**Шаг 3. Проект и venv (под сервисным пользователем).**
```bash
# mcp
cd ~
git clone https://github.com/LeonidYasin/mcp-server.git
cd mcp-server
python3 -m venv .venv
.venv/bin/pip install -e . gunicorn
```
Ключевое: `pip` — именно из venv (`.venv/bin/pip`), а не системный. И venv создаётся **под тем же пользователем**, который будет запускать сервис — чтобы файлы принадлежали ему.
**Шаг 4. systemd-юнит (под root).**
`/etc/systemd/system/mcp-server.service`:
```ini
[Unit]
Description=MCP GitHub Server
After=network.target
[Service]
Type=simple
User=mcp
Group=mcp
WorkingDirectory=/home/mcp/mcp-server
ExecStart=/home/mcp/mcp-server/.venv/bin/gunicorn mcp_server.server:app \
--bind 127.0.0.1:3001 --workers 2 --timeout 120
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
```
```bash
# root
systemctl daemon-reload
systemctl enable --now mcp-server
systemctl status mcp-server # ожидаем: active (running)
```
Проверка изнутри (можно под любым пользователем):
```bash
curl -s http://127.0.0.1:3001/health | head -c 200
```
Должен вернуться JSON с `tool_count` и списком инструментов.
**Шаг 5. Домен.**
Нужен публичный домен, указывающий на IP VPS. Бесплатный вариант — [DuckDNS](https://www.duckdns.org): регистрируешь поддомен `имя.duckdns.org`, указываешь IP VPS. Проверка:
```bash
dig +short имя.duckdns.org # должен вернуть публичный IP VPS
```
Домен обязателен: клиенты (в т.ч. Claude как кастомный коннектор) требуют URL вида `https://домен`, а Let's Encrypt выдаёт сертификат на домен. GitHub Pages для этого не подходит — он отдаёт только статику и не умеет проксировать на VPS.
**Шаг 6. Caddy: HTTPS + аутентификация по ключу (под root).**
Сгенерировать ключ клиента:
```bash
# root
openssl rand -hex 32
```
`/etc/caddy/Caddyfile`:
```caddy
имя.duckdns.org {
# ключ клиента в отдельном заголовке, чтобы не конфликтовать
# с Authorization: Bearer <github_token>, который уходит в GitHub API
@authorized header X-API-Key "<СЕКРЕТНЫЙ_КЛЮЧ_КЛИЕНТА>"
handle @authorized {
reverse_proxy 127.0.0.1:3001
}
handle {
respond "Unauthorized" 401
}
}
```
```bash
# root
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
systemctl status caddy # ожидаем: active (running)
```
Caddy сам получит и будет продлевать сертификат Let's Encrypt. Запросы без правильного `X-API-Key` отсекаются на входе (401) и до приложения не доходят.
**Шаг 7. Проверка снаружи (с любой машины, не с VPS).**
```bash
# без ключа — ожидаем 401
curl -i https://имя.duckdns.org/health
# с ключом — ожидаем 200 и JSON
curl -i -H "X-API-Key: <СЕКРЕТНЫЙ_КЛЮЧ_КЛИЕНТА>" https://имя.duckdns.org/health
```
Заголовок `Server: gunicorn` и `Via: 1.1 Caddy` в ответе подтверждают, что цепочка Caddy → Gunicorn работает.
**Шаг 8. Подключение клиента (например, Claude как кастомный коннектор).**
- **URL:** `https://имя.duckdns.org/mcp`
- **Request header:** `X-API-Key: <СЕКРЕТНЫЙ_КЛЮЧ_КЛИЕНТА>`
GitHub-токен по-прежнему передаётся в `Authorization: Bearer <github_token>` и доходит до приложения как есть — Caddy его не трогает.
**Шаг 9. Файрвол (опционально, под root).**
Gunicorn слушает только `127.0.0.1`, поэтому порт 3001 извне недоступен и без файрвола. Как дополнительный слой:
```bash
# root
ufw allow 22/tcp # SSH — СНАЧАЛА, иначе потеряешь доступ
ufw allow 80/tcp # HTTP-01 challenge для Let's Encrypt
ufw allow 443/tcp # HTTPS
ufw enable
ufw status
```
Порт 3001 в UFW открывать не нужно.
### Включение локальных инструментов
```bash
export ENABLE_LOCAL_TOOLS=1 # localfs + localgit
export ENABLE_LOCAL_SHELL=1 # shell (run_command, run_python)
export ENABLE_SYNAPSE=1 # synapse
export LOCAL_TOOLS_ROOT=/workspace # whitelist-корень (по умолчанию ~/workspace)
python -m mcp_server.server
```
## Подключение к DeepSeek++
- **URL:** `http://127.0.0.1:3001/mcp`
- **Тип:** HTTP
- **Заголовок:** `Authorization: Bearer <ваш_github_token>`
> **Какой транспорт выбирать в плагине DeepSeek++.** В поле **Transport** плагина есть отдельные варианты: `Streamable HTTP`, `HTTP`, `SSE`, `Studio Bridge`, `Native`. Для этого сервера выбирайте **`Streamable HTTP`** — он реализует MCP Streamable HTTP с persistent-сессией (`Mcp-Session-Id` переносится между запросами). Вариант `HTTP` в плагине — тоже HTTP, но **без сохранения сессии**; он часто работает, однако `Streamable HTTP` — корректный и рекомендуемый выбор (см. [MCP Transport Mechanisms](https://deepwiki.com/zhu1090093659/deepseek-pp/4.1-mcp-transport-mechanisms) в документации плагина).
---
## 📚 Документация и Skills
| Файл | Что внутри |
|------|------------|
| [`README.md`](README.md) | этот файл: установка, запуск, обзор подпакетов, Source of truth |
| [`TOOLS.md`](TOOLS.md) | **полный каталог всех 110 инструментов**, сгруппированный по подпакетам |
| [`ROADMAP.md`](ROADMAP.md) | дорожная карта: сделанные батчи, Synapse 7b, token economy, безопасность |
| [`SANDBOX.md`](SANDBOX.md) | изоляция локальных инструментов (localfs/localgit/shell), уровни 1–4 |
| [`docs/synapse-protocol.md`](docs/synapse-protocol.md) | JSON-схема обмена (item: offer/want, profile, contact, exchange) |
| [`docs/skills/github-mcp-safe-workflow/SKILL.md`](docs/skills/github-mcp-safe-workflow/SKILL.md) | **Skill для ИИ-агентов**: безопасный workflow работы с GitHub через MCP |
> **Skills** (`docs/skills/`) — это инструкции для ИИ-агентов, как правильно и безопасно пользоваться инструментами этого сервера. Каждый Skill — отдельная папка с `SKILL.md`. Новые Skills добавляйте рядом и упоминайте здесь.
---
## Структура проекта
```
mcp-server/
├── pyproject.toml # source of truth для версии
├── README.md
├── ROADMAP.md
├── SANDBOX.md
├── TOOLS.md # полный каталог инструментов
├── docs/
│ ├── synapse-protocol.md # JSON-схема item/profile/contact/exchange
│ └── skills/ # Skills для ИИ-агентов (см. ниже)
└── mcp_server/
├── server.py # Flask HTTP-сервер (MCP transport, token handling)
├── core/
│ ├── tool.py # Tool dataclass
│ └── registry.py # ToolRegistry + @mcp_tool
└── tools/
├── build/ # сборка и отладка
├── github/ # GitHub API
├── localfs/ # локальные файлы (ENABLE_LOCAL_TOOLS)
├── localgit/ # git в workspace (ENABLE_LOCAL_TOOLS)
├── meta/ # list_my_tools, describe_tool
├── shell/ # shell/python (ENABLE_LOCAL_SHELL)
├── synapse/ # находимость людей (ENABLE_SYNAPSE)
├── utils/ # утилиты и данные
└── web/ # web_fetch, web_search, rss, html
```
---
## Как добавить новый инструмент
### Шаг 1. Создайте файл в нужном подпакете
Пример: `mcp_server/tools/github/create_branch.py`
```python
"""MCP tool: create_branch - создаёт новую ветку."""
from mcp_server.core.registry import mcp_tool
from mcp_server.tools.github.client import GitHubClient
@mcp_tool(
name="create_branch",
description="Создаёт новую ветку в репозитории",
parameters={
"owner": {"type": "string", "description": "Владелец репозитория"},
"repo": {"type": "string", "description": "Имя репозитория"},
"branch": {"type": "string", "description": "Имя новой ветки"},
"from_branch": {"type": "string", "description": "Источник (по умолчанию main)"},
},
required=["owner", "repo", "branch"],
)
def create_branch(client: GitHubClient, owner: str, repo: str, branch: str, from_branch: str = "main") -> dict:
"""Создать новую ветку."""
...
```
### Шаг 2. Подключите файл в `__init__.py` подпакета
Реестр (`mcp_server/core/registry.py`) регистрирует **только то, что явно импортировано** в `mcp_server/tools/<подпакет>/__init__.py`. Без импорта функция не станет инструментом.
### Шаг 3. Обновите доки
Добавьте инструмент в [`TOOLS.md`](TOOLS.md) и, если нужно, в этот README. Инкрементируйте **ревизию документации** в блоке Source of truth.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessUnresponsive