Skip to main content
Glama
Abdulaziz1224

moysklad-analytics-mcp

MoySklad Analytics

Interactive dashboard over the MoySklad JSON API 1.2 — sales & profit by product, stock & restocking, customers & channels, and trends over time.

Host note

moysklad.uz is a localized front-end for the same MoySklad platform — its login button points at online.moysklad.ru, and there is no api.moysklad.uz (the hostname does not resolve). Tokens issued through the .uz site work against the standard API host:

https://api.moysklad.ru/api/remap/1.2

Two quirks worth knowing, both handled in server/src/moysklad/moysklad.service.ts:

  • The API returns a bare nginx 415 to any client that does not advertise gzip.

  • Account limits are 45 requests / 3 seconds and 5 concurrent; breaching either returns 429 with a X-RateLimit-Retry-After header (milliseconds).

Related MCP server: Company Data MCP Server

Setup

pnpm setup                                    # install server + web deps
cp .env.example .env                          # then put your token in it
echo 'MOYSKLAD_TOKEN=xxxxxxxx' > .env

Get a token in the MoySklad UI under Настройки → Обмен данными → Токены доступа к JSON API, or:

curl -X POST https://api.moysklad.ru/api/remap/1.2/security/token \
  -H "Authorization: Basic $(printf 'login:password' | base64)" \
  --compressed

Check the account first

pnpm probe

This validates the token and prints what the account actually holds: identity, row counts per entity, stores, currency, document volumes, the date range of real sales, and a reachability check across all 14 report endpoints with sample row shapes. Run it before trusting any number on the dashboard.

Permissions matter. MoySklad hides cost price, purchase price and profit from users who lack those rights. If the token belongs to a restricted user the profit and margin columns come back empty — the probe shows this immediately, and the dashboard labels such panels "нет прав" rather than "нет данных".

Run

pnpm dev            # API on :8787, dashboard on :5173

Open http://localhost:5173. The browser never sees the token — it talks only to the Vite proxy, which forwards to the NestJS API.

pnpm build          # production build of both
pnpm typecheck

Layout

