market-mcp
# market-mcp
Локальный MCP-сервер, через который Claude получает данные Московской биржи (MOEX ISS) и брокерского счёта в Т-Инвестициях (T-Invest API). Сервер **только читает данные**: методов выставления заявок в коде нет.
## Установка
Нужны Python 3.12+ и [uv](https://docs.astral.sh/uv/) (`pip install uv` или `winget install astral-sh.uv`).
```powershell
git clone <repo> C:\projects\market-mcp
cd C:\projects\market-mcp
uv sync
copy .env.example .env # при необходимости впишите TINVEST_TOKEN
uv run pytest # юнит-тесты на фикстурах, без сети
```
Без токена работают котировки, свечи, скринер, индексы и купоны через MOEX ISS с задержкой 15 минут. С токеном котировки идут в реальном времени, а ещё появляются стакан, дивиденды и (при `ENABLE_PORTFOLIO=true`) данные счёта.
**Токен T-Invest:** в приложении Т-Инвестиций откройте Настройки → Токены T-Invest API и выпустите токен **«Только чтение»**. Для разработки можно включить `TINVEST_SANDBOX=true`.
## Подключение к Claude Desktop
Добавьте блок в `%APPDATA%\Claude\claude_desktop_config.json` и перезапустите Claude:
```json
{
"mcpServers": {
"market": {
"command": "uv",
"args": ["--directory", "C:\\projects\\market-mcp", "run", "market-mcp"],
"env": { "TINVEST_TOKEN": "t.…", "ENABLE_PORTFOLIO": "true" }
}
}
}
```
Если `uv` не находится, укажите полный путь к `uv.exe` (`where uv`). Токен можно не писать в конфиг: сервер читает `.env` из папки проекта.
Для Claude Code: `claude mcp add market -- uv --directory C:\projects\market-mcp run market-mcp`.
## Инструменты
| Инструмент | Параметры | Источник | Что возвращает |
| --- | --- | --- | --- |
| `find_instrument` | `query`, `kind?`, `limit=10` | оба | Кандидаты: ticker, board, figi, secid, название, тип |
| `get_quote` | `ticker`, `board?` | T-Invest → ISS | Цена, изменение за день, объём, статус торгов |
| `get_candles` | `ticker`, `interval`, `date_from?`, `date_to?`, `board?`, `cursor?` | ISS / T-Invest | OHLCV, не больше 1000 за вызов |
| `get_orderbook` | `ticker`, `depth=10`, `board?` | T-Invest | Стакан bids/asks |
| `screen_market` | `board`, `filters?`, `sort`, `order`, `limit=50`, `cursor?` | ISS | Бумаги режима торгов, не больше 500 строк |
| `get_index` | `index_id=IMOEX`, `date_from?`, `date_to?` | ISS | Текущее значение и история |
| `get_payouts` | `ticker`, `date_from?`, `date_to?` | T-Invest (купоны — и ISS) | Дивиденды или купоны с датами отсечки |
| `get_portfolio` | `account_id?` | T-Invest | Стоимость, доли классов, позиции, P&L |
| `get_operations` | `date_from?`, `date_to?`, `account_id?`, `types?`, `cursor?` | T-Invest | Сделки, комиссии, выплаты |
`get_portfolio` и `get_operations` не регистрируются, пока `ENABLE_PORTFOLIO` не равен `true`.
В ТЗ параметры периода названы `from`/`to`. В сервере это `date_from`/`date_to`: `from` — зарезервированное слово Python, а MCP SDK не умеет переименовывать аргументы.
### Контракт ответа
```json
{
"data": { "ticker": "SBER", "board": "TQBR", "price": "279.26", "change_day_pct": "-0.24",
"volume": 10617169, "currency": "RUB", "trading_status": "normal_trading" },
"source": "moex",
"as_of": "2026-09-17T10:06:23Z",
"stale": true,
"delay_minutes": 15
}
```
- Цены и суммы передаются строками, чтобы не терять точность `Decimal`. Все метки времени в UTC. Даты `YYYY-MM-DD` на входе считаются торговыми днями по Москве.
- `stale: true` означает, что данные не в реальном времени (ISS). Вне торгов `price` — цена закрытия, а в ответе есть `notice.code = "market_closed"`.
- Цена облигации в `price` — рубли за бумагу с НКД. Цена в процентах от номинала лежит в `price_pct_of_face`.
- Если ответ разбит на страницы, в нём есть `next_cursor`: передайте его в `cursor` следующего вызова.
- Ошибка приходит как `{"error": {"code", "message", "retriable"}}`. Коды: `instrument_not_found`, `ambiguous_instrument` (+`candidates`), `auth_failed`, `token_required`, `rate_limited` (+`retry_after_seconds`), `upstream_unavailable`, `portfolio_disabled`, `invalid_argument`, `account_not_found`.
## Настройки
| Переменная | Назначение | По умолчанию |
| --- | --- | --- |
| `TINVEST_TOKEN` | Токен «только чтение» | — |
| `TINVEST_SANDBOX` | Работа через песочницу | `false` |
| `ENABLE_PORTFOLIO` | Регистрировать инструменты счёта | `false` |
| `MOEX_BASE_URL` | Базовый URL ISS | `https://iss.moex.com/iss` |
| `CACHE_PATH` | Файл SQLite-кэша | `~/.market-mcp/cache.db` |
| `LOG_LEVEL` | Уровень логов | `INFO` |
| `HTTP_TIMEOUT` | Таймаут запроса, с | `10` |
## Как устроено
```
src/market_mcp/
├─ server.py регистрация инструментов и запуск по stdio
├─ tools/ market.py, portfolio.py — схемы и описания инструментов
├─ app.py сборка зависимостей; ошибки превращаются в {"error": …}
├─ domain.py выбор источника, нормализация, кэш, постраничность
├─ providers/ moex.py (ISS), tinvest.py (REST-фасад, только методы чтения)
├─ mapping.py справочник ticker ↔ figi ↔ instrument_uid ↔ SECID в SQLite, обновление раз в сутки
├─ cache.py SQLite + TTL, ключ sha256(tool + аргументы)
├─ ratelimit.py token bucket (ISS 5 rps) и учёт квот T-Invest по x-ratelimit-*
├─ http.py общий httpx-клиент, 3 повтора (0.5 → 2 → 8 с) на сетевые ошибки, 429 и 5xx
├─ money.py Quotation / MoneyValue ↔ Decimal
├─ models.py Instrument, Quote, Candle, Position, коды ошибок, конверт ответа
└─ logging_setup.py JSON-логи только в stderr, токен вырезается
```
- **TTL кэша:** справочник — 24 ч, свечи закрытых дней — бессрочно, свечи текущего дня — 60 с, котировки — 15 с, скринер — 5 мин. Портфель, операции и стакан не кэшируются, данные счёта в SQLite не пишутся.
- **Безопасность:** токен передаётся только в заголовке `Authorization`, в логи, ошибки и ответы не попадает. Номера счетов маскируются до `****1234`, и тот же формат принимается в `account_id`.
- **TLS:** у T-Invest сертификат выдан российским удостоверяющим центром, поэтому httpx проверяет его по системному хранилищу сертификатов через `truststore`. Если T-Invest отвечает ошибкой TLS, установите в Windows корневой сертификат Минцифры.
## Разработка
```powershell
uv run ruff check src tests
uv run mypy # strict
uv run pytest # фикстуры + respx, без сети
uv run pytest -m live # реальные API, запускать вручную
uv run python scripts/smoke.py # живой прогон всех рыночных инструментов через stdio
npx @modelcontextprotocol/inspector uv run market-mcp
```
В VS Code конфигурации из `.vscode/launch.json` запускают MCP Inspector, smoke-тест и pytest.
Главное правило: **никаких `print()`**. stdout занят протоколом MCP, а все логи пишутся через `logging` в stderr.
TDQS
Scored across 7 tools
Each tool targets a distinct purpose: instrument lookup, current quote, historical candles, order book, screening, index data, and payouts. There is no meaningful overlap; even get_quote and get_candles are clearly separated by real-time vs historical scope.
All tool names follow a snake_case verb_noun pattern: find_instrument, get_quote, get_candles, get_orderbook, screen_market, get_index, get_payouts. The verbs (find/get/screen) are distinct and consistent with each tool's action, and the overall pattern is uniform.
Seven tools is well within the ideal range for a market-data server. Each tool covers a necessary data type without redundancy, making the set feel complete yet focused.
The surface covers the core market-data lifecycle: searching instruments, current quotes, historical candles, order book depth, market screening, index comparison, and dividend/coupon payouts. No obvious missing operations for the stated domain.