Добавь README, CHANGELOG и CI с публикацией в npm по тегу
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d7d02267e2
commit
f836e0a489
3 files changed
+223
No files matched your search
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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).
|
||||||
Reference in new issue
Block a user