Kontacts docs

MCP for agents

A hosted MCP server over the same API key — six tools, one project-shaped context per call.

POST /api/v1/mcp — Streamable HTTP MCP, protocol version 2025-03-26. This is not IMAP and not a mail client: an agent gets the project mailbox, domains, and bookings as tools, scoped by the same API key as the REST API.

Auth

Authorization: Bearer gm_live_ — the exact same key as send and list emails. There is no separate MCP credential and no session-cookie path; see Authentication for the two key formats.

Every tool takes an undeclared project_id

None of the six tools' advertised inputSchemas mention project_id — you won't see it from a tools/list call. But every handler reads args.project_id, and with an org/scoped key, five of the six tools hard-fail with project_id is required for this key if you don't pass it. A project key doesn't need it (its own project is implied), which is probably why the schema omits it — but that leaves an org-key caller with no way to discover the argument except this page. Pass project_id explicitly whenever you're calling with an org/scoped key.

Tools

Read tools first, then write tools — calling a write tool spends the organisation's send quota, same as send:

  • list_emails
  • get_email
  • list_domains
  • list_bookings
  • send_email (spends send quota)
  • reply_email (spends send quota)

list_emails

Lists mail in scope, newest first (same defaults as the dashboard: live, non-archived, both directions). Arguments: direction (inbound | outbound, optional), archived (boolean, optional), limit (optional, capped at 50), plus the undeclared project_id above.

Asymmetric with the other five tools

This is the one read tool that does not hard-fail an org key with no project_id: if you omit it, an org/scoped key gets mail across every project the key can see, resolved the same way GET /api/v1/projects resolves scope. get_email, list_domains, list_bookings, send_email, and reply_email all require you to name one project — this tool is the only one that can span several in a single call.

get_email

Reads one message by id, including text and html body — the single-message read that has no REST equivalent (see List emails). An id from outside the key's project is reported as not found, not as a permission error.

list_domains

Lists domains attached to a project and their verification status. No arguments beyond project_id.

list_bookings

Lists bookings on a project, most recent start first. limit optional, capped at 100. Never includes manage-token hashes.

send_email

Required: to, subject, text; optional html. Re-enters POST /api/v1/send in-process — same validation, same quota, same response shape, same "no from field" rule. There is no separate MCP send path to drift from the REST one.

reply_email

Required: id (an inbound email to reply to), text; optional html and confirm. Two-phase by design: call it once without confirm and you get a preview (from, to, subject, the text you sent, and a hint) with nothing sent. Call it again with confirm: true to actually send. This is the only tool here that requires a second call to take effect — useful for an agent that should show a human what it's about to send before it goes.

Rate limit

Per key, per server instance — not a global number

The server enforces roughly 60 requests per minute per key (MCP_RATE_LIMIT_WINDOW_MS / MCP_RATE_LIMIT_MAX in apps/web/lib/mcp.ts), but the counter is an in-memory Map local to one serverless instance, not a shared store. On a platform that can route your calls to more than one instance, the effective ceiling for a single key can be higher than 60/min in practice. Treat it as "at least 60/min per key," not as an exact global cap — and don't build retry logic that assumes the 429 arrives at a precise, repeatable request count.

Protocol surface

JSON-RPC 2.0 over a single HTTP endpoint. Batches (a JSON array of requests) are supported. A request with no id is a notification: it's processed but never answered, and a body that is only notifications gets 202 with no JSON body at all.

MethodBehaviour
initializeReturns protocolVersion, capabilities, and server info.
pingEmpty result.
tools/listReturns the six tool definitions above.
resources/listAlways { resources: [] } — no resources are exposed.
prompts/listAlways { prompts: [] } — no prompts are exposed.
tools/callDispatches to one of the six tools by name.

Other HTTP behaviour: OPTIONS returns 204 (CORS is wide open, Access-Control-Allow-Origin: *); GET returns 405 with an Allow header pointing at POST; invalid JSON is 400.

JSON-RPC error codes: -32600 invalid request, -32601 method or tool not found, -32602 invalid params (for example, tools/call with no name).

Tool failures are not HTTP errors

A tool that fails — bad arguments, "not found", a downstream error — returns a successful JSON-RPC result with isError: true and the problem in content/structuredContent, never a non-2xx HTTP status or a JSON-RPC error object. Check isError in the result, not the HTTP status code, to know whether a tools/call actually worked.

Connecting a client

Cursor, Claude Code, and other MCP clients that take a remote HTTP server URL:

{
  "mcpServers": {
    "kontacts": {
      "url": "https://kontacts.dev/api/v1/mcp",
      "headers": {
        "Authorization": "Bearer gm_live_…"
      }
    }
  }
}

Claude Code CLI:

claude mcp add --transport http kontacts https://kontacts.dev/api/v1/mcp \
  --header "Authorization: Bearer gm_live_…"

Put the key in the header your client sends, never in a prompt — it's a live credential with the same reach as any other API key.

Keys are minted under Settings → API keys (/dashboard/settings/api-keys), same page as REST keys.

On this page