From f836e0a489360ec7b22de6e71a4392b10b6039c5 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Sat, 24 Oct 2026 11:01:52 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D1=8C=20README,?= =?UTF-8?q?=20CHANGELOG=20=D0=B8=20CI=20=D1=81=20=D0=BF=D1=83=D0=B1=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D0=B5=D0=B9=20=D0=B2=20npm=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=82=D0=B5=D0=B3=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/ci.yaml | 75 ++++++++++++++++++++++ CHANGELOG.md | 15 +++++ README.md | 133 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 .gitea/workflows/ci.yaml create mode 100644 CHANGELOG.md create mode 100644 README.md diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..0f0c233 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,75 @@ +name: CI + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + test: + name: Test + runs-on: ubuntu-22.04 + container: catthehacker/ubuntu:act-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - run: npm ci + - run: npm run typecheck + - run: npm run lint + - run: npm test + - run: npm run build + - run: npm pack --dry-run + + # Публикация только по тегу v*. Тег обязан совпадать с версией в + # package.json: иначе в npm уехала бы версия, которой нет в истории тегов. + publish: + name: Publish to npm + needs: test + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-22.04 + container: catthehacker/ubuntu:act-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - run: npm ci + + - name: Check tag matches package.json version + env: + REF_NAME: ${{ github.ref_name }} + run: | + set -eu + pkg_version="$(node -p 'require("./package.json").version')" + tag_version="${REF_NAME#v}" + if [ "$pkg_version" != "$tag_version" ]; then + echo "версия package.json ($pkg_version) не совпадает с тегом ($REF_NAME)" >&2 + exit 1 + fi + + - run: npm run build + + # Токен подставляется через env шага, а не в текст команды: подстановка + # в run попадает в командную строку процесса и разбирается shell. + # Одинарные кавычки — в ~/.npmrc пишется литерал ${NPM_TOKEN}, npm + # раскрывает его из окружения сам, и значение не попадает на диск. + - name: Publish + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + set -eu + trap 'rm -f ~/.npmrc' EXIT + printf '%s\n' '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > ~/.npmrc + npm publish --access public + + - name: Cleanup + if: always() + run: rm -f ~/.npmrc diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4052945 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +Формат — [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/), версии — [SemVer](https://semver.org/lang/ru/). + +## [0.1.0] — 2026-10-24 + +### Добавлено + +- Шесть инструментов MCP: `list_models`, `pick_model`, `estimate_cost`, `get_balance`, `get_usage`, `list_api_keys`. +- stdio-транспорт; запуск через `npx -y @openrusrouter/mcp` (команда `openrusrouter-mcp`). +- Read-only периметр: только GET-запросы к API, без создания/отзыва ключей, платежей, промокодов и тикетов. +- Конфигурация через `OPENRUSROUTER_API_KEY` и `OPENRUSROUTER_BASE_URL` (https-only вне localhost). +- Кэш каталога моделей в памяти процесса на 10 минут. + +[0.1.0]: https://git.realmanual.ru/pub/openrusrouter-mcp/releases/tag/v0.1.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..83889c5 --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# openrusrouter-mcp + +Клиентский MCP-сервер (stdio) для [openrusrouter.ru](https://openrusrouter.ru): каталог моделей с ценами вашего аккаунта, баланс, расход и список API-ключей — прямо из Claude, Cursor, OpenCode или любого другого MCP-агента. + +Пакет: `@openrusrouter/mcp`, команда: `openrusrouter-mcp`. + +## Инструменты + +Все шесть инструментов — только чтение (помечены `readOnlyHint`), сервер делает только GET-запросы к API площадки. + +| Инструмент | Что делает | +|------------|------------| +| `list_models` | Список моделей с фильтрами (модальность, вендор, подстрока, контекст, vision, бесплатные) и сортировкой. Цены — ₽ за 1M токенов с персональной наценкой вашего аккаунта. По умолчанию — только модели, доступные текущему ключу. | +| `pick_model` | Подбор моделей под требования (модальность, vision, контекст, потолок цены, ожидаемый объём токенов). Локальный фильтр по ценам каталога, не рекомендация ИИ; только среди моделей, доступных текущему ключу. | +| `estimate_cost` | Оценка стоимости запроса по числу токенов: `cost_rub` — по рублёвым ценам аккаунта без скидки за кэш, `cost_usd` — по USD-ценам каталога с ценой кэш-чтения, если модель её объявляет. | +| `get_balance` | Текущий баланс аккаунта в рублях и реферальный баланс (decimal-строки). | +| `get_usage` | Расход за последние N дней (1–180) с группировкой по дню, модели, ключу или ключу и модели: запросы, токены, сумма в ₽. | +| `list_api_keys` | Список API-ключей: префикс, лимит расхода и его окно, израсходовано, разрешённые модели. Полный ключ не выводится никогда; `is_current_key` отмечает ключ, которым работает MCP. | + +## Чего тут нет и почему + +> **Создание, отзыв и правка API-ключей, платежи, промокоды и тикеты через MCP не делаются — и не появятся.** +> +> MCP-сервер ходит в API тем же Bearer-ключом, что и обычный API-вызов: сервер openrusrouter.ru их не различает. А решение вызвать инструмент принимает ИИ-агент, а не человек, нажавший кнопку, — и агентом можно управлять через содержимое, которое он читает (prompt injection). Поэтому платёжные и ключевые мутации по такому каналу сознательно не допускаются: на площадке они доступны только из браузерной сессии личного кабинета. + +## Быстрый старт + +Нужен Node.js ≥ 20. + +### 1. Заведите отдельный ключ под MCP + +В [личном кабинете](https://openrusrouter.ru) создайте **отдельный** API-ключ специально для MCP: с небольшим лимитом расхода (`spend_limit_rub`) и, по возможности, ограниченным списком моделей (`allowed_models`). Не используйте мастер-ключ без ограничений — почему, см. [Безопасность](#безопасность). + +### 2. Подключите сервер к клиенту + +**Claude Desktop** — `claude_desktop_config.json` +(macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`, Windows: `%APPDATA%\Claude\claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "openrusrouter": { + "command": "npx", + "args": ["-y", "@openrusrouter/mcp"], + "env": { + "OPENRUSROUTER_API_KEY": "<ваш ключ>" + } + } + } +} +``` + +**Claude Code (CLI):** + +```sh +claude mcp add openrusrouter -e OPENRUSROUTER_API_KEY=<ваш ключ> -- npx -y @openrusrouter/mcp +``` + +**Проектный `.mcp.json` (Claude Code) или `.cursor/mcp.json` (Cursor)** — та же форма `mcpServers`: + +```json +{ + "mcpServers": { + "openrusrouter": { + "command": "npx", + "args": ["-y", "@openrusrouter/mcp"], + "env": { + "OPENRUSROUTER_API_KEY": "<ваш ключ>" + } + } + } +} +``` + +Если файл лежит в репозитории, не коммитьте в него ключ: подставляйте его из окружения (`"${OPENRUSROUTER_API_KEY}"` в Claude Code, `"${env:OPENRUSROUTER_API_KEY}"` в Cursor). + +**OpenCode** — `opencode.json`, блок `mcp`: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "openrusrouter": { + "type": "local", + "command": ["npx", "-y", "@openrusrouter/mcp"], + "enabled": true, + "environment": { + "OPENRUSROUTER_API_KEY": "<ваш ключ>" + } + } + } +} +``` + +OpenCode по умолчанию ждёт список инструментов 5 секунд; первый запуск через `npx` скачивает пакет и может не уложиться — тогда добавьте `"timeout": 30000`. + +## Переменные окружения + +| Переменная | Обязательна | Описание | +|------------|-------------|----------| +| `OPENRUSROUTER_API_KEY` | да | API-ключ из личного кабинета. Без него сервер не стартует. | +| `OPENRUSROUTER_BASE_URL` | нет | Адрес API, по умолчанию `https://openrusrouter.ru`. Допускается только `https` (`http` — только для `localhost`/`127.0.0.1`/`::1`), без логина/пароля, query-параметров и фрагмента. | + +> **Внимание:** ключ уходит в заголовке `Authorization` на `OPENRUSROUTER_BASE_URL`. Переопределив адрес, вы отправляете свой ключ на указанный хост — на свой риск. При нестандартном адресе сервер на старте пишет в stderr, на какой хост будет отправляться ключ. + +## Безопасность + +- **Нет телеметрии.** Сервер обращается только к `OPENRUSROUTER_BASE_URL`. +- **Ключ не пишется никуда:** ни в stdout, ни в stderr, ни в сообщения об ошибках, ни в результаты инструментов (`list_api_keys` отдаёт только префиксы). +- **В stdout идёт только протокол MCP.** Диагностика — в stderr. +- **Кэш каталога — в памяти процесса на 10 минут**, на диск ничего не пишется. +- **Отдельный ограниченный ключ.** MCP делает только GET-запросы и платных вызовов моделей не совершает, но ключ в конфиге агента лежит открытым текстом — в файле, который читают другие программы, попадает в бэкапы и легко оказывается в репозитории. Тем же ключом можно ходить в прокси к моделям за ваш счёт. Отдельный ключ с небольшим `spend_limit_rub` и узким `allowed_models` ограничивает ущерб при утечке, и его можно отозвать в кабинете, не трогая остальные интеграции. + +## Ограничения + +- У image-, transcription- и speech-моделей цена за токен не публикуется — они тарифицируются по факту. `estimate_cost` для них возвращает ошибку, а не ноль. +- Каталог площадки может содержать модели, недоступные текущему ключу (из-за `allowed_models`). По умолчанию `list_models` их скрывает; `include_disallowed=true` покажет их явно. `pick_model` подбирает только среди доступных. +- Оценка стоимости — это оценка: фактическое списание идёт по usage ответа провайдера. + +## Разработка + +```sh +npm ci +npm run typecheck +npm test +npm run build +npm run lint +npm run gen:types # регенерация типов из живой OpenAPI-спеки +``` + +## Лицензия + +MIT — см. [LICENSE](LICENSE).