Skip to content

Use leancosts with an agent

Point an AI agent at leancosts and let it answer from your bill instead of guessing. leancosts runs a Model Context Protocol server at POST /api/mcp. Every read the web app performs is also a tool, so an agent reaches the same numbers the pages render.

Claude Desktop, claude.ai, Cowork, mobile. Add a custom connector and approve it once: there is no token to copy. The full walkthrough, including what the grant permits and how to revoke it, is Connect Claude to leancosts.

https://api.leancosts.com/api/mcp

Claude Code. It can send a header, so it uses a Personal Access Token (Use the API with a token) and skips OAuth:

Terminal window
claude mcp add --transport http leancosts \
https://api.leancosts.com/api/mcp \
--header "Authorization: Bearer leancosts_pat_xxxxxxxx"

The leancosts plugin for Claude Code. It adds the server and two skills: remediate takes one savings opportunity to executed, and connect takes one cloud to its first sync. Export a Personal Access Token, then install it:

Terminal window
export LEANCOSTS_PAT=leancosts_pat_xxxxxxxx
/plugin marketplace add intersector-io/leancosts
/plugin install leancosts@leancosts

Codex. Codex reads the same skills. Copy them into ~/.agents/skills/ and add the server to ~/.codex/config.toml:

[mcp_servers.leancosts]
url = "https://api.leancosts.com/api/mcp"
bearer_token_env_var = "LEANCOSTS_PAT"

Any other MCP client. The transport is stateless Streamable HTTP: one JSON-RPC message per POST, application/json back. GET and DELETE answer 405, because there is no session to resume.

Terminal window
curl -s https://api.leancosts.com/api/mcp \
-H "Authorization: Bearer leancosts_pat_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Setup. get_onboarding_status says which clouds are connected and what is still missing. get_grant_artifacts returns the read-only role or policy with your organization’s values already substituted, for you to run with your own cloud CLI. Azure connects through a device-code sign-in (start_azure_oauth), AWS through a cross-account role (create_aws_connection). trigger_sync, get_sync_progress and get_sync_logs cover the first ingest and why it failed.

Costs. get_cost_summary is the canonical monthly total and its breakdowns. get_cost_variance explains a movement between two months, get_daily_costs splits one month by day, and get_cost_evolution gives the month-over-month grid. get_cost_forecast returns the projection with its disclosed band. query_costs runs an ad-hoc pivot, and export_costs hands back the raw line items when the agent has to compute something leancosts does not.

Findings and proof. list_opportunities is the Savings Register with its one canonical open-claim total; get_opportunity adds the decision history and, where the hunter produced one, the formula behind the claim. list_hunter_findings and get_hunter_rollup read the last hunter snapshot. list_ledger_entries is what the bill has actually graded, and get_change_request is what your team did about a finding.

Triaging a finding. Every decision the register offers on the web has a tool with the same permission: accept_opportunity, defer_opportunity (look again on a date), discard_opportunity (not now; it comes back if the cost changes), reject_opportunity (never), assign_opportunity, set_opportunity_external_reference (your ticket id), defend_opportunity (the figure finance agrees to, on an open row) and attribute_opportunity (whether your team fixed a finding that stopped being detected). Each one records the decision in leancosts and writes it to the audit trail, which list_activity reads back.

Explaining a cost movement. list_variance_notes reads the notes people left on a pair of months (a from and a to, both YYYY-MM), and add_variance_note records one the human gives you. A note is plain text, tied to that pair, and changes no cost, anomaly or forecast. Reading needs no extra permission; adding one needs the same right as a note on a resource.

Acting on a finding. accept_opportunity, draft_change_request, submit_change_request and approve_change_request take a register row through the same steps as the web app, with the same permissions. Dual control holds: the author cannot approve their own request unless your organization turned self-approval on. get_change_request_execution_bundle hands back the script, and mint_execution_receipt_token a token that can only report that one change request as done. You run the script with your own credentials, and it posts the receipt itself.

Tags and allocation. get_tag_coverage ranks the gaps by what each untagged resource costs. get_tag_catalog is the taxonomy, list_allocation_rules and list_shared_cost_patterns are how spend is attributed to teams, and get_tag_reconciliation groups violations for bulk fixing. list_staged_tags and list_virtual_tags show tags that exist inside leancosts before, or instead of, reaching the cloud.

Saved queries. list_saved_queries names the cost questions your organization has agreed on, and run_saved_query re-asks one by id or slug. A saved query stores the question, never the figures, so it answers from today’s data.

Talking to support. contact_support sends the leancosts support team a bug report, a feature suggestion or a question, under your name. Support replies to your email address. The agent should show you the message before it sends it. It works during a trial too, and allows 10 messages per person per hour.

