AI assistants (MCP)
Quiddly exposes a remote MCP server at
https://$HOST/mcp. An MCP client - Claude, an IDE agent, or any assistant
that speaks the protocol - signs in as a Quiddly user, the user approves the
connection on a consent screen, and the assistant can then call a curated set
of read-only tools against that user’s data.
Three things hold for every call:
- It is the user’s view. Every tool applies the same tenant and client scoping as the REST API, so a client-scoped user only ever reaches its own client’s rows.
- It is the user’s rights. No new rights exist for MCP. Each tool reuses the right that already gates the same data over REST, so granting a role MCP access is the same decision as granting it the endpoint.
- It only reads. There are no write tools.
- It is your provider’s copy. What the assistant reads leaves Quiddly and is processed by your model provider under your terms with them. See Where the data goes.
Prerequisites
Section titled “Prerequisites”- MCP enabled on your tenant. Two switches, both set by Quiddly: the
Developer module, the same opt-in that gives programmatic access, and
the MCP setting, which switches this surface on its own. Without either,
every MCP and OAuth route answers
404. Contact your Quiddly account manager to enable MCP, or to switch it off again without affecting your other integrations. - A Quiddly user login. MCP signs in through the same hosted login the Operations Portal uses on your tenant. Service accounts cannot sign in here, and neither can users whose roles are managed in your own identity provider through Enterprise Connect. On a tenant where the client portal signs in through a different connection than the Operations Portal, only Operations Portal users can connect.
- The rights the tools need, on that user’s role. See Tools for the right each tool requires.
Connect a client
Section titled “Connect a client”Point the client at https://$HOST/mcp, where $HOST is the API host of the
tenant you are reading this on. Everything else - discovery, registration and
login - happens automatically.
claude mcp add --transport http quiddly https://$HOST/mcpThen run /mcp in the session and pick quiddly to start the login.
Any client that reads an mcp.json:
{ "mcpServers": { "quiddly": { "type": "http", "url": "https://$HOST/mcp" } }}Add a remote (HTTP) MCP server with the URL https://$HOST/mcp and
leave authentication on OAuth. The client discovers the authorization
server from the first 401 it receives.
What happens next:
- The client registers itself with Quiddly and opens your browser.
- You sign in with your Quiddly user on the hosted login page.
- Quiddly shows a consent screen naming the client and the address it will be sent back to. Click Approve. If you did not start this login, or you do not recognise the address, click Cancel.
- The client receives its tokens and lists the tools it may call. Ask your assistant a question about an invoice, a debtor or a payment and it will pick the tool.
Where the data goes
Section titled “Where the data goes”The MCP server is a data interface. Quiddly runs no model: the assistant you connect, and the model provider behind it, are yours. Every tool result the assistant reads leaves Quiddly and is processed by that provider under your agreement with them, not under your agreement with Quiddly.
| What leaves Quiddly | Whatever the tools return for the signed-in user: debtor and client names, organisation and personal identification numbers, contact details, invoice and loan balances, payment references and payer names, collection case data and credit decision statuses. The withheld fields never leave. |
| Where it goes | To the model provider your assistant uses, in the region that provider processes in. Quiddly does not see, store or control that copy. |
| Retention and training | Governed by your provider’s terms. Quiddly cannot limit how long the provider keeps prompts and results, or whether it uses them to improve its models. Check this before connecting. |
| Roles under GDPR | You are the controller. Quiddly processes the data on your instruction as your processor; the call an authenticated user of yours makes through the assistant is that instruction. Your model provider is your processor, not Quiddly’s sub-processor. |
| What stays with Quiddly | Quiddly logs which tool was called, by which user and client, with which argument names, and how many rows came back. The rows themselves are not logged. Nothing is sent anywhere by Quiddly. |
Before you connect an assistant to production data:
- Use a provider you have a data processing agreement with, in a region your own data protection assessment allows.
- Give the connecting user a role with only the rights the assistant needs. MCP inherits the user’s rights exactly; a narrower role is the simplest control.
- Keep a person in the loop for anything the assistant drafts from this data, in particular communication to debtors.
- Asking Quiddly to switch the MCP setting off closes MCP for the whole tenant within a minute, and leaves your other integrations untouched.
How authorization works
Section titled “How authorization works”This section is for anyone building or debugging an MCP client. Quiddly follows the MCP authorization specification (2025-06-18): the MCP server is an OAuth 2.1 resource server and hosts its own authorization server, which delegates the interactive login to Quiddly’s identity provider.
| Endpoint | Purpose |
|---|---|
POST /mcp |
The MCP server. Unauthenticated calls answer 401 with a WWW-Authenticate header that points at the protected resource metadata. |
GET /.well-known/oauth-protected-resource/mcp |
Protected resource metadata (RFC 9728). Names the authorization server. |
GET /.well-known/oauth-authorization-server |
Authorization server metadata (RFC 8414). Lists every endpoint below. |
POST /oauth/mcp/register |
Dynamic client registration (RFC 7591). |
GET /oauth/mcp/authorize |
Starts the login. |
POST /oauth/mcp/consent |
Submitted by the consent screen; this is what issues the authorization code. |
POST /oauth/mcp/token |
Exchanges a code, or a refresh token, for tokens. |
POST /oauth/mcp/revoke |
Revokes a refresh token (RFC 7009). |
Registration
Section titled “Registration”- Only public clients are accepted (
token_endpoint_auth_methodmust benone). redirect_urismust behttps, orhttpon loopback, without userinfo (https://user@host/is rejected). Private URI schemes such asvscode://are accepted only when Quiddly has enabled them for your tenant. At most 10 URIs per registration.- Registration and authorization requests are rate limited per caller IP.
Authorization is also limited per
client_id, counting only logins that never return to the callback, with a much higher ceiling.
Authorization request
Section titled “Authorization request”- PKCE with
S256is required. redirect_uriis required and must match one from the registration exactly.resource(RFC 8707) is accepted and must behttps://$HOST/mcp. The token is bound to that resource and is rejected anywhere else.statemay be at most 512 characters.- The authorize response sets a short-lived cookie on the user’s browser, and the callback only continues in the browser that carries it. Open the authorize URL in the user’s browser and let that browser return; do not fetch it server-side or hand the callback URL to another browser.
- A request that fails these checks, or a callback arriving in a browser that did not start the login, is answered with an error page in the browser, not a redirect: nobody has logged in yet, so Quiddly does not send the user to a registered address it cannot vouch for. Your client learns of the failure when the user tries again.
- After login, no code is issued until the user approves on the consent
screen. Cancelling returns
error=access_deniedto yourredirect_uri, as does a login the user finished but Quiddly cannot accept (no active user, or a user managed by an external identity provider). Failures of the login itself are reported the same way, witherror,stateandiss. An expired login session or consent key answers400directly, since the request it belonged to is gone.
Tokens
Section titled “Tokens”- Codes and refresh tokens are single use. A refresh rotates the refresh token and consumes the one presented. Presenting a refresh token that was already rotated out revokes the whole session, so keep exactly one live token and retry a lost refresh response by signing in again.
- A session ends 90 days after the approval however often it rotates; the
final refresh response carries no
refresh_token. Refreshing also re-checks that the user is still active. - Revoking a refresh token revokes the session it belongs to.
- Client,
redirect_uri, PKCE andresourceare validated before a code or refresh token is consumed, so a mismatched request cannot burn a credential the legitimate client is about to use. - MCP tokens carry
token_use: "mcp"and are only valid at/mcp. They are rejected by every other API endpoint, and an ordinary Quiddly API token is rejected at/mcp. Use a service account for REST.
Transport
Section titled “Transport”- Streamable HTTP, stateless: one JSON-RPC message per
POST /mcp, no session id, no server-initiated stream.GETandDELETEanswer405. - Protocol versions
2025-06-18,2025-03-26and2024-11-05are negotiated oninitialize. AnMCP-Protocol-Versionheader outside that list answers400. - Notifications answer
202. Batched requests are rejected. - Every tool result carries
structuredContentnext to its text block, and every tool publishes anoutputSchemaand read-only annotations.
The server’s initialize instructions also name the documentation for the
environment it runs on: this portal’s llms.txt for API
questions and the knowledge base’s llms.txt for how the
Operations Portal and the Client Portal work. An assistant reads the section
file a question is about instead of guessing or paging the data tools, and
reads it for the release that environment is on.
| Area | Tool | Required right |
|---|---|---|
| Invoices | get_invoice |
ReadInvoice |
search_invoices |
SearchInvoices |
|
search_invoice_financial_events |
SearchInvoiceFinancialEvents |
|
search_invoice_ledger_events |
SearchInvoiceLedgerEvents |
|
| Debtors | get_debtor |
ReadDebtor |
search_debtors |
SearchDebtors |
|
search_debtor_financial_events |
SearchDebtorFinancialEvents |
|
| Collection | get_collection_batch |
ReadCollectionBatch |
search_collection_batches |
SearchCollectionBatches |
|
| Clients | get_client |
ReadClient |
search_clients |
SearchClients |
|
search_client_accounts |
SearchClientAccounts |
|
search_client_account_transactions |
SearchClientAccountTransactions |
|
search_client_to_dos |
SearchClientToDo |
|
| Money in | search_client_unplaced_payments |
SearchClientUnplacedPayments |
search_unplaced_payments |
SearchUnplacedPayments |
|
search_payment_transactions |
SearchPaymentTransactions |
|
search_payment_batches |
SearchPaymentBatches |
|
| Money out | search_payouts |
SearchPayouts |
search_payout_batches |
SearchPayoutBatches |
|
| Distribution | search_distributions |
SearchDistribution |
search_distribution_events |
SearchDistributionEvents |
|
| Documents | search_documents |
SearchDocument |
| Lending | get_business_loan |
ReadBusinessLoan |
search_business_loans |
SearchBusinessLoans |
|
search_business_loan_events |
SearchBusinessLoanEvents |
|
search_business_loan_ledger_events |
SearchBusinessLoanLedgerEvents |
|
| Credit | search_debtor_factoring_decisions |
SearchDebtorCreditDecisionStatus |
list_credit_decision_statuses |
ReadCreditDecisionStatus |
|
| Totals | summarize |
the chosen register’s search right |
tools/list returns every tool regardless of the user’s rights. Calling a
tool the user may not use returns a normal result with isError: true and a
message naming the missing right; only protocol problems (unknown tool,
malformed request) produce a JSON-RPC error.
Result conventions
Section titled “Result conventions”- Amounts are exact decimals carried as strings with two decimals. Invoice and collection case amounts are in the currency named on the row; debtor balances are in the tenant’s system currency.
- Dates and timestamps are ISO-8601.
- Search tools take typed filters (equality, lists, text match, date ranges)
and page with
limitandoffset, returning{ <rows>, count, limit, offset, has_more }. A list filter takes at most 100 values and a text filter at most 200 characters, both declared in the tool’s input schema;offsetis at most 10,000, and%or_in a text filter match literally. - Free text written by a payer or a debtor (payment references and messages, names, notes) is returned as data. The server tells the model so in its instructions and in the field descriptions; an agent you build should do the same.
- Read the
structuredContentof a result; the text block is the same data serialized for models that do not support structured output.
Totals, counts and aging
Section titled “Totals, counts and aging”summarize answers “how much”, “how many” and “per client, status or month”
in one call, as a database aggregate, instead of the assistant paging through
a search tool and adding rows up. It takes the register to aggregate, the
same filters as that register’s search tool, the dimensions to group by and a
group limit:
{ "register": "invoices", "filters": { "only_booked": true, "overdue_more_than_days": 30 }, "group_by": ["client"]}- Every group carries
countand the register’s sums. Plain amounts are in the group’s own currency, which is always a grouping key, so they never mix;*_localeamounts are in your tenant’s system currency and can be added across groups. age_bucketgroups by days past due (not_overdue,1_15,16_30,31_60,61_plus) andmonthby calendar month, where the register has a date for it.overdue_more_than_daysandoverdue_at_most_daysfilter on the same calendar.only_bookedon the invoice tools is the receivables ledger as the Client Portal shows it: activated, paused, disputed, stopped and closed invoices, drafts and pending ones left out. The invoices summary requires a narrowing filter (only_booked,only_unpaid, an overdue window, a client or debtor, or statuses); an unfiltered call is rejected. For the open ledger passonly_bookedtogether withonly_unpaidor an overdue window. Debtor balances fromsummarizearebalance_total_locale/balance_capital_locale(system currency).- Registers with sums: invoices, collection cases, debtors, client accounts and their transactions, unplaced payments, payments and payment batches, payouts, business loans and their events, invoice and debtor financial events. Distributions, documents, to-dos, ledger steps, payout batches and the credit decision board are counts only. The tool’s description lists each register’s dimensions and sums.
- At most 200 groups are returned, largest first;
has_moresays the list was cut. Narrow the filters or group by fewer dimensions rather than paging. - Scoping, rights and the client-user rules are the search tool’s: a summary never shows more than the matching search would.
Scoping and withheld fields
Section titled “Scoping and withheld fields”- Instance-wide registers.
search_payment_transactions,search_payment_batchesandsearch_payout_batchescover data that has no client until it is placed, so they answer403for anyone who is not a system user. - Business loan events require a
business_loan_id; a loan the user cannot see reads as not found. - Debtor financial events are closed to ERP and client handler users, as the REST endpoint is.
- Payer identity on payments follows the REST rule: a caller holding
SHOW_RESTRICTED_SENDER_INFORMATION_FOR_CLIENTSgets the payer’s identification and account number asnull. The payer’s name is never restricted. - Documents are metadata only. No bytes, no storage key and no storage
name;
has_stored_filesays whether a file exists, and the row carries the document id and its human-readable name. Open it in the Operations Portal or through the authenticated REST API. - Interest invoices are hidden from client users, as they are in the REST invoice searches.
get_collection_batchanswers403to ERP and client handler users, as the REST get does.search_client_unplaced_paymentsreturns the client-facing field set: nodebtor_id,source_invoice_idormatching_status. The operator toolsearch_unplaced_paymentshas them.- Payout destinations show the receiving bank and channel, never the account number or IBAN.
- Not exposed: credit monitoring, payout files, debtor payout accounts, document templates, the payment matching engine, payment method configuration and reconciliation.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause |
|---|---|
404 on /mcp or any /oauth/mcp/* route |
MCP is not enabled on the tenant: the Developer module or the MCP setting is off. Switching either off takes effect within a minute. |
401 on /mcp with a token that works on REST |
REST tokens are not accepted at /mcp. Sign in through the client’s OAuth flow. |
401 on a REST endpoint with an MCP token |
MCP tokens only work at /mcp. Use a service account for REST. |
401 after about an hour |
The access token expired and the client did not refresh. Reconnect from the client. |
invalid_grant saying the refresh token was already used |
The client presented a rotated-out token, or someone else did. The session is revoked; sign in again. |
access_denied after a successful login |
No active Quiddly user matches the login, or the user is managed by an external identity provider. |
| An error page instead of a redirect when starting the login | The authorization request failed validation before login: PKCE, redirect_uri, response_type, resource or state. |
Tool result with isError: true naming a right |
The user’s role lacks that right. Ask your tenant administrator. |
403 from a payment or payout batch tool |
Those registers are instance-wide and need a system user. |
invalid_client_metadata on registration |
Confidential client, a non-https and non-loopback redirect_uri, or more than 10 URIs. |
| Consent screen names a client or address you do not recognise | Someone else started the login. Click Cancel. |