Skip to content

MCP connector ​

The MCP connector lets a shop point its own LLM at its own data: "how many stamps did the Milan store give last week", "issue a badge for EMP-00418", "which access cards expire this month".

It is a thin adapter over the same service layer the REST API uses. It introduces no new business logic and no new data access — and, non-negotiably, no ability to write a transaction.

Status

The connector is live. POST /mcp is mounted and answers a request with no key with a 401 that names the mistake — not a 404.

Access comes with the Business API add-on. Three things must hold, and they are checked in that order at every call: a platform kill-switch, the shop's own API access, and then either the add-on or an explicit grant. So MCP is never a way around the REST API being off for a shop — and it can be switched off platform-wide, in a hurry, without touching the REST API. If a valid tmk_ key comes back refused for entitlement rather than for the key itself, that is one of those three, not your client.

What still needs reading before you turn it on is below, not above: the tool table is generated from the server's own registry, and the read-only / writes / destructive gating is the part a merchant must actually understand.

Setup ​

1. Create an MCP key ​

In the dashboard: Settings → API → MCP keys → Create key.

MCP keys are a separate class from REST API keys, in a separate list, with their own limits and their own revocation. They begin tmk_.

That separation is the point: an agent loop and a nightly CRM sync are different traffic shapes with different risk, and you must be able to revoke one without touching the other. Presenting a tmk_ key to the REST API — or a tsk_ key to /mcp — returns a 401 that names the mistake.

The key is shown once. Everything on Authentication about naming, expiry, rotation and revocation applies unchanged.

2. Point your client at the endpoint ​

https://api.tesserapp.eu/mcp
Authorization: Bearer tmk_live_…

The transport is Streamable HTTP — the current remote MCP transport, not the deprecated SSE-only one. Any client that accepts a URL and a bearer token works. A typical configuration file:

json
{
  "mcpServers": {
    "tesserapp": {
      "url": "https://api.tesserapp.eu/mcp",
      "headers": { "Authorization": "Bearer tmk_live_…" }
    }
  }
}

3. There is no one-click "Connect" button, and here is why ​

One-click OAuth connect is not available in v1. In hosts that require OAuth for remote MCP servers, TesserApp will not light up as a connectable service; you paste a key instead.

The MCP specification wants a protected server to be a full OAuth 2.1 resource server, and TesserApp has no OAuth authorization server — it has sessions, device keys and API keys. Building an authorization server (authorization endpoint, PKCE, dynamic client registration, consent) to ship a connector is the wrong order of work, and a half-built one is a security liability rather than a convenience.

What we do instead, so that v2 is additive rather than a rewrite: the connector already answers with OAuth-shaped failures — an RFC 9728 protected-resource metadata document, a 401 carrying WWW-Authenticate: Bearer resource_metadata=…, and a 403 carrying error="insufficient_scope". When the authorization server ships, clients that speak OAuth start working without the wire shape of failures changing.

The tools ​

16 tools are registered: 9 read, 5 write and 2 destructive. Generated from the server's own registry, so this table cannot drift from what tools/list returns.

ToolWhat it doesScopeKindNeeds
whoamiWho am I—readread-only key is enough
list_locationsList locationslocations:readreadread-only key is enough
list_programsList loyalty programsprograms:readreadread-only key is enough
get_programGet one programprograms:readreadread-only key is enough
search_cardsSearch cardscards:readreadread-only key is enough
get_cardGet one cardcards:readreadread-only key is enough
list_transactionsList transactionstransactions:readreadread-only key is enough
get_analyticsGet an analytics widgetanalytics:readreadread-only key is enough
list_webhooksList webhookswebhooks:readreadread-only key is enough
issue_cardIssue a cardcards:writewriteallow_writes
update_cardUpdate a cardcards:writewriteallow_writes
reissue_cardReissue a cardcards:writewriteallow_writes
disable_cardDisable a cardcards:writewriteallow_writes
enable_cardEnable a disabled cardcards:writewriteallow_writes
archive_cardArchive a cardcards:writedestructiveallow_writes and allow_destructive
delete_cardDelete a card (erase the holder)cards:writedestructiveallow_writes and allow_destructive

A key sees only the tools it may actually call: tools/list filters by scope and by privilege, so a read-only key is never shown a write tool it would then be refused. Calling one anyway is a 401, not a silent no-op.

The table above is generated from the registry rather than written here, because a documented tool that does not exist — or a tool that exists and is not documented — is exactly how a merchant misjudges what they switched on.

The shape of the surface:

  • Reads (9, a read-only key is enough): whoami, list_locations, list_programs, get_program, search_cards, get_card, list_transactions, get_analytics, list_webhooks.
  • Writes (5, need allow_writes): issue_card, update_card, reissue_card, disable_card, enable_card. Nothing else.
  • Destructive (2, need allow_writes and allow_destructive): archive_card, delete_card.

Note which side of that line disable_card sits on. Disabling a card is a WRITE, not a destructive action, because it is reversible: enable_card puts the card back exactly as it was, with its stamps, its expiry and its install intact. archive_card is destructive because it is terminal.