Setup:

  • “Using leancosts, what’s connected and what’s still missing?”: get_onboarding_status
  • “Give me the read-only AWS role for leancosts so I can run it myself.”: get_grant_artifacts
  • “Connect our Azure tenant.”: start_azure_oauth, then get_azure_oauth_status
  • “Kick off a sync on the production subscription and tell me when it lands.”: trigger_sync, get_sync_progress
  • “Why did the last sync fail?”: get_sync_logs

Costs:

  • “What did we spend last month, by service?”: get_cost_summary
  • “Why did the bill move between June and July?”: get_cost_variance
  • “Which day drove last month’s spike, and which resources?”: get_daily_costs
  • “Show storage spend by team tag over the last six months.”: get_cost_evolution, query_costs
  • “Are we going to blow the budget this month?”: get_cost_forecast
  • “Pull the raw lines for June so I can reconcile them in a spreadsheet.”: export_costs
  • “Which reservations are underutilised?”: list_commitments
  • “The August Functions spike was a one-off backfill. Note that on August against July.”: add_variance_note

Findings and proof:

  • “What are our open savings opportunities, biggest first?”: list_opportunities
  • “Show me the arithmetic behind that idle-disk claim.”: get_opportunity
  • “How much have we actually saved so far this year?”: list_ledger_entries, get_impact_summary
  • “What is this VM, what does it cost, and is it covered by a reservation?”: get_resource
  • “What does the idle-database hunter read, what does it claim, and when does it stay quiet?”: describe_hunter
  • “What change requests are waiting on approval?”: get_change_request

Triaging a finding:

  • “Push the idle-disk finding to next quarter.”: defer_opportunity
  • “Dismiss that one for now: it is our DR replica.”: discard_opportunity
  • “Give the database findings to ana@example.com and link ticket FIN-142.”: assign_opportunity, set_opportunity_external_reference
  • “Who dismissed findings last month, and why?”: list_activity

Acting on a finding (the remediate skill):

  • “Fix the top leancosts opportunity.”: list_opportunities, accept_opportunity, draft_change_request, submit_change_request, approve_change_request, get_change_request_execution_bundle, mint_execution_receipt_token

Tags and allocation:

  • “How much untagged spend do we have, and where is it?”: get_tag_coverage
  • “Which tag keys are we supposed to be using?”: get_tag_catalog
  • “Break last month’s bill down by cost centre.”: list_allocation_rules
  • “Stage the missing env=prod tags on those resources.”: stage_tags
  • “Treat everything named payments-* as team=payments inside leancosts.”: upsert_virtual_tag
  • “Who is violating the tag policy right now?”: get_tag_reconciliation

Saved queries:

  • “What cost questions has the team already saved?”: list_saved_queries
  • “Run the monthly-showback query.”: run_saved_query
  • “Save that pivot as platform-monthly so finance can re-run it.”: save_query

Support:

  • “Tell leancosts support the forecast chart is empty for last month. Show me the message first.”: contact_support with category: bug
  • “Suggest a feature to leancosts: a weekly digest in Slack.”: contact_support with category: feature

Every tool the endpoint serves, with its inputs and its gating. This section is generated from the server’s own registries, so it cannot disagree with what you can call.

Connect a cloud and watch the first sync. A locked trial can reach these, and the tools marked static, but nothing else.

