Terndays
← Back to Terndays

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/json

Either 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.

MethodNotes
initializeNegotiates the protocol version, returns server info
notifications/initializedA notification — no response sent back
pingEmpty result, for a liveness check
tools/listReturns the two tools below, with their input schemas
tools/callCalls 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.

ToolArgumentsReturns
list_countersnoneEvery counter on the account
get_countercounterId (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.