MCP server that exposes the Yandex Cloud Billing Usage API as cost-reporting tools, with automatic currency conversion. Usable from Claude Desktop, Cursor, IDE extensions, or any MCP-compatible client.
The server is intentionally focused on cost reports: it answers "how much did we spend on service X / cloud Y / folder Z in this period?" It does not expose account balances, budgets or the price catalogue — only consumption data.
Tiny helper layer for resolving service IDs that you pass to the spend tools:
| Tool | What it does |
|---|---|
list_services |
The Yandex Cloud service catalogue (Compute, Storage, MK8s, …). |
get_service |
One service by id. |
Most tools below take from_date, to_date (YYYY-MM-DD or ISO 8601), an optional
billing_account_id (falls back to YC_BILLING_ACCOUNT_ID) and aggregation_period
(DAY | WEEK | MONTH | QUARTER | YEAR, default MONTH). The breakdown tools return a
three-level structure: totals (cost, credits, expense), the per-entity breakdown for
the requested dimension, and a time series.
| Tool | What it answers |
|---|---|
spend_summary |
"How much did we spend on this billing account in this period?" |
spend_by_service |
"How much did we spend on Compute / S3 / MK8s …?" |
spend_by_cloud |
"Which cloud (tenant) drove the bill?" |
spend_by_folder |
"Which folder / project drove the bill?" |
spend_by_sku |
"Which line items (vCPU / RAM / egress …) cost the most?" Scope it with a labels filter, resource_ids or service_ids — unscoped it returns every SKU in the account. |
spend_by_resource |
"Which individual VM / bucket / cluster was the outlier?" resource_ids is required — the Usage API rejects a resource report without it. |
list_label_keys |
"Which resource label keys exist, and how concentrated is each?" Returns [{key, distinct_values, total_cost, high_cardinality}] — start here to pick a key for the two tools below. |
spend_grouped_by_label |
"Cost per value of ONE label key" — the chargeback workhorse (label_key="project" → cost per project, "team" → cost per team). Full breakdown, compact rows, sorted by cost. |
spend_by_label |
"Cost for a SPECIFIC label filter" — verbose per-label-value rows. Needs a labels filter ({key: [values]}) or a service/folder/cloud scope. |
These hit the ConsumptionCoreService gRPC API, which has a 1-request-per-minute
per-IP rate limit. Responses are cached in-process for YC_USAGE_CACHE_TTL seconds
(default 300). Requires the role billing.accounts.getReport.
The typical chargeback flow is discover → group → drill down:
list_label_keys— see which label keys your resources actually carry (and which are too granular to group by — those come back flaggedhigh_cardinality).spend_grouped_by_label(label_key="project")— the full cost-per-value breakdown for one key, sorted descending. This is the answer to "what does each project/team/cluster cost".spend_by_label(labels={"project": ["…"]})— the verbose per-value detail once you know which specific value(s) you care about.
Both label-filtering tools refuse an unscoped call: a bare label report would
enumerate every label key × value in the account (thousands of rows), so you must
pass either a labels filter or a service_ids / folder_ids / cloud_ids scope.
High-cardinality backstop. If a spend_grouped_by_label result would exceed the
response budget (YC_GROUPED_MAX_BYTES, default 40 000 — sized to stay under a typical
16K-token tool-output limit), the tool does not truncate rows or sample a top-N.
Instead it returns a status: "too_many_values_to_list" summary carrying the complete,
correct total + value_count and concrete ways to narrow (group by a coarser key,
add a scope, or name specific values). Real business keys (project / team / cluster)
are far smaller and never trip it.
The server keeps a session-level display currency. Every spend response carries a
top-level display block with cost / expense / credit totals converted at the
current CBR daily rate. Default: USD (override via YC_DISPLAY_CURRENCY env).
| Tool | What it does |
|---|---|
get_display_currency |
Read the active display currency. |
set_display_currency |
Switch to a new one — any 3-letter code from the CBR feed (USD, EUR, RUB, KZT, CNY, GBP, JPY, …). |
get_exchange_rates |
Dump the full CBR rate table (RUB per 1 unit, with publication date). |
convert_amount |
One-off conversion between two arbitrary currencies. |
The LLM only needs to call set_display_currency once — the user says "show me everything
in euros", LLM switches, all subsequent tools auto-include EUR values alongside native
YC ones.
The server picks the first auth method whose env var is set, in this priority:
YC_IAM_TOKEN— a ready IAM token (yc iam create-token). Not refreshed.YC_SA_KEY_JSON— service account key JSON inline. Signs a PS256 JWT and exchanges it.YC_SA_KEY_FILE— path to the same JSON on disk.YC_WORKLOAD_TOKEN_FILE— path to a projected OIDC token (Kubernetes Workload Identity Federation). The token is exchanged athttps://auth.yandex.cloud/oauth/token.YC_WORKLOAD_SA_IDis required — the target YC service account id (not the federation id) that should receive the IAM token. That SA must have a federated credential whoseexternal_subject_idmatches the JWT'ssubclaim.YC_OAUTH_TOKEN— Yandex Passport OAuth token, exchanged for an IAM token.YC_USE_METADATA=true— read the IAM token from the Compute instance metadata service (169.254.169.254). Use this when the pod or VM has a YC service account attached.
All refreshable providers cache the token until 60 s before expiry.
uv venv && source .venv/bin/activate
uv pip install -e .
export YC_IAM_TOKEN="$(yc iam create-token)"
yc-billing-mcp
# → Streamable HTTP MCP endpoint on http://127.0.0.1:8000/mcpThe default bind is 127.0.0.1 per the MCP spec (DNS rebinding protection). Set
MCP_HOST=0.0.0.0 only when running behind a reverse proxy / inside a container.
MCP_TRANSPORT selects the wire format:
streamable-http(default) — single endpoint atMCP_PATH, POST for requests and an optional SSE upgrade for streaming. This is the modern MCP HTTP transport.sse— the legacy HTTP+SSE transport, still useful for older clients.stdio— for desktop clients that spawn the server as a subprocess.
{
"mcpServers": {
"yc-billing": {
"url": "http://127.0.0.1:8000/mcp"
}
}
}- Create a federation in YC and bind it to a service account with the
billing.accounts.viewer(or stronger) role on the billing account. - Project a JWT into the pod with the federation's expected audience:
apiVersion: v1
kind: Pod
spec:
serviceAccountName: yc-billing-mcp
containers:
- name: server
image: ghcr.io/your-org/yc-billing-mcp:latest
env:
- { name: YC_WORKLOAD_TOKEN_FILE, value: /var/run/secrets/tokens/yc-token }
- { name: YC_WORKLOAD_SA_ID, value: "ajeXXXXXXXXXXXXXX" } # target YC SA
- { name: MCP_HOST, value: "0.0.0.0" }
volumeMounts:
- name: yc-token
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: yc-token
projected:
sources:
- serviceAccountToken:
path: yc-token
audience: https://yc.example/federations/<fed-id>
expirationSeconds: 3600- Front the pod with a Service / Ingress that adds your own auth (mTLS, OIDC at the proxy, etc.) — the server itself does not authenticate MCP clients.
docker build -t yc-billing-mcp .
docker run --rm -p 8000:8000 \
-e YC_IAM_TOKEN="$(yc iam create-token)" \
yc-billing-mcpbilling.accounts.getReporton the billing account you query — required for allspend_*tools (ConsumptionCore API).- No role is needed to read the public service catalogue.
See .env.example.
{ "mcpServers": { "yc-billing": { "command": "yc-billing-mcp", "env": { "MCP_TRANSPORT": "stdio", "YC_SA_KEY_FILE": "/Users/me/.yc/sa-key.json" } } } }