ToolWhat it answersInputsNotes
create_aws_connectionCreate an AWS connection using the cross-account role from get_grant_artifacts (secretless: access keys are deliberately not accepted).name (string, required); payerAccountId (string, required); payerRoleArn (string, required); externalId (string, required); memberRoleArnTemplate (string)needs costs.connections.manage · reads connector setup state
create_oci_connectionCreate an OCI (Oracle Cloud) connection for one tenancy.name (string, required); tenancyOcid (string, required); userOcid (string, required); homeRegion (string, required)needs costs.connections.manage · needs the oci toggle · reads connector setup state
create_salesforce_connectionCreate a Salesforce connection for one org.name (string, required); loginUrl (string, required); consumerKey (string, required); username (string, required)needs costs.connections.manage · needs the salesforce toggle · reads connector setup state
get_azure_oauth_statusPoll a One-Click Azure Connect flow started with start_azure_oauth.flowId (string, required)needs the azure toggle · reads connector setup state
get_connection_permissionsWhich documented read-only grants of one Azure, AWS or GCP connection are provably working, from evidence the last syncs already persisted (no cloud call).provider (string, one of azure, aws, gcp, required); connectionId (string, required)needs costs.connections.manage · reads connector setup state
get_grant_artifactsMachine-readable grant artifacts for connecting a cloud, with per-org values (AWS ExternalId) already substituted.provider (string, one of azure, aws, gcp, required); agreement (string, one of standard, ea, mca, csp)reads connector setup state
get_onboarding_statusWhere this organization stands in setup.nonereads connector setup state
get_sync_logsTail the persisted sync/validation log lines for one connection (the same stream the web Connection Console shows).connectionId (string, required); since (integer, default 0)reads connector setup state
get_sync_progressLive progress of running syncs for this organization (phase, counts, per-connection).connectionId (string)reads connector setup state
probe_connectionRun a read-only probe of the account behind one OCI or Salesforce connection, with the connection’s stored credentials (same as the Run probe button).provider (string, one of oci, salesforce, required); connectionId (string, required)needs costs.connections.manage · reads ingested data (refused for a locked trial)
request_admin_handoffWhen the person you are helping cannot create Azure role assignments (Owner or User Access Administrator on the subscription), hand the connect step to whoever can.adminEmail (string)needs costs.connections.manage · reads connector setup state
start_azure_oauthStart the One-Click Azure Connect device-code flow: returns a verificationUri + userCode for the customer to sign in with.tenantId (string, required); name (string, required)needs costs.connections.manage · needs the azure toggle · reads connector setup state
test_connectionRe-validate a connection against the provider right now (a real cloud API call, same as the Test button).provider (string, one of azure, aws, gcp, oci, salesforce, required); connectionId (string, required)needs costs.connections.manage · reads connector setup state
trigger_syncStart a sync for one connection (same as the Sync button).provider (string, one of azure, aws, gcp, oci, salesforce, required); connectionId (string, required); force (boolean, default false)needs costs.connections.manage · reads connector setup state

What the bill is, where it moved, and what it is forecast to be.

ToolWhat it answersInputsNotes
compare_periodsCompare two billing periods and explain the movement.mode (string, one of variance, anomaly, default "variance"); month (string)reads ingested data (refused for a locked trial)
export_costsThe raw cost LINE ITEMS for one or more billing months, paged.periods (array of string, required); format (string, one of json, focus, csv, default "json"); providers (array of string, one of azure, aws, gcp); accounts (array of string); services (array of string); tags (array of string); offset (integer, default 0); limit (integer, default 1000)reads ingested data (refused for a locked trial)
get_bedrock_usageAWS Bedrock model usage for a day window, read from the AWS cost data the AWS connector already ingests.from (string); to (string)needs the bedrock toggle · reads ingested data (refused for a locked trial)
get_cost_by_tagThe tags-by-month grid: every value of one tag key with its cost in each month of the range, its row total, the realized and planned savings credited to it, and its month-end projection (the same blend the /costs headline uses, over that row’s own months; available: false means no defendable forecast for that value, not zero, and the reason says which gate it failed).tagKey (string); from (string); to (string); lens (string, one of live, planned, default "live"); providers (array of string, one of azure, aws, gcp); limit (integer, default 100)reads ingested data (refused for a locked trial)
get_cost_evolutionThe month-over-month grid: every service (or every value of one tag key) with its amount in each ingested billing month, ranked by total.dimension (string, one of service, tag, default "service"); tagKey (string); providers (array of string, one of azure, aws, gcp)reads ingested data (refused for a locked trial)
get_cost_forecastThis month’s projected spend with its DISCLOSED confidence band, next month’s model point, and the Azure budgets mirrored from the customer’s own subscriptions with how much of each is already consumed.nonereads ingested data (refused for a locked trial)
get_cost_metersDrill one scope (an account, a service, or a single resource) down to its METER grain: the billed units under the money, with the top contributing resources.accountId (string); serviceName (string); resourceId (string); period (string); topResources (integer, default 10)reads ingested data (refused for a locked trial)
get_cost_readinessWhether the ingested data is COMPLETE enough to reason over, plus the baseline cohort the program measures against.windowMonths (integer, default 12); providers (array of string, one of azure, aws, gcp)reads ingested data (refused for a locked trial)
get_cost_summaryThe organization’s canonical spend summary.lens (string, one of live, planned, default "live"); providers (array of string, one of azure, aws, gcp); region (string); cohortPeriod (string)reads ingested data (refused for a locked trial)
get_cost_varianceExplain the movement between two billing months.mode (string, one of services, pvm, default "services"); from (string, required); to (string, required); accountId (string); providers (array of string, one of azure, aws, gcp)reads ingested data (refused for a locked trial)
get_daily_costsDay-by-day spend inside one billing month, each day with its top services.period (string); date (string); topResources (integer); providers (array of string, one of azure, aws, gcp)reads ingested data (refused for a locked trial)
get_dynatrace_usageDynatrace observability spend for a day window.from (string); to (string); limit (integer, default 50)needs the dynatrace toggle · reads ingested data (refused for a locked trial)
get_finops_scorecardHow well the org is running FinOps, from three angles.view (string, one of resources, maturity, teams, default "resources"); status (string, one of red, yellow, green); limit (integer, default 50)reads ingested data (refused for a locked trial)
get_impact_summaryThe realized-savings ledger rolled up: what the org’s executed changes actually saved, measured against the bill, net of the tool’s own subscription cost.from (string); to (string)reads ingested data (refused for a locked trial)
get_kubernetes_costsCost allocated per Kubernetes namespace, per cluster, for the current and previous billing month - AKS, EKS and GKE.months (integer, default 2)reads ingested data (refused for a locked trial)
get_savings_pipelineThe savings funnel (identified → in-flight → executed → realized) plus the counterfactual: what the bill would have been had none of the executed work happened.windowMonths (integer, default 12)reads ingested data (refused for a locked trial)
get_tag_driversExplain ONE tag value’s month-over-month swing: which resources and services under that tag drove the change against the prior calendar month.tagKey (string, required); tagValue (string); period (string, required)reads ingested data (refused for a locked trial)
list_alert_rulesThe org’s alerting configuration and history.include (string, one of rules, channels, events, default "rules")reads ingested data (refused for a locked trial)
list_anomaliesDetected cost anomalies with their totals and classification split.status (string, one of open, acknowledged, snoozed, withdrawn); direction (string, one of increase, decrease); classification (string); includeSuppressed (boolean, default false)reads ingested data (refused for a locked trial)
list_change_requestsRecent change requests across every status (draft / approved / rejected / executed) with author, estimated monthly impact and last update.nonereads ingested data (refused for a locked trial)
list_commitmentsActive reservations and savings plans with utilization %, covered dollars and monthly waste, worst first.providers (array of string, one of azure, aws, gcp)reads ingested data (refused for a locked trial)
list_connector_suggestionsVendors the cloud bill already charges for that the org has not connected: Azure DevOps and Databricks billed through the Azure invoice.limit (integer, default 50)reads ingested data (refused for a locked trial)
list_insightsThe org’s open optimization findings.view (string, one of by_category, ranked, default "by_category")reads ingested data (refused for a locked trial)
list_resourcesThe org’s cloud resource inventory grouped by provider, with each resource’s tags.providers (array of string, one of azure, aws, gcp)reads ingested data (refused for a locked trial)
query_costsAggregate this organization’s ingested cost lines.groupBy (string, one of service, account, period, provider, resource, tag, default "service"); tagKey (string); period (string, one of last_month, last_3_months, last_6_months, last_12_months, all_time, explicit, default "last_3_months"); periods (array of string); providers (array of string, one of azure, aws, gcp); services (array of string); accounts (array of string); tagFilters (array of string); tagExcludes (array of string)reads ingested data (refused for a locked trial)

