Tennsa MCP connector
Tennsa OS gives your business its own AI. It knows everything across your systems, comes to you with what needs attention, and works from Claude, ChatGPT or Gemini. This connector is how: it lets an AI client such as Claude or ChatGPT read the daily briefing, and the history Tennsa has recorded behind it, on the business owner's behalf. Everything here is read-only: nothing the client can call changes a record, connects a source or moves money. The one thing a call can spend is an Investigate credit, and the tool that spends it says so.
Connect
Server URL: https://tennsa.com/api/v1/mcp
Transport: Streamable HTTP, in its stateless form: JSON-RPC 2.0 over HTTPS POST, one request per POST, application/json back, no session id and no server-initiated stream. A GET on the endpoint answers 405. The MCP-Protocol-Version header is honoured: an unsupported version answers 400 naming the supported ones, and a request without the header is served as a 2025-03-26 client. A present Origin header is validated (a missing one is accepted; an unrecognised one answers 403). Every response carries an X-Request-Id header.
Authentication: OAuth 2.0 authorisation code with PKCE (S256), public clients, dynamic client registration. A pasted token is not accepted.
Claude (claude.ai, Claude Desktop, Claude Code)
- Open Settings, then Connectors, then Add custom connector.
- Enter the server URL above. Claude registers itself and sends you to
tennsa.comto sign in. - Sign in to your Tennsa account and press Allow on the consent screen, which names the application and states what the connection does and does not reach.
- Claude now lists the tools below. Ask it, for example, "what does my Tennsa briefing say this morning?".
ChatGPT
- Open Settings, then Connectors (or Apps), and add a connector with the server URL above.
- ChatGPT sends you to
tennsa.comto sign in and approve the connection, exactly as above. ChatGPT and Claude are registered as separate clients: an authorisation granted to one cannot be used by the other.
Gemini Enterprise
- Add the server as a custom MCP tool with the server URL above and OAuth 2.0 authorisation, using the client id (and the client secret, if Tennsa issued one) supplied with the listing.
- Gemini sends you to
tennsa.comto sign in and approve, exactly as above. Gemini Enterprise is registered as its own client; an authorisation granted to it reaches nothing else.
Any other MCP client
Point the client at the server URL. An unauthenticated request receives a 401 with a WWW-Authenticate header naming the protected-resource document, from which the client discovers the authorisation server and registers itself. No client secret is issued; PKCE is what binds a code to the software that requested it.
Authentication
| Document or endpoint | URL |
|---|---|
| Protected-resource metadata (RFC 9728) | https://tennsa.com/.well-known/oauth-protected-resource |
| Authorisation-server metadata (RFC 8414) | https://tennsa.com/.well-known/oauth-authorization-server |
| Authorise | https://tennsa.com/oauth/authorize |
| Token | https://tennsa.com/oauth/token |
| Revoke (RFC 7009) | https://tennsa.com/oauth/revoke |
| Register (RFC 7591) | https://tennsa.com/oauth/register |
- Grant:
authorization_codewith PKCE, methodS256only.plainis refused. - Scope:
mcp. A token issued here reaches the MCP endpoint and nothing else on the Tennsa API; any other path answers403 insufficient_scope. - Access tokens live 15 minutes. Refresh tokens live 30 days and rotate on every use. A rotation retires the refresh token it spends and the access token issued beside it at once, in the same step that issues their replacements: if that step fails, nothing is retired and the same refresh token can be tried again (unless the answer was lost after the step completed: the token was then spent, and presenting it again counts as reuse). A rotated refresh token is never accepted twice, even by a second request racing the first; presenting it again revokes the whole grant, and the person who approved the connection approves it again.
- A refused token Tennsa issued says why, while its record is kept (a revoked token's record is deleted within a day of its expiry, any other's 30 days after it). At the MCP endpoint the
401body carries acode(token_rotated,grant_revoked,token_revokedortoken_expired) and areasonsentence, and theWWW-Authenticateheader carrieserror="invalid_token"with the same sentence aserror_description; an access token past its 15 minutes always readstoken_expired. A refused refresh grant answersinvalid_grantwith the cause as itserror_description, told only to the client the token was issued to. A string that is not a Tennsa token gets the plain401. - Authorisation codes are single-use and expire after 60 seconds.
- Revocation:
POST /oauth/revokewith the token (revoking a refresh token also revokes the access token issued beside it), or the person who approved the connection removes it under AI assistants on their Tennsa account page, which revokes every token that client holds for them. Revocation is immediate on every request that follows; there is no cache.
Scopes: both documents advertise mcp and offline_access. Every grant carries a rotating refresh token, and every token response states mcp offline_access; a scope parameter on the authorise request is accepted and never refused. The RFC 8707 resource parameter is accepted on the authorise and token requests and must name https://tennsa.com/api/v1/mcp; any other value answers invalid_target. Confidential clients may authenticate with client_secret_post or client_secret_basic; dynamically registered clients are public and bind their code with PKCE.
Tools
Every tool identifies the business from the bearer token; none takes an account identifier. Every tool declares its behaviour to the client, and every tool declares an outputSchema: the shape of its structuredContent, with every object closed to keys it does not name, so a host may validate what it receives. Each one is derived from what the tool actually returns rather than from an internal model, and a test drives every tool through every branch it has and validates the live response against the declared schema in both directions. Every tool is read-only and non-destructive. The tools that read Tennsa's own store are also idempotent and closed-world - they reach no third party during the call. ask_consultant is the exception on both counts: it is open-world and not idempotent, because it calls Tennsa's model, and each answered call spends one Investigate credit from the business's monthly allowance.
get_briefing
The current operational briefing: the deterministic findings computed from the connected accounting, payments, commerce and stock systems, with a freshness status. Takes one optional argument, verbose: false by default, which returns each finding's headline, severity, sources and suggested action; true adds the full narrative body and the rule name. Either way the response lists what it omitted.
Returns status (ready, stale or not_ready), computed_at, computed_by (delivery, warm, os_render or null when we did not record it - a computed_at later than the day's briefing means it was refreshed, not that something is wrong), age_seconds, a findings list, a coverage block naming the sources the briefing read, and withheld_count with a withheld_note sentence when findings were withheld (see the next section). A cold cache answers not_ready honestly; the tool never computes on demand.
{
"status": "ready",
"computed_at": "2026-09-08T04:30:12+00:00",
"computed_by": "warm",
"age_seconds": 1840,
"findings": [
{
"category": {"key": "customer_payment_behavior", "label": "Customer payment behaviour"},
"severity": "high",
"headline": "Buildmore Holdings is 31 days overdue on R84,500",
"body_plain": "Two invoices ... The last payment from this customer arrived 46 days late.",
"sources": [{"key": "xero", "label": "Xero"}],
"suggested_action": "Call before the next statement run ..."
}
],
"coverage": {"covered": ["xero", "snapscan"], "unreached": {"error": [], "timeout": []}},
"withheld_count": 1,
"withheld_note": "1 finding derived from connected email, calendar or chat data is withheld from this response ..."
}
get_data_coverage
Which sources the business has connected right now, each with its label and a health
state: live, degraded, not_connected or
unknown. A degraded source is still connected - either its
credential has stopped working (needs_reconnect) or the last run
could not read it, and failure_reason says which. last_success_date
is a calendar day, never a time, and is null when no successful day is held;
last_success_basis says which kind of null it is.
last_attempt is when Tennsa OS last read the business's sources - usually
for the briefing, though a refresh or a report can also be that run. The connection
state is live, so it may honestly differ from the briefing's own coverage, which was
recorded when the briefing was computed. Takes no arguments. Every entry carries
hidden_on_this_channel: true for the email, calendar and chat
sources whose findings this channel withholds, and false for every other source. A
hidden entry says whether the source is connected but carries no account identifier. A
mailbox connected with an email password, rather than through Google or Microsoft
sign-in, has no entry of its own and does not count as connected here, so a Gmail or
Microsoft 365 entry can read not connected while such a mailbox is connected.
get_activity
Recent activity on the Tennsa account: briefings sent and refreshed, sources connected or disconnected, reports downloaded. A log of actions, never of content. Takes no arguments.
get_business_areas
The five business areas Tennsa organises a business by (financials, payments and commerce, stock, customer relations, communications), each saying whether the business has a connected source for it and carrying that area's current findings, with its own withheld count. Each answer carries the same status, computed_at, computed_by and age_seconds freshness fields as get_briefing. Takes no arguments.
Each area also carries undisclosed_sources and
undisclosed_note. An area can be connected on a source this channel
may never report a finding from - a Gmail, Microsoft 365 or IMAP mailbox, or a Slack
workspace - and on
a day when no such finding was produced the withheld count is zero and there is
nothing else to say so. These two fields say it regardless: an empty findings list
on such an area is this channel declining to speak for what is connected there, not
a statement that nothing came up. They are independent of
withheld_count and read the same whether or not anything was withheld.
ask_consultant
One question to Tennsa's consultant, answered from the business's connected accounting, payments, commerce, stock and CRM sources and any documents it has uploaded. Takes one argument, question (a string of at most 2000 characters). Stateless: every call stands alone.
Email, calendar and chat sources are never consulted through this channel. That is a property of the code path, not a filter over the answer: the Gmail, IMAP, Microsoft 365 and Slack contexts are not built for this channel at all, every excluded slot is checked empty before the prompt exists, and the call is refused if that check fails. The result always carries email_sources_note saying so, so a client cannot read silence as an empty mailbox or workspace.
Returns status (answered, not_grounded, quota_exceeded or invalid_question), answer, note (why this is not an answer, empty when it is), sources_consulted (display labels of the sources that grounded the answer), unreached (connected sources the grounding load could not read in time), email_sources_note, and the pair credits_used and credits_remaining - the latter an integer, the string unlimited where the tier's ceiling is a runaway guard rather than a quota, or null where no balance was established on that call. Never read null as zero. When nothing this channel reads can ground the question the tool answers not_grounded without running the model and without spending a credit - which for a business whose only connected sources are mail is a statement about this channel, not about what it has connected, and the note says so.
Cost and duration: each answered call uses one Investigate credit from the business's monthly allowance (the same allowance the Investigate page spends) and takes ten to thirty-five seconds. A call that does not answer spends nothing. This tool has its own rate limit (see Limits).
{
"status": "answered",
"answer": "Three customers have slipped past 45 days ...",
"note": "",
"sources_consulted": ["Xero", "SnapScan"],
"unreached": [],
"email_sources_note": "Connected email, calendar or chat sources (Gmail, IMAP mailboxes, Microsoft 365 and Slack) are not consulted through this channel ...",
"credits_used": 1,
"credits_remaining": 487
}
get_history
What Tennsa recorded over a period, and what it did not see. Answers three questions for a date range: the state of one item as at the end of it, what changed inside it, and the business's own documents dated in it. Takes a period (start_date and end_date, each YYYY-MM-DD) and optionally a source and an entity_id.
Every answer carries coverage: how many days in the range Tennsa OS read, how many it could not reach, and how many had no run at all. That is the point of the tool. A day Tennsa did not observe is never reported as a day on which nothing happened, and coverage_note is the sentence a client can repeat to the owner.
Returns status (answered, no_history, declined, invalid_period, invalid_source or unavailable), note, period, source, coverage, coverage_note, state (with its own basis and trusted flag), changes with changes_basis and changes_total, transactions_summary (per-kind counts, and totals over the documents that were realised or issued - see below), transactions with transaction_count and transactions_total, omitted, the completeness flags changes_complete, transactions_complete and truncated, and withheld_count with withheld_note. An empty changes list means three different things, so changes_basis says which: observed (we hold this kind of history for that source and nothing moved), not_recorded_for_this_source (we have never recorded it there) or not_asked (no source was named). A long period is capped at 200 rows and truncated says so, with the totals beside it. A period earlier than the records held answers declined and says how far back they go, rather than guessing. A read that failed answers unavailable, which is not an empty period. On every status other than answered, changes, transactions, the counts and the completeness flags are null: nothing was counted, and null is never zero or an empty list. A completeness flag is false only for the half whose read failed. History derived from connected email, calendar or chat data is withheld, and asking for such a source by name is refused.
transactions_summary holds, per document kind, count (every document dated in the period) and the totals amount and amount_due with their currency. A total is over the documents that were realised or issued, and nothing else: an invoice, bill or credit note that was issued (AUTHORISED or PAID), and a payment or order its connector recorded as realised (the status success). amount_count and amount_due_count say how many documents the totals are summed over. Every other document is counted and in no total, and says why: cancelled_count is how many were cancelled, and not_totalled maps every other status to how many documents carry it - a draft or submitted invoice, a failed, pending or refunded payment, an unpaid order. Each is sent only when it is not zero. A document dated in the period that a later read of its source no longer returned - voided or deleted there: an accounting system often simply stops returning a voided or deleted document - is in no row, not in count and in no total: no_longer_returned says how many, also sent only when it is not zero, and note names each one. A document whose status a total counts but whose amount could not be read (a charge whose amount never arrived, for example) is in count and not in amount_count. note puts all of these in sentences, naming each kind with the rule its own total follows. Refunds are counted and never netted off a total, because providers report them in ways that cannot be netted alike. A partially refunded order is in no total at all: the record holds no refunded amount to take off it. Sales orders from SYSPRO and Odoo are confirmed commitments before tax, not money received, so they are totalled apart from paid orders under sales_order_commitment; their itemised rows keep the kind order. Two currencies are never added together: a kind whose totalled documents carry more than one gives null for the currency and both totals.
This is an as-of view of what was observed and stored. It can honestly differ both from get_briefing, which is a compute-time snapshot, and from get_data_coverage, which is live connection state. None of the three is wrong when they disagree, and a client should not try to reconcile them.
{
"status": "answered",
"period": { "start": "2026-08-01", "end": "2026-08-31" },
"source": "iq_retail",
"coverage": { "window_days": 31, "read_days": 28, "unreached_days": 3, "not_run_days": 0, "unknown_days": 0 },
"coverage_note": "though we could not read iq_retail on 2026-08-04, 2026-08-05 and 2026-08-06, so this period is measured over the 28 days we did see",
"state": { "entity_id": "CEM-42", "as_of": "2026-08-29", "basis": "carried", "trusted": true, "state": { "QtyOnHand": 18.0 } },
"changes": [ { "entity_id": "CEM-42", "as_of_date": "2026-08-29", "state": { "QtyOnHand": 18.0 } } ],
"changes_basis": "observed",
"transactions": [],
"transaction_count": 0,
"transactions_total": 0,
"truncated": false,
"withheld_count": 0
}
What is and is not accessible
Accessible
- The computed findings: headline, plain-text body, severity, category, contributing source labels, suggested action.
- Connection state per source (connected, needs reconnecting).
- The account activity log and the business-area summary.
- A consultant answer grounded on the connected sources other than email, calendar and chat, and on uploaded documents, naming the sources it consulted.
Not accessible
- Findings derived from connected email, calendar or chat data (Gmail, Microsoft 365 and IMAP mailboxes, and Slack). They are withheld by provenance at the boundary, the response says how many were withheld, and the guard fails closed: a finding with no recorded provenance is withheld too.
- Email, calendar or chat content through the consultant. The four mail and chat sources are excluded from the consultant's grounding on this channel by construction, and the answer names the sources it did consult.
- The raw contributing data behind a finding (invoice lists, message subjects, sender addresses). Only the narrative fields are served.
get_historyis the one exception and a narrow one: it serves the business's own recorded documents for a period - date, kind, counterparty, amount, currency, status and reference - because that is what a question about a period asks for. It serves no figure Tennsa has computed about a third party. - Connected-account identifiers for mail and chat sources, credentials of any kind, and every other Tennsa API route.
- Any write: the connection cannot change a record, connect or disconnect a source, or make a payment. The one thing a call spends is the meter - an answered
ask_consultantcall costs one Investigate credit from the monthly allowance.
Why the email exclusion exists: Google's Workspace API user-data policy restricts transfers of Google user data, including data derived from it, to third parties. A tool result handed to an AI vendor is such a transfer, so findings computed from mailbox content stay inside Tennsa's own briefing; the privacy policy sets out this email exclusion. Microsoft 365 and Slack are excluded as a privacy default for the same class of content: correspondence with people who never agreed to have it summarised to an AI vendor.
Limits
- Tool calls per business account: 30 a minute, and no more than 6 in any 10 seconds. Protocol methods (initialize, ping, tools/list and the rest of the connect handshake) do not count against that window; they ride a separate backstop of 120 requests a minute and 40 in any 10 seconds. Over either limit the endpoint answers
429with aRetry-Afterheader, and the body'slimitfield names which window refused. ask_consultanthas its own ceiling within that: 6 calls a minute and no more than 2 in any 10 seconds per business account. Over it the tool answers a tool result withisErrortrue and a retry time, so the other tools stay usable in the same turn. Each answered call uses one Investigate credit.- A protocol problem (unknown method, unknown tool, malformed JSON) is a JSON-RPC error object at HTTP 200, so the client can correlate it by id. HTTP status codes are reserved for the HTTP exchange itself: 401, 402 (subscription lapsed), 403 (scope), 429.
- Batch requests are not supported.
Testing the connector
A reviewer can exercise every tool in a few minutes with a Tennsa test account (credentials are supplied with the directory submission, never on this page).
- Add the connector in your client using the server URL above and sign in with the test account when redirected.
- Ask for the briefing. Expect a
readybriefing with several findings across the account's connected sources (accounting, payments, stock), and a withheld note if any findings derived from connected email, calendar or chat data exist. - Ask which sources are connected. Expect the connected list, with mail and chat sources showing connected but carrying no account identifier.
- Ask about a business area, for example stock. Expect that area's findings and connection state.
- Ask what happened lately on the account. Expect recent activity: briefings sent, sources connected, reports downloaded.
- Ask for the history of a period Tennsa has recorded, for example last month. Expect an
answeredresult whosecoveragesays how many days were read and how many were not, and which never reports an unread day as a day with nothing in it. - Ask for a period from before the account existed, for example 2019. Expect
declined, with a note saying how far back the records go. This is the property the tool exists for: it refuses rather than presenting a gap as a fact. - Ask the consultant a question, for example which customers are paying late. Expect an
answeredresult within about thirty seconds naming the sources it consulted, none of them a mail or chat source. - Remove the connection under AI assistants on the Tennsa account page, or revoke its refresh token at
/oauth/revoke. Expect the next call to answer401.
The discovery documents at the two .well-known URLs above are pure functions of the base URL and answer without touching a database, so they respond in milliseconds.
Support
Support for this connector: info@tennsa.com. Every response carries an X-Request-Id header; quote it, with the client you are using, and we can find the exact call. Nothing about a tool call is logged beyond the tool name, its status and counts.
Operated by Tennsa (Pty) Ltd, South Africa. Privacy policy and terms of service.