Cirrus ArgusCirrus Argus Portal
AI ServicesMCP DirectoryDocsSign In

Documentation

Developer HubOnboarding GuideQuick Start
MCP Integration
OverviewWhen to useGetting started (5 min)AuthenticationFinding IDsTagging requestsEndpoint & envelopeTool referenceRecipesIsolationOnboardingObservability
Changelog
Integration SurfaceJSON-RPC 2.0PAT-authenticated

Integrations MCP

The Integrations MCP is a read-only, per-org, PAT-authenticated JSON-RPC surface for sibling products in the Cirrus AI suite (RAIA, FinOps, custom in-house tools) to pull traces, metrics, and usage data from Argus without going through the admin-session UI. It runs at POST /v1/integrations/mcp.

When to use this surface

Argus exposes four orthogonal surfaces. Pick this one when the caller is another product, not a user, an operator, or an LLM client.

SurfaceCallerAuthShape
/v1/chat/completionsLLM client (Claude Code, Cursor, custom SDK)Virtual keyOpenAI-compatible
/v1/admin/*Human operator via UI, or automationSession cookie or admin-scope PATREST
/v1/argus/mcpOperator tooling / agent acting as an operatorPAT only (never a session cookie)JSON-RPC 2.0 / MCP
/v1/integrations/mcpSibling product (RAIA, FinOps)Service-account PAT, integration scopeJSON-RPC 2.0 / MCP

Getting started (5 min)

A cold-start walkthrough — go from "no token" to a first useful call. Every following section assumes you have NEXUS_PAT and NEXUS_URL set.

  1. Have a Argus admin mint you a service-account PAT. In the Control Plane: Settings → Service Accounts → Create service account. Scope: Integration. Product: raia / finops / your own slug.
    The plaintext token is shown once and never again — copy it before closing the dialog.
  2. Set your two env vars.
    # Once you have the token, put it in your caller's secret store.
    # For quick local tests:
    export NEXUS_PAT="nxat_...paste-token-here..."
    export NEXUS_URL="https://nexus.cirruslabs.technology"
  3. Ping. This confirms auth works and echoes back the org and product slug the token resolved to.
    Request
    bash
    curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
      -H "Authorization: Bearer $NEXUS_PAT" \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": { "name": "integration.ping", "arguments": {} }
      }' | jq

    Expected response:

    Response
    json
    {
      "jsonrpc": "2.0",
      "id": 1,
      "result": {
        "content": [
          {
            "type": "text",
            "text": "{\"pong\":true,\"org_id\":\"org_abc\",\"product_slug\":\"raia\",\"scope\":\"integration\",\"server_time\":\"2026-07-07T04:12:00.000Z\"}"
          }
        ],
        "isError": false
      }
    }
    If you see "error":"unauthorized" — the token is wrong or was revoked. If you see "error":"suite_product_disabled" — the org needs the product enabled (Settings → Suite Products).
  4. Learn to unwrap responses. Every tool returns the same JSON-RPC envelope with a stringified payload inside result.content[0].text. Get comfortable with the double-parse:
    # The content is a JSON string inside a JSON envelope.
    # Extract the inner payload once and reuse the shape for every tool call.
    
    RESULT=$(curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
      -H "Authorization: Bearer $NEXUS_PAT" \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc": "2.0", "id": 1, "method": "tools/call",
        "params": { "name": "integration.metrics.overview", "arguments": {} }
      }')
    
    # Pull the inner text and re-parse
    echo "$RESULT" | jq -r '.result.content[0].text' | jq
  5. Call something useful. Try integration.metrics.overview for a one-shot KPI snapshot, or integration.traces.list to pull the last hour of activity. Full reference below.
Tip: if you're prototyping in a notebook, use the same double-parse in Python: json.loads(resp.json()["result"]["content"][0]["text"]).

Authentication

Callers hold a service-account Personal Access Token with two required bindings. Read-only here is a property of the integration scope, not of service accounts as such — a service account minted with admin or write scope can perform admin writes, it just cannot call this surface.

scopeintegration

Dedicated scope mapping to a fixed permission set: integration:traces:read, integration:metrics:read, integration:usage:read. Those three are the only permissions the integration scope carries, so a token scoped this way can read but never mutate.

productSlugraia | finops | <custom>

The sibling product the token represents. Emitted on every audit row and gated by the org's Suite Products entitlement (see below). Metadata-only today; a future release will make this binding load-bearing.

Not a privilege tier
admin scope and the integration:* permissions are disjoint by design, and this trips people up. An admin-scoped PAT is not a superset of an integration one: it deliberately cannot reach this surface and gets permission_denied on every tool here. Instance-admin status does not help either — the integration:* permissions are excluded from the admin short-circuit on purpose. Nor is the reverse true: an integration token cannot touch /v1/admin/* or /v1/argus/mcp. integration is a separate identity domain for sibling-product attribution, not a lower rung on a ladder. A human PAT cannot even be minted with it — only an org-owned service account can hold integration scope. If you are getting permission_denied here with a working admin token, this is why: you need a second, separately minted service-account token.
Suite-product gate
Every call is gated by a per-org Suite Products lookup, Redis-cached with a 30-second TTL. If the org has never had a Suite Products row, the gate is fail-open (backwards-compat with orgs provisioned before the entitlement system existed). Once the org has any Suite Products row, calls for a disabled product return suite_product_disabled. If Redis is unreachable, calls still proceed — the gate never brings the surface down.

Finding the IDs you'll pass in

Every non-trivial tool takes at least one resource id. Here's how to look them up.

Virtual key IDs

From the Control Plane: Keys page — the id is the first column, displayed as vk_.... Click the row to open the detail page; the URL carries the id.

Programmatically:

Look up keys
bash
# List keys for your org (admin session or admin PAT — this endpoint
# is NOT part of the integration surface; it's how a human operator
# looks up IDs to hand off to the integration caller).
curl -sS "$NEXUS_URL/v1/admin/keys" \
  -H "Authorization: Bearer $ADMIN_PAT" | jq '.keys[] | { id, name, keyType }'

# Or from a running trace: the trace summary includes virtual_key_id.
# Grab a recent trace's key id in one call:
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"integration.traces.list","arguments":{"limit":1}}}' \
  | jq -r '.result.content[0].text' | jq '.traces[0].virtual_key_id'
$ADMIN_PAT is an admin-scoped PAT — different from $NEXUS_PAT. The admin endpoints are outside the integration surface; they're what your operators use to hand off IDs to you.

Team IDs

From the Control Plane: Teams page — click any team; the URL becomes /teams/<team_id>. Copy from the address bar.

Programmatically:

Look up teams
bash
# List teams for your org
curl -sS "$NEXUS_URL/v1/admin/teams" \
  -H "Authorization: Bearer $ADMIN_PAT" | jq '.teams[] | { id, name, slug }'

# You can also read team IDs off cost_attribution — every by_team row
# includes team_id + team_name so you can build a lookup on the fly.
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"integration.metrics.cost_attribution","arguments":{}}}' \
  | jq -r '.result.content[0].text' | jq '.by_team[] | { team_id, team_name }'
For FinOps callers: cost_attribution returns team_id + team_name together, so you can render your report without a separate lookup.

Trace IDs

Trace IDs come from two places:

  • integration.traces.list — every summary carries trace_id.
  • Control Plane Traces page — click a row; the URL becomes /traces/<trace_id>.
# Trace IDs come from two places:
#
# 1. integration.traces.list — every summary carries trace_id.
# 2. The Control Plane Traces page — click any row, the URL is
#    /traces/<trace_id>. Copy from the address bar.
#
# Once you have a trace_id, fetch spans or the RAIA transcript:
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "tools/call",
    "params": {
      "name": "integration.traces.get",
      "arguments": { "trace_id": "01J1Z...", "format": "raia_transcript" }
    }
  }' | jq -r '.result.content[0].text' | jq

Time windows

Every tool that queries analytics accepts start and end as ISO 8601 timestamps. Omit both to get the trailing 24h — Argus fills in the defaults on the server. Buckets snap to calendar boundaries (UTC), so two callers asking for the same window get the same bucket edges.

Tagging requests so you can find them later

You don't have to hunt through the Traces UI for the row you care about — the gateway accepts three different headers that let the caller pre-stamp requests with an identifier or label. Pick whichever fits your architecture; you can combine traceparent/x-argus-trace-id with x-argus-trace-tag if you want both a stable id and a structured tag.

traceparent — W3C standard trace id

Most OpenTelemetry-instrumented SDKs emit this header by default. Argus adopts the 32-hex trace-id portion as the row's trace_id, so the same id appears on both the caller's side and in Argus. Recommended when the caller already runs an OTel pipeline.

# W3C traceparent — most OTel-instrumented SDKs emit this by default.
# Argus adopts it as the row's trace_id, so the same id appears on both
# sides of the call.
curl -sS -X POST "$NEXUS_URL/v1/chat/completions" \
  -H "Authorization: Bearer $NEXUS_VK" \
  -H "traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"hi"}]}'

# Later, pull that exact conversation:
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"integration.traces.get",
                 "arguments":{
                   "trace_id":"0af7651916cd43dd8448eb211c80319c",
                   "format":"raia_transcript"
                 }}}'

x-argus-trace-id — explicit override

For clients that can't emit W3C traceparent. Whatever string you send becomes the trace's canonical id. Handy for evaluation frameworks that want to pin a deterministic id to each item in a batch.

# For clients that can't emit W3C traceparent, Argus honors an override.
# Same effect — your string becomes the trace's canonical id.
curl -sS -X POST "$NEXUS_URL/v1/chat/completions" \
  -H "Authorization: Bearer $NEXUS_VK" \
  -H "x-argus-trace-id: eval-run-2026-07-07-batch-42-item-018" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-sonnet-4-6","messages":[...]}'

x-argus-trace-tag — structured label

A caller-provided label up to 128 characters, stored alongside the trace and echoed back on every traces.list row. Use this when you don't care about picking the trace_id yourself but do want to filter your own traffic later — feature flag, cohort, evaluation batch, canary experiment, anything.

Recommended shape: a pipe-delimited key=value string like feature=summarize|user=u123|exp=v2. That gives you a substring you can search for later.
# x-argus-trace-tag is a 128-char label. Use it when you don't want to
# hand-pick the trace_id but DO want to filter your own traffic later
# (feature flag, cohort, evaluation batch, ...).
curl -sS -X POST "$NEXUS_URL/v1/chat/completions" \
  -H "Authorization: Bearer $NEXUS_VK" \
  -H "x-argus-trace-tag: feature=summarize|user=u123|exp=v2" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-sonnet-4-6","messages":[...]}'

# Every traces.list row echoes the tag back:
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"integration.traces.list","arguments":{"limit":100}}}' \
  | jq -r '.result.content[0].text' \
  | jq '.traces[] | select(.trace_tag | contains("feature=summarize"))'
Current limitation: integration.traces.list returns trace_tag on every row but does not yet accept it as a filter server- side. Pull recent traces and filter client-side (as in the jq example above) until the filter lands. Filed as a followup ticket.
Header propagation: these headers flow through the OpenAI-compatible surface (/v1/chat/completions, /v1/embeddings, etc.) — set them once at the caller's HTTP client and every subsequent tool call inherits them. The gateway also honors traceparent on /v1/integrations/mcp itself, so RAIA/FinOps can pin their own trace id to the audit-log row too.

Endpoint & envelope

Standard JSON-RPC 2.0 with an MCP-shaped tools/call method:

Request
http
POST /v1/integrations/mcp HTTP/1.1
Host: nexus.cirruslabs.technology
Authorization: Bearer <PAT>
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": "req-1",
  "method": "tools/call",
  "params": {
    "name": "integration.ping",
    "arguments": {}
  }
}

Successful calls return { content: [{type:"text", text:"..."}], isError: false }. The tool serialises its response as a single JSON string inside that text block — hence the double-parse in the walkthrough. Errors use the same envelope with isError: true; the inner JSON carries an error plus a code:

Error
json
{
  "jsonrpc": "2.0",
  "id": "req-1",
  "result": {
    "content": [
      { "type": "text", "text": "{\"error\":\"permission_denied\",\"required\":\"integration:usage:read\"}" }
    ],
    "isError": true
  }
}

Error codes

codemeaningwhat to do
unauthorizedMissing/invalid PAT or the token was revoked.Re-mint the token; check for typos in the header.
permission_deniedToken scope does not grant the tool's required permission.The response includes the missing permission. Check the token scope; integration PATs should carry the integration scope.
suite_product_disabledToken's productSlug is not enabled for the org.Ask a Argus admin to enable the product under Settings → Suite Products.
not_foundResource id (trace, key, team) either doesn't exist or belongs to a different org. Always returned for cross-tenant lookups — never leaks existence.Double-check you're using an id from your own org.
invalid_requestSchema violation (bad window, missing required arg).The error message names the offending field.
unknown_toolTool name is not registered in this release.Refresh your tool inventory with tools/list.

Tool reference

All tools are read-only and org-scoped. Org id is derived from the calling PAT — any orgId passed in arguments is silently ignored.

integration.ping

integration:traces:read

Smoke test. Confirms auth and dispatch are healthy; echoes back the org and product slug the token resolved to. Use this from a health-check to catch entitlement regressions early.

Arguments: none.

integration.traces.list

integration:traces:read

List recent trace summaries grouped by trace_id. Cursor-paginated; supports a time window and rich filters.

Arguments: start, end, limit (1–200, default 50), cursor, filters.virtual_key_id, filters.model, filters.provider, filters.has_error, filters.include_internal.

Example 1 — errored traces in the last 24h
bash
# All errored traces in the last 24h, newest first
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "tools/call",
    "params": {
      "name": "integration.traces.list",
      "arguments": {
        "limit": 100,
        "filters": { "has_error": true, "include_internal": false }
      }
    }
  }' | jq -r '.result.content[0].text' | jq '.traces[] | { trace_id, model, cost_usd }'
Example 2 — recent traces for one key
bash
# Recent traces for a single virtual key — useful when a customer
# reports "my key is behaving oddly, can you look?"
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "tools/call",
    "params": {
      "name": "integration.traces.list",
      "arguments": {
        "start": "2026-07-06T00:00:00Z",
        "limit": 50,
        "filters": { "virtual_key_id": "vk_abc" }
      }
    }
  }'

The response carries traces[] (summaries with trace_id, model, provider, cost_usd, duration_ms, has_error, ...), next_cursor for pagination, and total (approximate count in the window).

integration.traces.get

integration:traces:read

Return a single trace by id. format="spans" (default) yields the raw span array — good for debugging. format="raia_transcript" yields the RAIA-compatible envelope (messages, model, provider, cost, tokens) that RAIA's eval pipeline expects.

Arguments: trace_id (required), format ∈ spans | raia_transcript. Foreign-org trace ids return not_found.

Response body — raia_transcript format
json
{
  "trace_id": "01J1Z...",
  "conversation_id": "conv_abc",
  "started_at": "2026-07-06T18:14:22.104Z",
  "messages": [
    { "role": "user",      "content": "Summarise this incident report.", "timestamp": "..." },
    { "role": "assistant", "content": "The report describes...",         "timestamp": "..." },
    { "role": "tool",      "name": "lookup_ticket", "arguments": {}, "result": {} }
  ],
  "model": "claude-sonnet-4-6",
  "provider": "anthropic",
  "cost_usd": 0.0423,
  "total_tokens": 3187
}

integration.metrics.overview

integration:metrics:read

Org-level KPI snapshot for a window: total requests, total cost, p95 latency, cache hit rate, error rate. Same query the Argus dashboard uses. Ideal for a nightly "how'd we do yesterday?" digest.

Arguments: start, end (both default to trailing 24h).

Response body
json
{
  "start": "2026-07-01T00:00:00.000Z",
  "end":   "2026-07-07T00:00:00.000Z",
  "total_requests": 128432,
  "total_cost_usd": 1284.19,
  "p95_latency_ms": 1820,
  "cache_hit_rate": 0.34,
  "error_rate": 0.007
}

integration.metrics.timeseries

integration:metrics:read

Time-series values for a single metric, optionally grouped by a dimension. Buckets snap to calendar boundaries in UTC so multiple callers see identical alignment.

Arguments: metric (required), group_by ∈ none (default) | model | provider | team | virtual_key, bucket ∈ hour | day (default) | week, start, end, filters.* (same set as traces.list).

Example — daily cost by model
bash
# Daily spend broken down by model — the primary series RAIA
# plots on its "cost trend" widget.
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "tools/call",
    "params": {
      "name": "integration.metrics.timeseries",
      "arguments": {
        "metric": "cost",
        "group_by": "model",
        "bucket": "day",
        "start": "2026-07-01T00:00:00Z",
        "end":   "2026-07-07T00:00:00Z"
      }
    }
  }'
Response body
json
{
  "metric": "cost",
  "bucket": "day",
  "buckets": ["2026-07-01", "2026-07-02", "2026-07-03", "..."],
  "series": [
    { "key": "claude-sonnet-4-6", "values": [12.4, 18.1, 22.9, "..."] },
    { "key": "gpt-4.1",           "values": [ 8.2,  9.7, 10.4, "..."] }
  ]
}

integration.metrics.cost_attribution

integration:metrics:read

The primary chargeback query. Cost + request count grouped three ways in a single round-trip: by team, by (model + provider), and by caller class (client vs internal). One call renders a full chargeback report.

Arguments: start, end, top_models_limit (1–100, default 20).

Example — monthly chargeback
bash
# The one-call chargeback query — spend split three ways
# (team, model+provider, caller class) for a billing month.
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "tools/call",
    "params": {
      "name": "integration.metrics.cost_attribution",
      "arguments": {
        "start":            "2026-06-01T00:00:00Z",
        "end":              "2026-07-01T00:00:00Z",
        "top_models_limit": 20
      }
    }
  }'
Response body
json
{
  "by_team": [
    { "team_id": "team_platform", "team_name": "Platform", "cost_usd": 412.10, "requests": 41230 },
    { "team_id": "team_ml",       "team_name": "ML",       "cost_usd": 318.45, "requests": 22004 }
  ],
  "by_model": [
    { "model": "claude-sonnet-4-6", "provider": "anthropic", "cost_usd": 612.87, "requests": 45120 },
    { "model": "gpt-4.1",           "provider": "openai",    "cost_usd": 401.22, "requests": 33044 }
  ],
  "by_caller_class": [
    { "caller_class": "client",   "cost_usd": 1102.44, "requests": 118032 },
    { "caller_class": "internal", "cost_usd":  181.75, "requests":  10400 }
  ]
}

integration.metrics.latency_percentiles

integration:metrics:read

End-to-end latency bands (p50 / p75 / p90 / p95 / p99 / p99.9), time-to-first-token (TTFT) bands, and average tokens/sec per (provider, model) tuple. Backs RAIA's "model performance" view; useful for tuning model choice or catching a provider regression.

Arguments: start, end.

Response body
json
{
  "series": [
    {
      "provider": "anthropic",
      "model":    "claude-sonnet-4-6",
      "requests": 41230,
      "latency_ms": { "p50": 720, "p75": 940, "p90": 1220, "p95": 1810, "p99": 3200, "p999": 6100 },
      "ttft_ms":    { "p50": 120, "p75": 165, "p90": 210,  "p95": 290,  "p99": 480,  "p999": 812 },
      "tokens_per_second": 92.4
    }
  ]
}

integration.usage.by_key

integration:usage:read

Usage totals + bucketed series for one virtual key. Optional grouping by model or provider. Returns not_found if the key doesn't belong to your org (checked before the ClickHouse query fires — no timing side-channel).

Arguments: virtual_key_id (required), start, end, bucket ∈ hour | day (default) | week, group_by ∈ model | provider (optional).

Example — weekly by-day-by-model
bash
# Weekly usage for one key, bucketed daily, split by model
curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "tools/call",
    "params": {
      "name": "integration.usage.by_key",
      "arguments": {
        "virtual_key_id": "vk_abc",
        "start":          "2026-07-01T00:00:00Z",
        "end":            "2026-07-08T00:00:00Z",
        "bucket":         "day",
        "group_by":       "model"
      }
    }
  }'
Response body
json
{
  "virtual_key_id": "vk_abc",
  "start": "2026-07-01T00:00:00.000Z",
  "end":   "2026-07-08T00:00:00.000Z",
  "totals": {
    "request_count":     41230,
    "prompt_tokens":  18234000,
    "completion_tokens": 8100200,
    "cached_tokens":  4210000,
    "cost_usd":         412.10
  },
  "buckets": [
    { "bucket_start": "2026-07-01", "request_count": 5820, "cost_usd": 58.4 },
    { "bucket_start": "2026-07-02", "request_count": 6100, "cost_usd": 62.1 }
  ],
  "groups": [
    { "key": "claude-sonnet-4-6", "totals": { "request_count": 30120, "cost_usd": 312.4 } },
    { "key": "gpt-4.1",           "totals": { "request_count": 11110, "cost_usd":  99.7 } }
  ]
}

integration.usage.by_team

integration:usage:read

Team-scoped usage totals. Returns not_found if the team isn't in your org.

Arguments: team_id (required), start, end.

Response body
json
{
  "team_id": "team_platform",
  "total_requests": 128432,
  "total_tokens":  30200000,
  "total_cost_usd": 412.10,
  "avg_latency_ms":    820,
  "error_rate":         0.008,
  "unique_users":      42
}

integration.usage.by_org

integration:usage:read

Org-wide totals plus per-team breakdown in one call. Use this for chargeback/budget-enforcement dashboards that need everything together.

Arguments: start, end.

Response body
json
{
  "total_requests": 892120,
  "total_tokens":  201200000,
  "total_cost_usd": 3120.44,
  "avg_latency_ms":    880,
  "error_rate":         0.006,
  "unique_users":     220,
  "unique_teams":       9,
  "team_breakdown": [
    { "team_id": "team_platform", "team_name": "Platform", "cost_usd": 412.10, "requests": 41230 }
  ]
}

Recipes — end-to-end examples

Copy-pastable shell scripts for the three most common real-world flows. Each stitches two or three tools together to produce something an on-call or FinOps analyst would actually use.

1 · Monthly chargeback report → Slack

Post spend-by-team to Slack every 1st of the month. A single cost_attribution call produces the whole table.

chargeback.sh
bash
#!/usr/bin/env bash
# Recipe 1: nightly chargeback report
# Runs monthly; posts a spend-by-team table to Slack.
set -euo pipefail

MONTH_START="2026-06-01T00:00:00Z"
MONTH_END="2026-07-01T00:00:00Z"

# 1. One call for the whole breakdown
raw=$(curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg s "$MONTH_START" --arg e "$MONTH_END" \
        '{jsonrpc:"2.0",id:1,method:"tools/call",
          params:{name:"integration.metrics.cost_attribution",
                  arguments:{start:$s,end:$e,top_models_limit:10}}}')")

# 2. Unwrap the inner JSON exactly once
data=$(echo "$raw" | jq -r '.result.content[0].text')

# 3. Render as a Slack-friendly table (team | spend | requests)
echo "$data" | jq -r '.by_team | sort_by(-.cost_usd) |
  "| Team | Spend | Requests |\n|---|---|---|",
  (.[] | "| \(.team_name) | $\(.cost_usd | tostring) | \(.requests) |")'

2 · Ingest errored production traces into RAIA

Every hour, pull errored traces, fetch each as a RAIA transcript, and hand the batch to RAIA's eval ingest. Two tools + a loop.

raia-eval-ingest.sh
bash
#!/usr/bin/env bash
# Recipe 2: pull the last hour of errored production traces into
# RAIA's eval dataset. Uses the raia_transcript format so RAIA gets
# the same message shape its scorers expect from customer runs.
set -euo pipefail

# 1. List the trace IDs to ingest
list=$(curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"integration.traces.list",
                 "arguments":{"limit":100,
                              "filters":{"has_error":true}}}}')

trace_ids=$(echo "$list" | jq -r '.result.content[0].text' | jq -r '.traces[].trace_id')

# 2. Fetch each trace as a RAIA transcript, write to a JSONL file
: > eval_batch.jsonl
for tid in $trace_ids; do
  curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
    -H "Authorization: Bearer $NEXUS_PAT" \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg t "$tid" \
          '{jsonrpc:"2.0",id:1,method:"tools/call",
            params:{name:"integration.traces.get",
                    arguments:{trace_id:$t,format:"raia_transcript"}}}')" \
    | jq -c '.result.content[0].text | fromjson' >> eval_batch.jsonl
done

# 3. Hand the batch to RAIA's ingest pipeline
raia-cli ingest --file eval_batch.jsonl --dataset production-errors-$(date +%Y-%m-%d)

3 · Cost-spike triage for a single key

When a budget alert fires with a virtual key id, print a per-day-per-model breakdown so on-call can see what model or day drove the spike.

cost-spike.sh
bash
#!/usr/bin/env bash
# Recipe 3: cost-spike triage
# Given a virtual_key_id that pinged the budget alert, produce a
# per-day-per-model breakdown so an on-call can see what changed.
set -euo pipefail

VK_ID="${1:?usage: cost-spike.sh <virtual_key_id>}"

curl -sS -X POST "$NEXUS_URL/v1/integrations/mcp" \
  -H "Authorization: Bearer $NEXUS_PAT" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg vk "$VK_ID" \
        '{jsonrpc:"2.0",id:1,method:"tools/call",
          params:{name:"integration.usage.by_key",
                  arguments:{
                    virtual_key_id:$vk,
                    bucket:"day",
                    group_by:"model"
                  }}}')" \
  | jq -r '.result.content[0].text' \
  | jq -r '"Totals: $\(.totals.cost_usd) across \(.totals.request_count) requests\n",
           "By model:",
           (.groups[] | "  \(.key): $\(.totals.cost_usd)")'
These scripts hit real endpoints — set NEXUS_PAT and NEXUS_URL first, and expect a jq install. Port to Python/Go by replacing the curl+jq pair with your language's HTTP client plus the same JSON-in-string unwrap.

Per-tenant isolation guarantees

  • Org id is derived from the token, never from the request body. Even if a caller passes "organizationId":"org_B" in arguments, every ClickHouse and Postgres query filters on the org the PAT resolved to.
  • Foreign-org resources return not_found, not permission_denied. The existence check runs against Postgres before the analytics query fires — no timing side-channel to enumerate keys or teams in a different tenant.
  • Scope enforcement is per-tool. A token scoped to something other than integration is refused at the dispatcher, even for tools whose required permission is technically shared with another surface.
  • Regression coverage: two-tenant dispatcher tests (org A token + org B fixtures) run in CI and would catch any wiring change that leaks org ids.

Onboarding checklist

  1. Enable the product for the customer org: Control plane → Settings → Suite Products → toggle raia or finops on. (No entry needed the first time — the entitlement gate is fail-open until the org has any Suite Products row.)
  2. Mint a service-account PAT: Control plane → Settings → Service Accounts → Create service account. Scope: integration. Product: raia / finops / custom. Copy the plaintext token before closing the dialog.
  3. Verify from the caller: run integration.ping. Expect { "pong": true, "org_id": "...", "product_slug": "raia" }. On suite_product_disabled, revisit step 1. On unauthorized, re-mint the token.
  4. Wire the caller: point RAIA / FinOps at POST /v1/integrations/mcp, cache tool discovery (tools/list is stable per release), and use the JSON-RPC id field for distributed-trace correlation.
  5. Watch observability: alerts on integrations_mcp_calls_total{status="error"} catch entitlement regressions and mis-scoped tokens fast.

Observability

  • Prometheus: integrations_mcp_calls_total{tool,status} — one increment per tool call. status ∈ ok | error.
  • Audit trail: every call emits an integrations.mcp.call audit event with the tool name, resolved permission, product slug, IP, and actor id. Available under Audit Logs.
  • Traces: cross-product callers should propagate traceparent when calling /v1/integrations/mcp. Argus doesn't fabricate one when it's missing, so RAIA / FinOps can pin their own trace id to the audit row.