The products bought through AWS Marketplace and Google Cloud Marketplace: spend, seller, the contract term and renewal behind a paid-up-front fee, and the connector that explains each product. A contract is shown only when its agreement matches a payment to the cent, and the read says when the agreements could not be read.

ToolWhat it answersInputsNotes
list_marketplace_productsEvery product the org buys through a cloud marketplace (AWS Marketplace, Google Cloud Marketplace and Azure Marketplace) over the last 13 months, largest first.provider (string, one of aws, gcp, azure); limit (integer, default 50)reads ingested data (refused for a locked trial)

DBU consumption and its list cost, read from the Databricks account’s own system tables. List price is not an invoice, and for an Azure workspace the invoice is on the Azure bill beside it.

ToolWhat it answersInputsNotes
get_databricks_usageDatabricks DBU usage and list cost for the organization’s connected workspaces over the last 30, 60 or 90 days.days (string, one of 30, 60, 90, default "30")needs the databricks toggle · reads ingested data (refused for a locked trial)
list_databricks_computeThe Databricks compute inventory the last sync captured.kind (string, one of cluster, warehouse, pool); workspaceId (string); limit (integer, default 50)needs the databricks toggle · reads ingested data (refused for a locked trial)

The savings register, the hunters behind it, the change requests, and the ledger that grades them against the bill.

