Skip to main content
Glama
LeonidYasin

MCP GitHub Server

by LeonidYasin
README.md
# MCP GitHub Server

[![version](https://img.shields.io/badge/version-0.4.3-blue)](pyproject.toml)
[![tools](https://img.shields.io/badge/tools-110-brightgreen)](TOOLS.md)
[![python](https://img.shields.io/badge/python-3.10%2B-blue)](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.