Skip to main content

MCP Server

The Fincome MCP server gives LLM agents (Claude, Cursor, …) secure, read-only access to your company's financial metrics (MRR, ARR, churn, revenue, …) and row-level views (invoices, subscriptions, …) through the Model Context Protocol. The agent logs in as one of your users and sees exactly the data that user is allowed to read in the dashboard.

Connect

The server speaks MCP over Streamable HTTP and authenticates with OAuth 2.1 (Cognito hosted UI, authorization code + PKCE). Point your MCP client at the server URL — it discovers the login flow automatically and opens the hosted login in your browser:

mcp.json (Claude Desktop / Cursor)
{
"mcpServers": {
"fincome": {
"url": "https://mcp.fincome.co/mcp"
}
}
}

On first use the client redirects you to the Fincome login (the same credentials as the dashboard); you approve access and the agent receives a scoped, revocable token tied to your user and company.

Connecting multiple accounts

Each connection is tied to one user and company. To connect several Fincome accounts at once — say you manage multiple companies — give each its own server URL: clients like Claude Desktop won't accept two servers that share the same URL. The server also answers at …/mcp2, …/mcp3, … through …/mcp10 (the same server, ten interchangeable URLs in all). Add one entry per account and authorize each separately — log in as the account you want when you approve it.

mcp.json — two companies
{
"mcpServers": {
"fincome-acme": { "url": "https://mcp.fincome.co/mcp" },
"fincome-globex": { "url": "https://mcp.fincome.co/mcp2" }
}
}

You can review and revoke every connection at any time from Settings → Connected AI assistants in the dashboard.

Tools

ToolPurpose
list_metricsList the metrics you can read, grouped into report families.
describe_metricA metric's label, unit, kind, and the breakdown/filter dimensions available for your company (incl. your custom axes).
get_metricA metric's time series — optionally broken down by a dimension and/or filtered.
list_viewsList the row-level views you can read.
describe_viewA view's columns (name + type) — without reading any rows.
query_viewRead rows from a view, with structured filters, sorting and pagination.

list_metrics

No arguments. Returns the metrics you can read, grouped into report families (one level deep). Use a metric's slug with the other metric tools; dimensions are returned by describe_metric, not here.

list_metrics() → [MetricFamily] (excerpt — 18 families in all)
[
{ "id": "mrr", "label": "Mrr", "metrics": [
{ "slug": "mrr", "label": "mrr", "unit": "currency", "kind": "point_in_time" },
{ "slug": "arr", "label": "arr", "unit": "currency", "kind": "point_in_time" }
] },
{ "id": "mrr-growth", "label": "Mrr Growth", "metrics": [
{ "slug": "mrr_growth_customers", "label": "mrr_growth_customers", "unit": "currency", "kind": "periodic" },
{ "slug": "arr_growth_customers", "label": "arr_growth_customers", "unit": "currency", "kind": "periodic" }
] },
{ "id": "churn", "label": "Churn", "metrics": [
{ "slug": "customer_revenue_churn", "label": "customer_revenue_churn", "unit": "percent", "kind": "point_in_time" },
{ "slug": "customer_count_churn", "label": "customer_count_churn", "unit": "percent", "kind": "point_in_time" }
] },
{ "id": "ltv-over-cac", "label": "Ltv Over Cac", "metrics": [
{ "slug": "ltv_over_cac", "label": "ltv_over_cac", "unit": "number", "kind": "point_in_time" }
] }
]

Other families include revenue, renewals, arpa_and_acv, cac, ltv, revenue-retention, number-of-subscribers, quick-ratio, and growth-rate.

describe_metric

describe_metric(slug)   # slug from list_metrics, e.g. "mrr"

Returns the metric's label, unit, kind, and the dimensions available for your company (incl. your custom axes). Each dimension has an id and a human label — pass either to get_metric's dimension/filters. A filter_only dimension can be used in filters but not as a breakdown.

describe_metric('mrr') → MetricDescriptor
{
"slug": "mrr", "label": "MRR", "unit": "currency", "kind": "point_in_time",
"dimensions": [
{ "id": "product/id", "label": "Product", "filter_only": false },
{ "id": "customer/country", "label": "Country", "filter_only": false },
{ "id": "customer/custom_axis_5", "label": "vertical", "filter_only": false },
{ "id": "price/amount", "label": "Amount", "filter_only": true }
]
}

get_metric

get_metric(
slug, # from list_metrics, e.g. "mrr"
date_start?, date_end?, # ISO YYYY-MM-DD; default = last year up to your data's freshness date
timegrain = "month", # month | quarter | year
dimension?, # an id or name from describe_metric, e.g. "product/id" or "vertical"
filters? = [ { field, op, value } ] # op ∈ eq, neq, in, gt, gte, lt, lte, contains, between
)

A metric is either point_in_time (a stock measured at each period end — MRR, ARR; each point carries an as_of) or periodic (a flow totalled over the period — revenue, churn). With a dimension, the result has one series per dimension value (e.g. MRR by product); otherwise a single "total" series. Date ranges are inclusive of whole periods: 2024-05-012024-05-31 returns May; 2024-05-012024-06-01 returns May and June.

get_metric(slug='mrr', dimension='product/id', timegrain='month') → MetricResult
{
"slug": "mrr", "label": "MRR", "unit": "currency", "currency": "EUR",
"kind": "point_in_time", "timegrain": "month", "dimension": "product/id",
"series": [
{ "key": "Acme Plan", "label": "Acme Plan",
"points": [ { "period_start": "2024-01-01", "period_end": "2024-01-31",
"value": 50000.0, "as_of": "2024-01-31" } ] }
],
"meta": { "company_id": 77, "computed_at": "…", "freshness_date": "2024-08-15" }
}

list_views

No arguments. Returns the row-level views you can read (e.g. invoices, subscriptions, MRR by customer). Use a view's slug with describe_view / query_view.

list_views() → [ViewDescriptor]
[
{ "slug": "mrr_by_customer.bucketized", "label": "MRR by customer" },
{ "slug": "invoice_line_items.bucketized", "label": "Invoice line items" },
{ "slug": "subscriptions.bucketized", "label": "Subscriptions" }
]

describe_view

describe_view(slug)   # slug from list_views, e.g. "mrr_by_customer.bucketized"

Returns the view's columns (name + type) without reading any rows. Use the column names as fields in query_view's filters / order_by.

describe_view('mrr_by_customer.bucketized') → ViewDescriptor (no rows)
{
"slug": "mrr_by_customer.bucketized", "label": "MRR by customer",
"columns": [
{ "name": "timebucket", "type": "DATETIME" },
{ "name": "customer/name", "type": "VARCHAR" },
{ "name": "customer/country", "type": "VARCHAR" },
{ "name": "mrr_contribution", "type": "FLOAT" },
{ "name": "n_active_subscriptions", "type": "BIGINT" }
]
}

query_view

query_view(
slug, # from list_views, e.g. "mrr_by_customer.bucketized"
filters? = [ { field, op, value } ], # field = a view column; no raw SQL
order_by?, order_dir = "asc", # order_dir = "asc" | "desc"
limit = 100, # max 1000
offset = 0, # >= 0
timegrain = "month" # month | quarter | year (for time-bucketed views)
)

Returns columns + rows + a page cursor (has_more). Filters are structured only — { "field": "customer/country", "op": "in", "value": ["FR", "DE"] } — never raw SQL. The contains op is a case-insensitive substring match where SQL LIKE wildcards in the value are honored: % matches any run of characters and _ any single character (e.g. "ab%" is a prefix match).