ToolWhat it answersInputsNotes
approve_change_requestApprove a SUBMITTED change request.id (string, required)reads ingested data (refused for a locked trial)
create_change_requestFile a change request for something YOU found - a check leancosts does not ship as a hunter.title (string, required); why (string, required); scope (object); changes (array of object); links (array of string); opportunityId (string); submitImmediately (boolean, default false); detectionPattern (object); estimatedMonthlyImpactUsd (number)needs change_request.create · reads ingested data (refused for a locked trial)
describe_connectorWhat connecting a provider buys, WITHOUT a connection.provider (string, one of azure, aws, gcp, oci, databricks, openai, anthropic, github, dynatrace, azure_devops, salesforce)static (reads none of your data)
describe_hunterHow a hunter works, WITHOUT running it.source (string); kind (string)static (reads none of your data)
draft_change_requestTurn an accepted Savings Register row into a DRAFT change request.opportunityId (string, required); targetSku (string)needs change_request.create · reads ingested data (refused for a locked trial)
get_change_requestOne change request: what it proposes, who authored it, its approval status, and its SIGNED estimated monthly bill delta: NEGATIVE means the bill goes down.id (string, required); include (array of string, one of projection, sub_items)reads ingested data (refused for a locked trial)
get_change_request_execution_bundleThe runnable artifact for an APPROVED change request: the script, its provider and its rollback, packaged for the customer to schedule and run with THEIR OWN credentials.id (string, required)needs change_request.execute · reads ingested data (refused for a locked trial)
get_commitment_recommendationThe spend-ranked resource list behind ONE commitment (reservation or savings plan) recommendation.id (string, required)reads ingested data (refused for a locked trial)
get_hunter_coverageWhich cloud services the hunters looked at for THIS organization in its last sweep, one row per service.provider (string, one of azure, aws, gcp, oci, databricks, openai, anthropic, github, dynatrace, azure_devops, salesforce); state (string, one of not_connected, deferred, not_run, errored, blocked, finding, quiet); limit (integer, default 500)reads ingested data (refused for a locked trial)
get_hunter_readinessWhether the hunters can run yet: connections, discovery, cost completeness and metrics as an ordered stepper with the blocking gap named at each rung.nonereads ingested data (refused for a locked trial)
get_hunter_rollupThe hunter fleet’s top line, from the same snapshot list_hunter_findings reads.view (string, one of categories, impact, families, snapshot_status, default "categories")reads ingested data (refused for a locked trial)
get_ledger_entryOne impact-ledger entry in full, including its evidence window and the scope the measurement was taken over.id (string, required)reads ingested data (refused for a locked trial)
get_opportunityOne Savings Register row in full.id (string, required)reads ingested data (refused for a locked trial)
get_resourceOne resource in depth.externalId (string, required); include (array of string, one of details, meters, metrics, sizing, links, daily); days (integer, default 90)reads ingested data (refused for a locked trial)
list_accountsThe billing containers the organization has connected: Azure subscriptions, AWS accounts, GCP projects, with their provider and name.limit (integer, default 50)reads ingested data (refused for a locked trial)
list_activityThe organization’s audit trail, newest first.category (string, one of authentication, access, tokens, connections, change_requests, savings, tagging, sso, automation, notifications, admin, other); actor (string); since (string); until (string); cursor (string); limit (integer, default 50)needs governance.audit.read · reads ingested data (refused for a locked trial)
list_hunter_findingsThe hunters’ current findings, read from the persisted snapshot, never a live sweep.category (string, one of compute, database, storage, network, commitments, observability-ai, integration); source (string); cloud (string, one of azure, aws, gcp, ai_workload); resourceExternalId (string); includeLowConfidence (boolean, default false); limit (integer, default 50)reads ingested data (refused for a locked trial)
list_ledger_entriesThe impact ledger row by row: every claimed saving with the change it came from and, once graded against the bill, its MEASURED amount.from (string); to (string); status (string, one of draft, posted, disputed, void); entryType (string); limit (integer, default 50)reads ingested data (refused for a locked trial)
list_opportunitiesThe Savings Register: every finding the org has triaged, with its claim and its lifecycle status.view (string, one of register, value_history, value_events, default "register"); cloud (string, one of all, azure, aws, gcp, default "all"); status (array of string, one of surfaced, accepted, deferred, discarded, permanently_rejected, in_progress, done, realized, auto_resolved); source (array of string, one of hunter, commitment, anomaly); direction (array of string, one of down, up); confidence (array of string, one of low, medium, high); claimMin (number); claimMax (number); kind (string); assignee (string); limit (integer, default 50)reads ingested data (refused for a locked trial)
list_resource_metricsThe persisted utilization windows for MANY resources at once - the bulk twin of get_resource with include=metrics, and the way to check one signal across an estate without paying a call per resource.externalIds (array of string, required); metrics (array of string); windowDays (integer)reads ingested data (refused for a locked trial)
list_spend_commitmentsThe aggregate spend obligations above every reservation and savings plan (an Azure MACC, an AWS EDP or PPA, a GCP contract), each with its burn-down.limit (integer, default 50)reads ingested data (refused for a locked trial)
mint_execution_receipt_tokenMint the report-back token for an APPROVED change request: a signed credential scoped to this ONE change request, single purpose, expiring, and revocable from the web.id (string, required)needs change_request.execute · reads ingested data (refused for a locked trial)
record_change_request_executionRecord what actually happened to an APPROVED change request.changeRequestId (string, required); executedAt (string, required); outcome (string, one of as_proposed, modified, not_applied, required); note (string); executedChanges (array of object)needs change_request.execute · reads ingested data (refused for a locked trial)
search_resourcesFind resources across the estate by provider, type, resource group, account, tag or free text, ranked by their latest CLOSED period’s attributed spend.nameContains (string); providers (array of string, one of azure, aws, gcp); resourceTypes (array of string); resourceGroups (array of string); accountIds (array of string); tagKey (string); tagValue (string); tagMissingKey (string); minMonthlyCost (number); maxMonthlyCost (number); lens (string, one of live, planned, default "live"); pageNumber (integer, default 1); limit (integer, default 50)reads ingested data (refused for a locked trial)
submit_change_requestSubmit a DRAFT change request for approval (draft → submitted).id (string, required)reads ingested data (refused for a locked trial)