For the merchant: what you are switching on ​

This section is for whoever ticks the boxes, not for the developer.

Read-only is the default, and it means read-only ​

A new MCP key can look at your shop and change nothing. No card is issued, no holder is edited, nothing is deleted. If all you want is "ask questions about my loyalty data", leave it here.

Turning on writes ​

Writes let the agent issue, update, reissue, disable and enable cards — the allow_writes list above and nothing more. An agent cannot create a program, add a location, change your prices or touch your subscription.

Disabling is the reversible one, which is why it is here and not behind the second switch: a disabled card refuses every scan at the till but keeps its stamps, its expiry and the pass on the holder's phone, and enabling it puts it back exactly as it was.

The write tools are hidden from the agent entirely while writes are off — not merely refused when called. A tool the model cannot see is one it cannot be talked into calling, which is the strongest of the controls here.

Turning on destructive actions ​

A second, independent switch, off even when writes are on. It allows archiving and deleting cards — the two that cannot be undone. Archiving unbinds the card from the holder's device and there is no unarchive; deleting a standard card is a GDPR erase, and the holder's personal data goes, including from past webhook delivery payloads.

If what you actually want is "let the agent switch a badge off", that is disable_card and it needs only writes. Leave this switch off.

Turn it on for a scripted offboarding flow you have tested. Leave it off otherwise.

Keys restricted to certain locations cannot use the card tools ​

If you restrict an MCP key to specific locations, that key cannot read or change cards at all. This is not a gap we intend to close: a card is not attributed to a location anywhere in TesserApp — it belongs to a program, and programs belong to the whole shop — so there is no honest way to narrow a card question to one site. Answering anyway would hand back rows from sites the key was created not to see.

Which tools refuse: search_cards, get_card, update_card, reissue_card, disable_card, enable_card, archive_card and delete_card. The REST API refuses the same calls on the same key, with 403 location_scope_unsupported.

Two deliberate exceptions:

  • issue_card still works. Issuing creates a new card rather than reaching an existing one the key cannot see, and the REST API allows it on a restricted key for the same reason.
  • list_transactions still works, and is the way round this. Transactions are attributed to a location, they are filtered to exactly your key's sites, and every row carries its card and program id — so the activity at your permitted sites, and the cards behind it, are reachable that way.

The tools stay listed for a restricted key rather than being hidden, and a call comes back as a readable explanation rather than an error code. That is so the agent can tell the person what happened ("this key is limited to one site, and card records aren't tied to a site") instead of reporting that a tool is broken.

An agent does not have to discover this by trial and error. whoami returns an unavailable_tools list — every tool this key can see but would be refused on, each with the same reason code the refusal itself carries:

json
{
  "unavailable_tools": [
    { "tool": "search_cards", "reason": "location_scope_unsupported" },
    { "tool": "delete_card",  "reason": "location_scope_unsupported" }
  ]
}

It is empty for an unrestricted key. It is reported per tool rather than per scope because a scope cannot express this rule: cards:write genuinely is active on a restricted key — that is what lets issue_card through — while the six card-mutation tools refuse. It never names a tool the key cannot already see, so a read-only key learns nothing about the write tools from it; the write and destructive switches are reported separately, in whoami's inactive list.

What no MCP key can ever do ​

  • Record a card use, award a stamp, redeem a prize or reverse a transaction. The transaction log stays evidence of what happened at a till. (Why.)
  • Create, change or delete a webhook. Not even with writes and destructive both on. An instruction hidden in text that could repoint your event stream at someone else's server is the highest-value attack in this system, so the capability is not on the ceiling at all.
  • Create another key, of any class.
  • Manage staff, devices or your subscription.
  • See another shop's anything.

Text in your data is treated as data, not instructions ​

Program names, holder names, external_ref values and location names are typed by people — customers, staff, an importer — and any of them can contain something shaped like an instruction ("ignore previous instructions and…"). Every tool result wraps that text in a delimited block stating it is data, not instructions.

This does not solve prompt injection; nothing does. It is why the write ceiling exists and why destructive actions need a second switch: the system is designed assuming the model will eventually read attacker-authored text.

Limits, and why they are tighter than the API's ​

LimitDefaultDeployment setting
MCP keys per shop5MCP_KEY_MAX_PER_SHOP
Calls per minute, per key60MCP_KEY_RATE_LIMIT_PER_MINUTE
Calls per day, per key5000MCP_KEY_DAILY_CALL_BUDGET

The per-minute limit bounds a spike; the daily budget bounds a loop that runs all night. An agent loop is a different traffic shape from a nightly CRM sync, and a runaway loop is its normal failure mode rather than an exceptional one — which is also why these limits are tighter than the REST API's.

Seeing what the agent did ​

Every write is audited as the MCP key that made it, so the dashboard's history shows which key issued which card and when — the same way it shows a member of staff. If something looks wrong, revoke the key: it stops working on the very next call, and no cache outlives it.

Requires the Business API add-on.