Reading the numbers
Use Joinsight in your AI assistant
Add the remote MCP server to Claude, ChatGPT, Gemini, Copilot, Le Chat, Perplexity, Cursor, VS Code or Windsurf, choose a workspace, and ask questions. The tools, what the model sees, and how to revoke access.
Joinsight runs a remote MCP server, so you can ask Claude, ChatGPT, Gemini, Microsoft Copilot, Mistral Le Chat, Perplexity, Cursor, VS Code, Windsurf or any other client that supports remote MCP servers about your revenue, and get the same numbers the dashboard shows. The server is read-only. It serves pre-computed metrics from the same functions behind the dashboard screens. The model's job is to pick the right tool and explain what comes back, not to compute MRR its own way.
This page covers how to connect, what happens when you approve a connection, the tools and what they return, and how to revoke access. For the reasoning behind a metrics server like this, see Ask Claude about your revenue. The product overview is on the MCP server page.
The server URL
The server URL is your Joinsight address followed by /mcp:
https://joinsight.app/mcp
You can also copy it from Settings → AI assistants, under "Connect an AI assistant". The URL is the same for every workspace. You choose the workspace when you approve the connection, not in the URL.
Add it to your assistant
Menu names change between client versions, but the flow is the same everywhere. You add the URL, a browser window opens on Joinsight, you sign in and approve. There is no API key to paste.
Claude
In Claude, open Settings, then Connectors, and choose Add custom connector. Paste the server URL, then sign in to Joinsight and pick the workspace. In a new conversation, check that the connector is turned on before you ask.
In Claude Code, add it from the terminal:
claude mcp add --transport http joinsight https://joinsight.app/mcp
ChatGPT
ChatGPT adds custom MCP servers through developer mode. Open Settings, then Apps, and turn on developer mode under Advanced. Create an app with the server URL and choose OAuth as the authentication. ChatGPT then sends you to Joinsight to approve it.
Cursor
Add the server to ~/.cursor/mcp.json:
{
"mcpServers": {
"joinsight": {
"url": "https://joinsight.app/mcp"
}
}
}
Then select Connect next to joinsight in Cursor's MCP settings and approve it in the browser.
Gemini
In the Gemini web app, open Settings, then Connected apps, and add a custom app with the server URL. Custom apps are only offered on some accounts and regions, and only in English for now; once added, the app also shows in the Gemini mobile app. On Gemini Enterprise, an admin adds Joinsight as a custom MCP server for the team.
Microsoft Copilot
In Copilot Studio, open your agent, add a tool and choose Model Context Protocol. Paste the server URL and pick OAuth with dynamic discovery: Copilot Studio finds Joinsight's sign-in from the server and registers itself, so there is no client ID or secret to enter.
Mistral Le Chat
Open Intelligence, then Connectors, choose Add connector and the Custom MCP connector tab. Paste the server URL. Le Chat detects OAuth and opens Joinsight to approve the connection. On team plans, adding connectors may be limited to admins.
Perplexity
On a paid plan, open Settings, then Connectors, and choose Custom connector. Paste the server URL and sign in. Enable the connector in each thread where you want to use it.
VS Code
Run the command MCP: Add Server, choose HTTP and paste the server URL. VS Code opens Joinsight to approve the connection, and GitHub Copilot's agent mode can then call the tools. To share the setup with a project, add it to .vscode/mcp.json:
{
"servers": {
"joinsight": {
"type": "http",
"url": "https://joinsight.app/mcp"
}
}
}
Windsurf
Add the server to Windsurf's MCP config, ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"joinsight": {
"serverUrl": "https://joinsight.app/mcp"
}
}
}
Other clients
The server uses the Streamable HTTP transport with OAuth, and it answers each request with plain JSON. It has no sessions and no server-sent event stream, so a client that only supports the older SSE transport can't connect. Any client that discovers OAuth from the server's 401 response, as the MCP specification describes, should work.
Approving the connection
When the client sends you to Joinsight, you sign in if you aren't already, and see a consent screen titled "Connect" followed by the app's name. It shows:
- Sends you back to: the host the app will receive the authorization on.
- Identified by: for apps that publish a client metadata document, the host of that document. For apps that registered themselves on the fly, it says "Self-registered. Continue only if you started this from" the app's name. A self-registered app chooses its own name, so only approve it if you just started the connection yourself.
- It can: read MRR, movements and customers. Read page, keyword and App Store revenue. Read alerts and data health. See customer emails only masked.
- It can't: change any data or settings. See your API keys or tokens. See full customer emails. Touch billing or members.
- Workspace: a list of the workspaces you belong to. The connection reads exactly one.
Choose "Allow read access" to approve or "Deny" to refuse. The token the app receives is bound to that workspace, to you and to that app. It can't see any other workspace, even one you own. To ask about a second workspace, connect again and choose it there.
The tools
There are 14 tools. Every one is marked read-only to the client. Inputs with a default can be left out. Period inputs take a key (7d, 28d, 30d, 90d, 12m, this_month, last_month) or explicit dates as {start, end} in YYYY-MM-DD, inclusive, in UTC. An end date in the future is moved back to today, and a period can span at most 3 years.
| Tool | Inputs | Returns |
|---|---|---|
get_overview | period (default 30d) | MRR at the end of the period, split into Stripe and RevenueCat, with at-risk MRR. MRR at the start and the change, net new MRR and its movements, active subscribers, ARPU, trial conversion, attributed share and MRR goal progress. Also one-time revenue in the period against the period before, with the top products. |
get_mrr_timeseries | start (required), end (default today), granularity: day, week or month (default month), source: stripe or revenuecat (default combined), cursor | MRR at the end of each day, week or month, with the split by source and the one-time sales paid in that bucket. 50 points per call. |
get_mrr_movements | period (default 30d), breakdown: plan, source, country or channel (default plan), cursor | New, expansion, reactivation, contraction and churned MRR, net new MRR and customer counts, in total and per group. The attributed share when broken down by channel. |
list_customers | segment: new, churned, at_risk, expiring_card or all (default new), period (default 30d, used by new and churned only), limit (default 20), cursor | Customers with id, masked email, your app's user ID, country, plan, sources, current and first MRR, key dates, lifetime one-time purchases, past-due MRR, card expiry and first touch. Churned customers also carry the MRR they had on their last paying day. all is everyone with MRR now. |
get_cancellation_reasons | period (default 30d), comments (default 10, up to 25) | Churned MRR in the period split into canceled by choice, payment failed and no reason recorded. Each reason with its customers, churned MRR and the first-touch channel most of it came from, plus the latest comments customers wrote when canceling, with masked emails. |
get_revenue_leaks | customers (default 5, up to 20) | The leak audit: forever coupons, customers on old prices, MRR past due for 30+ days and monthly customers ready for an annual plan, each with its monthly value and the largest customers behind it. Also the total leaking each month, which leaves annual candidates out. No period: it reflects billing as it stands. |
get_customer_journey | customer (required): the id from list_customers or your app's user ID | One customer: first page, channel and campaign with referrer and UTM tags, signup time, each subscription per source with its cancel reason and comment, the last 50 MRR changes and the last 50 one-time purchases. |
get_page_revenue | period (default 30d), sort: mrr, paying, signups, sessions, analytics, clicks or rpv (default mrr), limit (default 20), cursor | Per landing page: Search Console clicks, snippet sessions, signups, paying customers, attributed MRR, revenue per visitor and conversion rates, for customers whose first payment is in the period. When GA4 or Plausible is the traffic source, also that tool's sessions or visits per page, as context. Measured. With the attributed share. |
get_keyword_revenue | page (optional path), period (default 90d), limit (default 20) | Revenue per Search Console query, with clicks, impressions and position. Also organic revenue, the part left in hidden queries, and revenue on pages without Search Console data. Modeled. |
get_keyword_opportunities | limit (default 20) | Queries at average positions 8 to 20 on pages that already earn organic Google revenue, with impressions, clicks, revenue per click, the page's revenue over 90 days and a score. Modeled. |
get_app_store_funnel | period (default 30d), source (optional App Store source type) | Impressions, product page views and downloads by source type, or by campaign within one source. Next to them, RevenueCat trials started, new MRR and MRR at the end of the period. |
get_social_posts | period (default 30d), limit (default 20) | Instagram posts published in the period, plus older posts whose link drove anything in it. Views, reach, likes, saves and shares are totals since posting; sessions, signups, paying customers and MRR count the period only. Measured. |
explain_change | metric (required): net_new_mrr, new_mrr, churned_mrr, past_due_mrr, one_time_revenue, signups, sessions or search_clicks; period_a (default 7d); period_b (default: the equal-length window right before) | The metric over both periods and the change, the segments that contributed most across plan, product, source, country, channel, page and query (and cancel reason for churned MRR), each with its share of the change, and annotations in either period. The attributed share for MRR movements. |
get_alerts_and_health | since: a period key or a YYYY-MM-DD day (default 30d) | Up to 50 alerts and new-customer pings since that date, each with its sentence, the cause computed by code and a fix link where there is one. Also the attribution warm-up. |
get_social_posts is only listed in workspaces where Instagram insights are turned on, so a workspace without them sees 13 tools. The demo workspace has them on.
Measured and modeled
Keyword revenue and keyword opportunities are modeled. Google never tells anyone which query a specific visitor typed, so revenue is split across queries by their share of a page's clicks. Those responses have modeled: true, and the server tells the model to call the figures estimates. Everything else is measured. How attribution works explains the difference.
Why explain_change matters
explain_change is the tool that answers "why". It runs the same diff engine as the alerts and the Monday brief. It splits the change into segments and ranks them by how much each one contributed. Because the cause is computed before the model sees it, two people asking the same question get the same cause, and it matches what the brief said on Monday. Annotations you added, such as a price change or a launch, come back with it, so the model can point at them.
The definitions resource and the weekly prompt
The server publishes one resource, joinsight://definitions. It's a Markdown document with every metric definition: MRR, movements, one-time revenue, active subscribers, trial conversion, churn, ARPU, LTV, at-risk MRR, channel, attributed MRR and share, revenue per visitor, GA4 sessions and Plausible visits, keyword revenue, keyword opportunities and how Stripe and RevenueCat are deduplicated. It's the same text as the metric definitions page and the dashboard tooltips. Each tool's description also includes the definitions that apply to it, so the model has them even if the client never reads the resource.
It also publishes a prompt, weekly_review. It calls get_overview for 7 days, explain_change on net new MRR, get_keyword_opportunities, get_alerts_and_health and get_revenue_leaks. Then it asks for a summary under 200 words: MRR and its change, the top causes, one suggested action (the largest leak or a keyword opportunity, whichever puts more money at stake), and any alert or data-health issue. Clients that support prompts show it as a ready-made command. It's the Monday brief, on demand, inside your assistant.
What every response contains
Each response is a JSON object with a few fields that keep answers honest:
as_of: when the response was computed.currency: the workspace's reporting currency.freshness: one entry per source with its status, last sync time and last error, plus the snippet (active,silentornot_installed). If Stripe hasn't synced since yesterday, the model can see it and say so. Search Console lags 2 to 3 days, and the server tells the model to mention it when recent search data matters.modeled: true only for keyword revenue and keyword opportunities.attributed_share: on tools that credit revenue to pages, channels or posts. This is the share of new MRR linked to a first touch, with the customer counts and a note that the rest has an unknown source, not zero. The model is told to say how much could be attributed.- Money: as an object with
minorunits and aformattedstring. The model is told to quote the formatted string, so it never rounds or reformats an amount. - Periods: with
start,endand a label such as "Last 30 days".
Lists return at most 50 rows. Tools that page return a next_cursor. Pass it back as cursor to get the next page. A cursor belongs to the tool that issued it, and passing it to another tool returns an error that tells the model to call again without one.
Questions and the tools that answer them
| Question | Tool |
|---|---|
| What's my combined MRR, and how much is mobile? | get_overview |
| Why did MRR drop last week? | explain_change with churned_mrr or net_new_mrr, period_a 7d |
| Which blog posts brought paying customers last month? | get_page_revenue with last_month, sorted by paying |
| Which channel brings the most new MRR? | get_mrr_movements broken down by channel |
| Which keywords make money? | get_keyword_revenue (modeled) |
| Which keywords should I write more about? | get_keyword_opportunities |
| Who churned this month, and how did they find us? | list_customers with churned, then get_customer_journey |
| Why do customers cancel? | get_cancellation_reasons |
| Where am I losing money in billing? | get_revenue_leaks |
| Whose card expires this month? | list_customers with expiring_card |
| How has MRR grown over the last year? | get_mrr_timeseries by month |
| Where do App Store downloads come from? | get_app_store_funnel |
| Which Instagram posts drove signups? | get_social_posts |
| Did anything break this week? | get_alerts_and_health with 7d |
A good first question is one you already know the answer to, such as last month's MRR. If the answer matches your billing dashboard, you have a baseline for trusting the less obvious ones.
What the model sees
Mostly aggregates and labels: totals, plan names, channel labels, page paths, search queries and post captions. Four tools return individual customers. list_customers, get_revenue_leaks and get_customer_journey include each customer's country, plan, first touch and your app's user ID. get_cancellation_reasons and get_customer_journey include the comment a customer wrote when canceling in Stripe's customer portal, in their own words. Emails are always masked to the first letter of the name and of the domain, as in j•••@e•••.com, and responses say emails_masked: true. A masked email isn't unique, so get_customer_journey refuses one and asks for the customer id instead.
The model never sees your Stripe, RevenueCat or Google credentials, your Joinsight keys, or full customer emails. Nothing is passed through to Stripe or Google: every answer comes from data Joinsight already synced. See Security, privacy and your data for what Joinsight stores in the first place.
Limits and errors
Each connection can make 60 tool calls a minute. Past that, the tool returns a message with the number of seconds to wait. A bad input, such as an invalid date or an unknown customer, returns a message the model can act on. If a query fails on Joinsight's side, the tool returns "Joinsight could not compute this right now. Try again in a minute."
When the workspace is locked because the account's trial or subscription ended, every tool call returns a message saying that access is paused and where the owner can choose a plan. The connection itself stays in place, so the same assistant answers again as soon as a plan is active, with numbers that kept syncing in the meantime. See When access ends.
Every tool call is recorded in the workspace's audit log, whether it succeeded or not, with the tool, its arguments, how long it took, and which app and user made it. Settings → AI assistants shows the latest 50 under "Recent tool calls": when, which app, which member, the tool and its arguments, whether it answered, and how long it took. Every member of the workspace can see it.
Tokens and revoking access
The connection uses OAuth 2.1 with PKCE. You never handle a token yourself, but it helps to know how they behave:
- Access tokens last 1 hour. The client renews them on its own.
- Refresh tokens last 30 days and rotate. Each renewal retires the old refresh token and issues a new pair, so a connection you use keeps working, and one left unused for 30 days lapses.
- Reuse detection. If a refresh token that was already used is presented again, Joinsight assumes it leaked and revokes the whole connection, for both the leaked copy and the real app. You then approve the app again. The same happens if an authorization code is replayed.
- Membership. If you are removed from a workspace, your connections to it stop working at once.
Tokens are stored as hashes, never in plain text.
To see and revoke connections, open Settings → AI assistants. "Connected apps" lists each connection with the app's name, a "Self-registered" badge if the app registered itself, who connected it and when, and when it was last used. "Disconnect" revokes every token of that connection immediately. Any member can disconnect the apps they connected, and owners can disconnect anyone's.
Signing out everywhere ends your browser sessions, not your MCP connections. To cut off an assistant, disconnect it under Settings → AI assistants.