Record the team’s decision on a register row, one tool per decision the web drawer offers: accept, defer, dismiss for now or for good, answer who resolved it, assign an owner, set the defended amount, attach a ticket id. Each re-checks the capability its web action needs, and nothing touches the cloud.

ToolWhat it answersInputsNotes
accept_opportunityAccept a Savings Register row: surfaced or deferred → accepted, the triage decision that must come before draft_change_request.opportunityId (string, required)needs register.triage · reads ingested data (refused for a locked trial)
assign_opportunitySet who owns a register row: an organization member’s email, or an empty string to unassign.opportunityId (string, required); assignee (string, required)needs register.triage · reads ingested data (refused for a locked trial)
attribute_opportunityAnswer “this opportunity stopped being detected: did your team fix it?” for an auto-resolved row whose attribution is pending.opportunityId (string, required); answer (string, one of applied, independent, required)needs register.triage · reads ingested data (refused for a locked trial)
defend_opportunitySet the defended amount of an OPEN register row.opportunityId (string, required); defendedAmountUsd (number, required); reason (string, required)needs register.defend · reads ingested data (refused for a locked trial)
defer_opportunityDefer a surfaced or deferred register row until a review date in the future (“look at this again later”).opportunityId (string, required); reviewDate (string, required)needs register.triage · reads ingested data (refused for a locked trial)
discard_opportunityDismiss a register row FOR NOW (“not now”), with the human’s reason.opportunityId (string, required); reason (string, required)needs register.triage · reads ingested data (refused for a locked trial)
reject_opportunityReject a register row PERMANENTLY (“won’t fix”), with the human’s reason.opportunityId (string, required); reason (string, required)needs register.triage · reads ingested data (refused for a locked trial)
set_opportunity_external_referenceRecord the customer’s own ticket id (Jira, ServiceNow) on a register row, or an empty string to clear it.opportunityId (string, required); externalReference (string, required)needs register.triage · reads ingested data (refused for a locked trial)

Tag coverage, the taxonomy, allocation rules, the org units above allocation owners with their month rollup, the drivers a shared cost is split by, and tags staged or declared inside leancosts.

