MCP
One endpoint, two tools
A single Streamable HTTP endpoint speaking JSON-RPC 2.0 — the same shape Claude Desktop, Claude Code, claude.ai, and ChatGPT all expect from an MCP server.
Not publicly reachable yet — the API goes live with the hosted release. Status
Connecting
POST https://api.terndays.app/mcp
Authorization: Bearer <td_live_… API key, or td_oauth_at_… access token>
Content-Type: application/jsonEither credential from the authentication page works here, checked against the same counters.read scope. For Claude Desktop or Claude Code, this is a plain header-based remote server entry in your MCP config; for claude.ai or ChatGPT, add Terndays through the connector picker and the OAuth flow handles the header for you.
GET /mcp returns 405 — this server never opens a server-push stream, since v1's two tools are synchronous reads with nothing to push. A lightweight GET /mcp/ping route exists to confirm a credential authenticates, without exercising a real tool.
Protocol
Protocol version 2025-06-18, sent as an MCP-Protocol-Version header (omitting it on your very first request is fine — initialize hasn't run yet). Batch requests (a top-level JSON array) aren't supported, matching the 2025-06-18 revision of the spec.
| Method | Notes |
|---|---|
| initialize | Negotiates the protocol version, returns server info |
| notifications/initialized | A notification — no response sent back |
| ping | Empty result, for a liveness check |
| tools/list | Returns the two tools below, with their input schemas |
| tools/call | Calls one tool by name |
Sessions are optional: initialize offers an Mcp-Session-Id header, but nothing requires it back — every request authenticates on its own.
Tools
Both tools are scoped to counters.read — reads only. There's no ledger.read-shaped tool here, not even a stub: the raw day ledger is never served through this endpoint, for either custody tier, by design.
| Tool | Arguments | Returns |
|---|---|---|
| list_counters | none | Every counter on the account |
| get_counter | counterId (string) | One counter by id |
Example call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "list_counters" }
}Response shape today
Every account's agent view is still being wired up on our end — until it is, both tools answer honestly rather than guessing:
{
"status": "not_provisioned",
"reason": "Agent access to counters is not connected on this account yet. Terndays has not built the agent-view sync that shares a device's computed counter states with a hosted agent session, so there is nothing to read here today — this is an honest gap, not a fabricated result."
}Never a fabricated number, never a placeholder "23 Schengen days" — an agent reading this should tell you plainly that counters aren't connected yet, not invent one.
Response shape once connected
{
"status": "ok",
"counters": [
{
"id": "schengen-90-180",
"label": "Schengen 90/180",
"thresholdDays": 90,
"usedDays": 42,
"remainingDays": 48,
"asOf": "2026-07-06T00:00:00.000Z"
}
],
"asOf": "2026-07-06T00:00:00.000Z"
}Every number here is the on-device rule engine's own computed result, carried through verbatim — never re-derived or estimated on our servers. The asOf timestamp is always present so an agent can say "as of Jul 6" rather than presenting a stale number as live. get_counter with an unknown id answers {"status": "not_found", "counterId": "…"}.
Rate limits
Limited per credential — an API key and an OAuth token get separate buckets even from the same account, so one connection running hot doesn't stall another. The default is 60 requests a minute; going over it returns 429 with a Retry-After header, not a silent drop.