server/                     NestJS 11 + TypeScript
  src/moysklad/
    moysklad.service.ts     auth, gzip, pagination, rate limiter, retry
    rate-limiter.ts         40 req/3s, 5 concurrent
    moysklad.types.ts       report row shapes
  src/stats/
    stats.service.ts        report -> dashboard metrics, 5-minute TTL cache
    stats.controller.ts     GET /api/stats/*
    period.dto.ts           query validation
  src/common/
    api-exception.filter.ts upstream errors -> messages the UI can show
  src/probe.ts              account discovery

web/                        Vite 8 + React 19 + HeroUI v3 + Tailwind v4 + Recharts
  src/pages/                Overview, Products, Stock, Customers, Trends
  src/components/           Panel, KpiCard, DataTable, charts, PeriodPicker
  src/lib/                  api client, formatters, types

API

All endpoints take ?from=YYYY-MM-DD&to=YYYY-MM-DD (default: last 30 days).

Endpoint

Returns

GET /api/stats/meta

stores, organizations, default currency

GET /api/stats/overview

headline KPIs with period-over-period deltas

GET /api/stats/products

per-product revenue/profit/margin, loss makers, returns, Pareto

GET /api/stats/stock

stock value, days of cover, restock list, dead & overstocked

GET /api/stats/customers

top customers, repeat rate, concentration, channels, employees

GET /api/stats/trends?interval=hour|day|month

sales, orders and cash series

POST /api/stats/cache/clear

drop the TTL cache

GET /api/assistant/status

whether the assistant has an API key

POST /api/assistant/ask

SSE stream — {question, history} in, text/tool/done/error events out

POST /api/mcp

MCP (Streamable HTTP) for external AI clients — see below

Field semantics — verified against the live account

Four things the documentation does not make obvious, each confirmed against real rows and each a wrong number if handled naively:

  • Money arrives in minor units and is divided by 100. Confirmed: a bar priced at sellPrice: 9642.86 with sellSum: 67500 for 7 units is $96.43 each, not $9,642.

  • margin and salesMargin are different ratios, both fractions. margin = profit / cost (наценка) and salesMargin = profit / revenue (маржа). The dashboard shows salesMargin as "Маржа" and margin as "Наценка", each scaled by 100. Rendering the raw margin as a percentage reports 0.6% where the truth is 60.7%.

  • /report/stock/all returns hrefs with a ?expand=supplier suffix while the profit report returns bare ones. Joining them raw matches nothing and silently classifies the entire catalogue as dead stock.

  • The group profit reports count documents, not unitssalesCount, not sellQuantity, which does not exist on them.

  • The money series uses credit/debit, not income/outcome.

Judgement calls that remain yours:

  • Average cheque divides shipment revenue by shipment count (/entity/demand in the window), not by customer-order count — the two differ, and mixing them made Overview disagree with the Customers page.

  • Days of cover uses velocity over the selected window only, so a short window exaggerates urgency on seasonal items.

  • Dead stock means zero sales in the selected window, not all-time.

AI assistant

The Ассистент tab answers natural-language questions over the same data — "what should we reorder", "how are treadmills selling", "who are our biggest customers". Set DEEPSEEK_API_KEY (get one here) to enable it; without one the tab reports that it is off and nothing else changes.

It runs DeepSeek (deepseek-v4-flash, OpenAI-compatible API) with function calling over seven read-only tools that wrap StatsService, so the assistant and the dashboard panels read exactly the same numbers — there is no second definition of "revenue" to drift. Answers stream to the browser over SSE.

The tools are the assistant's only access to the account: it cannot write to MoySklad, and it cannot reach anything the dashboard cannot.

Four things worth knowing if you change it:

  • Why this model. Benchmarked against the alternatives on the real account. deepseek-v4-flash ($0.14 in / $0.28 out per 1M, $0.0028 on cache hits) scored 4/4 on tool selection at ~2s per round — cheapest and fastest of everything tried. deepseek-v4-pro ($0.435/$0.87) was ~2x slower with no accuracy gain. Local models were tested too and rejected: see the note below.

  • The balance is prepaid. A depleted balance returns 402 Insufficient Balance, surfaced in the UI as a readable message rather than a stack trace.

  • Let the tools default the dates. The prompt tells the model to omit from/to unless the question names a period. Left to itself it computed "last 30 days" as a 31-day window — figures correct for its window, but silently disagreeing with the dashboard preset for the same phrase.

  • The catalogue is in Russian, and the model is not. search_products matches on any single token and, on a miss, returns the account's category names so the model can retry with a real term.

  • The field semantics are in the prompt on purpose. Margin vs markup, the average-cheque basis, what "dead stock" counts — the same traps documented above. Without them the model reports plausible, wrong numbers.

Why not a local model

qwen3:8b and qwen3:4b were benchmarked on the production host via Ollama. Tool selection was fine (4/4), but the box has no GPU: a single model round took 46–198s warm, so one answer (2–3 rounds) would take 3–5 minutes against ~2s for DeepSeek. Worse, unconstrained inference took 7.7 of 8 cores and pushed the production admin panel from 0.5s to 5.5s — the same host runs the API, admin, bot, Postgres and MinIO. Capping it to 4 cores (CPUQuota=400%) protected production but made inference slower still. Revisit only on a machine with a GPU.

  • The catalogue is in Russian, and the model is not. Asked about "treadmills", every model searched the English word and would have found nothing. search_products therefore matches on any single token and, on a miss, returns the account's category names so the model can retry with a real term — and the prompt tells it to search in Russian.

  • The field semantics are in the prompt on purpose. Margin vs markup, the average-cheque basis, what "dead stock" counts — the same traps documented above. Without them the model reports plausible, wrong numbers.

MCP endpoint — the same data for external AI

POST /api/mcp speaks MCP over Streamable HTTP (stateless, JSON responses), so any MCP client — Claude, Cursor, ChatGPT connectors, another agent — can query the account directly.

The tool surface is the assistant's seven, plus seven business tools, plus raw:

  • the seven curated analytics tools (get_account_info, get_overview, search_products, get_products, get_stock, get_customers, get_trends) — the same buildTools(stats) objects the in-dashboard assistant uses, so an external model and the dashboard cannot disagree on what "revenue" means;

  • seven business tools (server/src/mcp/business-tools.ts), shaped by probing what the account actually holds: get_documents (any of 16 document types for a period — orders, shipments, cash, write-offs, transfers, ...), get_document (one document with line items), get_money (balances per org/account, cash vs bank flow, expenses by category — the cash desk is this account's busiest surface), get_purchases (supplies + purchase orders by supplier), get_debts (mutual settlements both directions), search_counterparties (lookup + per-counterparty history), get_turnover (opening/received/issued/closing per product). All money already in major units;

  • moysklad_api — raw read-only GET against any entity/... or report/... path, for everything the curated tools don't cover (counterparties, purchase documents, cash flow, single objects by id). Structurally read-only: the underlying client only issues GET. Results are capped at 100 rows / ~60k characters and meta objects are trimmed to href+type. Raw responses are in minor units (kopecks/tiyin) — the tool description tells the model to divide by 100; the curated tools already return major units.

Enable it by setting MCP_TOKEN (min 24 chars, openssl rand -hex 32). Without MCP_TOKEN the route simply stays behind Basic auth like everything else. With it, clients get in two ways:

1. Static bearer — for Claude Code and scripts:

claude mcp add moysklad --transport http https://your-deployment.example.com/api/mcp \
  --header "Authorization: Bearer <MCP_TOKEN>"

2. OAuth login — for the claude.ai and ChatGPT apps (including mobile), whose connector UIs cannot send custom headers. Add a custom connector with just the URL https://your-deployment.example.com/api/mcp; the app discovers the OAuth endpoints, opens the login page, and the user signs in with the dashboard credentials (DASHBOARD_USER / DASHBOARD_PASSWORD). In claude.ai: Settings → Connectors → Add custom connector (connectors added on the web are then available in the mobile app). In ChatGPT: Settings → Connectors (Developer mode required for custom MCP).

The OAuth server is the full MCP-spec stack — RFC 9728 resource metadata, RFC 8414 AS metadata, RFC 7591 dynamic client registration, authorization code

  • PKCE (S256, enforced), refresh tokens — implemented in server/src/oauth/. Design choices worth knowing:

  • Stateless everything. Codes, tokens and client registrations are HMAC-signed blobs; there is no database, and a redeploy logs nobody out. The signing key is MCP_TOKEN itself, so rotating that one value revokes the static credential and every OAuth grant at once.

  • One human identity. The login form checks the dashboard's Basic-auth credentials — same person, same password, however they arrive.

  • Access tokens live 30 days, refresh 180. The only server-side state is the used-code set (single-use enforcement); losing it on restart is a 10-minute replay window, accepted.

  • Login is throttled globally (20 failures/minute), not per-IP — behind the proxy every client is one IP anyway.

PUBLIC_URL pins the origin used in OAuth metadata; unset, it is derived from Host/X-Forwarded-Proto, which is correct behind Caddy.

Authentication

The server enforces HTTP Basic auth and refuses to start when NODE_ENV=production without DASHBOARD_USER and DASHBOARD_PASSWORD. Only /api/health is exempt, and it reports nothing but liveness. /api/mcp has its own Bearer token (above) and is exempt from Basic auth only when that token is configured.

This is not optional hardening. The dashboard shows revenue, cost prices, per-product margins and every customer with what they spend; on a public host an unguarded URL is not obscure, because the certificate transparency log publishes the hostname minutes after Caddy issues the certificate.

In development, omitting the variables runs without auth and logs a warning.

Deployment

docker build -t moysklad-stats:latest .     # API + built SPA in one image

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    A read-only MCP server for the NuMetric.work accounting/POS/ERP platform, exposing 38 tools to query live business data such as financial statements, invoices, taxes, projects, inventory, and documents. It enables AI assistants to answer from real accounting data without any create, edit, or delete capabilities.
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables read-only access to company data across PostgreSQL, MongoDB Atlas, and flat files through MCP tools, allowing AI assistants to query and retrieve information via natural language.
    -
  • F
    license
    B
    quality
    C
    maintenance
    A secure, read-only MCP server that enables AI assistants to inspect transactions, vendor performance, wallet balances, and analytics through validated REST API calls.
    19
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants to query internal business data for insights into customers, revenue, subscriptions, sales, and churn through controlled, read-only MCP tools.
    -

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Abdulaziz1224/moysklad-analytics-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server