ToolWhat it answersInputsNotes
get_allocation_proposalThe latest PERSISTED allocation-rule proposal the product generated for this org.nonereads ingested data (refused for a locked trial)
get_org_unit_rollupOne month of allocated cost rolled up the organization’s units, with the month before.period (string); limit (integer, default 50)reads ingested data (refused for a locked trial)
get_org_unit_treeThe organization’s hierarchy above the allocation owners (presidency, directorates, areas), depth first.limit (integer, default 50)reads ingested data (refused for a locked trial)
get_tag_catalogThe org’s tag taxonomy and the machinery around it.include (array of string, one of definitions, values, vocabulary, normalization, system_tags, tenant_pattern); sourceKey (string); limit (integer, default 50)reads ingested data (refused for a locked trial)
get_tag_coverageHow well the estate is tagged.view (string, one of required, by_key, default "required"); limit (integer, default 50)reads ingested data (refused for a locked trial)
get_tag_reconciliationLegacy tag debt: every resource violating the required taxonomy, grouped for bulk remediation.view (string, one of scan, runs, default "scan"); groupingMode (string, one of resource_group, account, service, with_linked, default "resource_group"); limit (integer, default 50)reads ingested data (refused for a locked trial)
link_org_unit_ownerPut an allocation owner (a recordTypeKey + recordKey target from list_allocation_rules include=summary, such as team = checkout) in an org unit.unitId (string, required); recordTypeKey (string, required); recordKey (string, required)needs costs.allocation.edit · reads ingested data (refused for a locked trial)
list_allocation_driversThe quantity per owner per month that a shared-cost pool is split by.driverKey (string); period (string); origin (string, one of cost, tag, usage, pushed); limit (integer, default 50); offset (integer)reads ingested data (refused for a locked trial)
list_allocation_rulesHow shared and tagged spend is attributed to teams and cost centres.include (string, one of rules, summary, both, trend, default "both"); limit (integer, default 50)reads ingested data (refused for a locked trial)
list_shared_cost_patternsThe rules that spread a shared cost pool (platform, networking, observability) across tag-value targets, each with a PREVIEW of what it would allocate in the latest period.limit (integer, default 50)reads ingested data (refused for a locked trial)
list_staged_tagsThe Tag Studio staging layer: tag changes recorded in leancosts and NOT applied to the customer’s cloud.status (string, one of staged, applied); provider (string, one of azure, aws, gcp, databricks, openai, anthropic, github, dynatrace, external); withCli (boolean, default false); limit (integer, default 50)reads ingested data (refused for a locked trial)
list_virtual_tagsEvery virtual-tag rule in the org, with the tag it writes, the filter that selects resources, how many resources it currently matches (matchedResources) and how many it skipped because the customer’s own cloud tags them production (skippedProduction).nonereads ingested data (refused for a locked trial)
push_allocation_driverReplace the whole month of one pushed driver: the rows sent are the month, and the rows it held before go.driverKey (string, required); period (string, required); unit (string, required); rows (array of object, required)needs costs.allocation.edit · reads ingested data (refused for a locked trial)
stage_tagsStage one tag change across a set of resources.resources (array of string, required); tagKey (string, required); op (string, one of set, unset, default "set"); value (string)needs tag.staging.write · reads ingested data (refused for a locked trial)
suggest_allocation_driversWhich driver could split a shared-cost pool in one month, with the split each would give.patternId (string, required); period (string, required)reads ingested data (refused for a locked trial)
upsert_org_unitCreate an org unit, or change one when id is given (only the fields you pass change).id (string); name (string); parentId (string); levelLabel (string); owner (string); accountingCode (string)needs costs.allocation.edit · reads ingested data (refused for a locked trial)
upsert_virtual_tagCreate or update one virtual-tag rule: a tagKey=tagValue written onto every resource the filter selects.id (string); name (string, required); tagKey (string, required); tagValue (string, required); providers (array of string, one of azure, aws, gcp, databricks, openai, anthropic, github, dynatrace, external); resourceTypes (array of string); tagFilters (array of string); accountIds (array of string); resourceGroups (array of string); missingTagKeys (array of string); nameContains (string)needs tag.staging.write · reads ingested data (refused for a locked trial)

The context people left on a resource (who owns it, why it looks like this, what not to touch) and on a pair of billing months (why the bill moved). A note is inert - it never changes a finding, a cost or a tag - and it is worth reading before recommending an action or explaining a variance.

ToolWhat it answersInputsNotes
add_resource_noteAppend one operational note to a resource, attributed to the caller and timestamped.externalId (string, required); body (string, required)needs costs.notes.write · reads ingested data (refused for a locked trial)
add_variance_noteAppend one note to a from/to pair of billing months, attributed to the caller and timestamped.from (string, required); to (string, required); body (string, required)needs costs.notes.write · reads ingested data (refused for a locked trial)
list_resource_notesOperational notes humans and agents left on one resource (who owns it, why it looks like this, what not to touch), newest first with author and timestamp.externalId (string, required); limit (integer, default 100)reads ingested data (refused for a locked trial)
list_variance_notesPlain-language notes humans and agents left on one from/to pair of billing months (why the bill moved), newest first with author and timestamp.from (string, required); to (string, required); limit (integer, default 100)reads ingested data (refused for a locked trial)

Features a vendor has announced it will retire on a date, and which of the organization’s resources they touch. A retirement is a dated fact, never a saving: it carries no money.

ToolWhat it answersInputsNotes
list_retirementsVendor-announced service retirements that touch the org’s resources.resource (string); withinDays (integer); limit (integer, default 100)reads ingested data (refused for a locked trial)

What the organization has declared its workloads need. A requirement is the customer’s own word, and it is the only thing that unlocks the findings that remove redundancy or shorten backup retention on production.

