Перейти к содержимому
Экосистема AI Vibe
← Назад к AI Vibe News

Редакция 30 июля 2026 г.

Разборы

Свой MCP-сервер для агента: read-only tools при helper'е с write-доступом к GitHub

Свой MCP-сервер для агента: read-only tools при helper'е с write-доступом к GitHub

Соло-разработчик, который подключает к Cursor собственный FastMCP-мост к GitHub, рискует перепутать «у меня нет write-tools» с реальной безопасностью: в разборе на Dev.to автор показывает, что общий HTTP-helper принимал POST и DELETE, хотя все три объявленных tool'а ходили только GET. Для любого, кто собирает MCP-слой под coding agent, это не теория — достаточно одного нового tool'а, копипаста или слишком ретивого агента, чтобы read-only сценарий опёрся на функцию с полным доступом к репозиториям.

FastMCP как мост между агентом и GitHub

В репозитории my-git-manager автор держит небольшой FastMCP-сервер (server.py): через него coding agent читает GitHub-профиль, список репозиториев и статистику по репо, а также — по описанию автора — статьи с Dev.to. Типичный соло-паттерн: один pet-project вместо корпоративного SecOps, свой набор MCP-tools под IDE.

Триггером аудита стал трендовый пост про write-доступ агента к публичным репозиториям. Первая реакция автора — «не моя проблема, write-tools у меня нет». Затем он открыл не объявления tool'ов, а реализацию общего helper'а — и нашёл разрыв между намерением и кодом.

Что может underlying function, а не только tool list

Все три GitHub-tool'а вызывают один helper _gh(path, method="GET", data=None):

Tool Назначение Вызов
get_github_profile профиль GET /users/{GITHUB_USERNAME}
list_repos список репо GET /users/{GITHUB_USERNAME}/repos?...
get_repo_stats метаданные репо GET /repos/{GITHUB_USERNAME}/{repo}

Ни один call site не передаёт data и не меняет method — дефолтный GET. Но сам helper строит запрос к https://api.github.com{path} через urllib.request.Request с параметром method; при наличии data тело сериализуется в JSON. Вызов с method="POST" или method="DELETE" технически возможен — параметр не зафиксирован на уровне функции.

Заголовок поста формулирует контраст жёстко: helper мог POST и DELETE, а каждый tool, который его вызывал, использовал только GET. Story «no write tools» верна по конвенции вызывающих, но не закреплена в коде.

Токен repo и blast radius одного credential

Токен из os.environ['GITHUB_TOKEN'] подставляется в каждый вызов _gh — и для read-only tool'ов, и для гипотетического write. В docs/project_notes/key_facts.md того же репозитория задокументированы scope repo, user. Scope repo — не «прочитать метаданные», а полный read/write ко всем репозиториям аккаунта, включая private: create, push, delete.

Автор отделяет этот кейс от другого своего поста, где в одном процессе жили два credential (GITHUB_TOKEN и DEV_TO_API). Здесь фокус на одном токене с избыточным scope при helper'е шире фактического использования — тот же паттерн, что и с substring-based attribution filter в том же репо (general-purpose helper шире реального сценария).

На момент аудита реальных write-вызовов не было: риск описан как теоретический — будущий tool, плохой edit, compromised или over-eager agent.

Guard на уровне helper: least privilege с трением

Фикс минимален и намеренно оставляет трение для будущих write-tool'ов:

if method != "GET":
    raise ValueError(f"_gh is read-only — got method={method!r}")

Проверка стоит до сети: после патча server._gh(..., method="DELETE") бросает ValueError до urlopen — DELETE «never reached urlopen at all». Stub urllib.request.urlopen подтверждает: get_github_profile() — один GET, форма ответа прежняя; попытка DELETE — исключение без сетевых вызовов.

Если понадобится настоящий write-tool, придётся сознательно расширить guard — а не случайно унаследовать POST/DELETE из общего клиента.

Чеклист для своего MCP под Cursor

Перед тем как доверить агенту MCP-сервер в продакшен-соло-воркфлоу, имеет смысл пройти тот же путь, что автор:

  1. Grep по call site общего HTTP-клиента — смотреть не «что делают tools по названию», а что может underlying function и какие HTTP-методы она принимает.
  2. Сверить credential с фактическим использованием: scope токена может быть write-capable, даже если все текущие tool'ы read-only.
  3. Закрепить ограничение в коде, а не в комментарии к tool list — конвенция вызывающих не защищает от нового tool'а или ошибки агента.

MCP обещает агенту прозрачный слой инструментов; без такого аудита «у меня только GET-tools» остаётся заявлением о сегодняшнем коде, а не гарантией на завтра.

Источники