ToolWhat it answersInputsNotes
list_requirementsEvery active requirement this organization has declared.serviceKind (string, one of managed_database, virtual_machine, service_plan, node_pool, storage, cache, search, messaging, data_platform); scope (string, one of org_default, account, class, resource); limit (integer, default 50)reads ingested data (refused for a locked trial)
set_requirementsDeclare requirements for this organization, 1-100 rows in ONE transaction (all rows land or none do).rows (array of object, required)needs governance.admin · reads ingested data (refused for a locked trial)

The savings appetite (how much evidence a finding needs before it reaches the register) and the monthly budget. Each write carries the value it replaces, so a stale one is refused instead of undoing someone else’s change.

ToolWhat it answersInputsNotes
get_program_tuningThe organization’s savings appetite and monthly budget, as they are in effect now.noneneeds governance.admin · reads ingested data (refused for a locked trial)
set_monthly_budgetSet the organization’s monthly budget in USD, 0-100000000.monthlyBudgetUsd (number, required); expected (number, required)needs governance.admin · reads ingested data (refused for a locked trial)
set_savings_appetiteSet the organization’s savings appetite.appetite (string, one of conservative, balanced, aggressive, required); expected (string, one of conservative, balanced, aggressive, required)needs governance.admin · reads ingested data (refused for a locked trial)

Tune an existing alert rule: its threshold, the channels it delivers to, or whether it is active. Reading rules, channels and events is list_alert_rules (under Cost analysis). A threshold write carries the values you read, so a stale one is refused, and the rule’s name, metric and scope never change.

ToolWhat it answersInputsNotes
update_alert_ruleTune an existing alert rule: its threshold, the channels it delivers to, or whether it is active.ruleId (string, required); threshold (object); expected (object); channelIds (array of string); active (boolean)needs governance.admin · reads ingested data (refused for a locked trial)

Cost questions the organization has named, so the same question is asked the same way twice.

ToolWhat it answersInputsNotes
list_saved_queriesThe cost queries this organization has named and shared.nonereads ingested data (refused for a locked trial)
run_saved_queryExecute one saved query by its id or slug and return the rows, the plain-language summary and the audit trace that defends them.idOrSlug (string, required)reads ingested data (refused for a locked trial)
save_queryName a cost query so the whole organization can re-run it from the Copilot page or from run_saved_query.name (string, required); groupBy (string, one of service, account, period, provider, resource, tag, default "service"); tagKey (string); period (string, one of last_month, last_3_months, last_6_months, last_12_months, all_time, explicit, default "last_3_months"); periods (array of string); providers (array of string, one of azure, aws, gcp); services (array of string); accounts (array of string); tagFilters (array of string); tagExcludes (array of string)needs change_request.create · reads ingested data (refused for a locked trial)

Report a problem, suggest a feature or ask a question. The message is emailed to the leancosts support team under the person’s name, and they reply by email. A locked trial can reach it.

ToolWhat it answersInputsNotes
contact_supportSend a message to the leancosts support team: report a problem (bug), suggest a feature (feature), or ask a question about the product or billing.category (string, one of question, bug, billing, feature, other, required); message (string, required); recommendationRef (string)static (reads none of your data)
  • Read-only by construction. leancosts holds no write credential for your cloud. The tools that write, write inside leancosts: tags staged or virtual, notes, saved queries, requirements, and a change request’s steps (accept, draft, submit, approve, mint its receipt token, record its execution). Each requires the same permission the matching page does. A change reaches your cloud only when you run its script with your own credentials.
  • Typed inputs only. No tool takes a free-text question. The agent picks a pivot, a window and filters; leancosts executes and returns the rows with the audit trace that defends them.
  • Canonical USD. Cost figures come back in canonical USD, so clouds compare. The two exceptions say so in their own output: Azure budgets and Azure Kubernetes namespace rows stay in the currency Azure reports them in, and must not be added to a USD total.
  • Null is not zero. A figure never observed comes back null and renders as an em dash. Reporting it as $0 would be a claim the data does not support.
  • Trial gate. An unsubscribed trial outside its free-scan window can reach the setup tools and nothing else: exactly the surface it reaches in the web app.
  • Metered. Every tools/call counts against your plan’s monthly MCP call allowance; past it the server answers 429 with the number and the reset date. initialize, tools/list and ping are free, so a client can always connect and read the catalog. Your plan and its limits are in Settings → Subscription.
  • Secretless. No tool accepts a client secret, access key or service-account key.

Ask the client to list its leancosts tools, or run the tools/list curl above. A healthy connection answers with the tool array.

ResponseCause
401The token is missing, expired or revoked.
403 oauth_wrong_audienceA connector token is being sent somewhere other than POST /api/mcp.
429This month’s MCP call allowance is spent; it resets on the 1st.
A tool refuses with a trial messageThe workspace is an unsubscribed trial outside its free-scan window.