BETA
Skip to content

MCP tools ​

Krafter exposes 64 tools over the Model Context Protocol (MCP) — 50 reads, 12 writes and 2 session tools (teams_list, teams_select) across nine services — letting AI assistants query and manage data using the same API keys you already issue for REST.

Endpoint: https://mcp.krafter.dev/mcp

GET /mcp opens the Server-Sent Events stream. POST /mcp carries JSON-RPC messages.

Transport ​

Krafter implements JSON-RPC 2.0 over Streamable HTTP through phantom_mcp, which negotiates the protocol version with the client up to 2025-11-25. Sessions are real: initialize returns an Mcp-Session-Id header, and responses arrive as SSE frames on the request's own stream.

The server identifies itself as { "name": "krafter", "version": "1.0.0" } in the initialize response, which also carries instructions describing the team binding, the scope rules and the irreversible operations.

Supported methods ​

MethodDescription
initializeNegotiates the protocol version and returns server capabilities and instructions.
notifications/initializedClient acknowledgement after initialize. Server replies 202 Accepted.
tools/listReturns the tools the calling API key is authorised for, with annotations.
tools/callInvokes a tool. The result is a JSON-encoded text content block, plus structuredContent where the tool returns an object.
pingLiveness check.

Any other method returns JSON-RPC error code -32601 ("Method not found"). Malformed messages return -32600 ("Invalid request").

Errors are tool results, not protocol errors ​

A tool that fails — a bad argument, a missing record, an insufficient scope, an unexpected exception — returns a normal tools/call result with isError: true and a message written for the model to correct itself against. JSON-RPC error codes are reserved for what is wrong with the message. This matters because most clients render a JSON-RPC error as a transport failure: the explanation never reaches the model, and the agent retries the same wrong call.

Annotations ​

Every tool carries MCP annotations, so a client can tell a listing from a deletion without parsing its description:

HintMeaning
readOnlyHintThe tool does not modify anything. True for all 37 read tools.
destructiveHintThe change does not come back. True only for mail_delete_domain and mail_delete_suppression.
idempotentHintCalling it again with the same arguments changes nothing further.

Output schemas ​

Every tool declares an outputSchema alongside its inputSchema, so a client can validate what comes back instead of parsing and hoping. The schema describes the envelope exactly — which key carries the list, that count and total are integers, that next_cursor is a string when present — and leaves item fields open. That restraint is deliberate: a schema that over-claims is worse than a narrow one, because a client acts on it.

next_cursor is never required. Its absence is how a caller knows a listing is finished.

Pagination ​

List tools are cursor-paginated. A response carries next_cursor when there is another page; pass it back as cursor. When there is no next page the key is absent, not null.

Cursors are opaque and keyset-based: they carry the position of the last row, not an offset, so rows arriving mid-listing do not shift the pages under you. A cursor is bound to the tool and the filters that produced it — handing one to a different tool, or reusing it after changing a filter, is refused rather than silently answered with the wrong rows.

Tool errors ​

JSON-RPC codeMeaning
-32001Tool not found
-32002Permission denied (API key lacks the required scope)
-32602Invalid or missing parameters
-32603Internal error (uncaught exception during tool dispatch)

Authentication ​

Two ways in. Both end at the same place — a credential with a team and a set of scopes — so every tool, limit and audit row below behaves identically whichever you use.

OAuth 2.1 — for MCP clients ​

Point your client at https://mcp.krafter.dev/mcp and let it do the rest. There is nothing to paste: the server answers an unauthenticated request with an RFC 9728 discovery pointer, the client registers itself (RFC 7591), sends you to a consent screen, and stores its own token.

WWW-Authenticate: Bearer error="invalid_token",
  resource_metadata="https://mcp.krafter.dev/.well-known/oauth-protected-resource/mcp"

On the consent screen you tick which teams the connection may act on and which scopes it gets. A connection covering one team acts on it straight away; one covering several must select the active team in-session with teams_select before any other tool works, and every action is recorded against the team it acted on. Revoking from one team's screen removes that team and leaves the connection working for the others.

What the server supports:

Issuerhttps://mcp.krafter.dev
Grantsauthorization_code, refresh_token — no implicit, no ROPC, no client credentials
PKCErequired, S256
Client authenticationnone — public clients only
Access token lifetime24 hours, with rotating refresh
Resource (RFC 8707)https://mcp.krafter.dev/mcp

Discovery documents: /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource/mcp.

Connections are listed and revoked in the dashboard under API Keys → Connected applications, beside the team's API keys. Revoking takes effect on the client's next request, not at token expiry.

API key — for your own code ​

Send a key in the Authorization header, the same Bearer format the REST API uses:

Authorization: Bearer kr_live_...

An API key is the right choice for a script or a service you run yourself, where there is no browser to approve a consent screen. Unlike an OAuth connection, a key can also be narrowed to named resources — one mail domain, one log project — and it opens the REST API and the SMTP relay as well, so treat it as the broader credential of the two.

Scopes ​

Both credential types use the same scope vocabulary, and the same table decides which tools a scope reaches. tools/list returns only the tools your scopes satisfy, and tools/call re-checks before dispatching — an absent tool and an unscoped tool look identical from the client.

ScopeReaches
analytics:readAnalytics reads
audit:readAI Audit reads
audit:writeAudit findings, tasks, and starting scans
cron:readCron reads
domains:manageCreating, verifying and deleting mail domains
flags:readFlags reads
forms:readForms reads
logs:readLogs reads
mail:manageSuppression list writes
mail:readMail reads
push:readPush reads
surveys:readSurveys reads
surveys:writeEditing surveys and questions
webhooks:readWebhooks reads
webhooks:writeReplaying webhook events

The wildcard * grants every tool. It is an API key scope only — an OAuth connection can never hold it, because that credential is granted by clicking a button rather than by deliberately minting a key.

A write scope does not imply read. Pair surveys:write with surveys:read to both list and edit. Krafter issues more scopes than the fifteen above, but the rest are REST-only and reach no MCP tool.

Origin validation ​

POST /mcp validates the Origin header against :krafter, :mcp_allowed_origins (default ["http://localhost:*"]). Mismatched origins return 403 Forbidden. Configure the list in runtime.exs to allow your assistant's host.

Tool inventory ​

Teams — any scope (2 tools) ​

Which team the connection acts on. Every other tool acts on the active team and takes no team argument; a connection granted for several teams selects one here first. Callable with any scopes — they expose nothing beyond the connection's own teams.

ToolDescription
teams_listThe teams this connection may act on, and which is active.
teams_selectMake one of them active (write — changes what every later call acts on). Refuses a team the connection was not granted for.

Analytics — analytics:read (7 tools) ​

ToolDescription
analytics_list_sitesAll analytics sites for your team, with each site's event retention.
analytics_get_statsPageviews, visitors, visits over a period with optional dimension breakdown.
analytics_realtimeActive visitors in the last 30 minutes.
analytics_top_pagesMost visited pages for a period.
analytics_top_referrersTop traffic sources for a period.
analytics_eventsCustom event counts and revenue.
analytics_goal_conversionsConversion stats for a specific goal.

AI Audit — audit:read, plus audit:write for the writes (9 tools) ​

A finding carries recommended_fix as a structured map, a severity and a numeric priority_score — output built to be read by software, which until recently nothing could reach.

org_id is not an argument: it is the team that owns the API key, so the tools derive it.

ToolDescription
audit_command_centerOverview of one project: open findings by severity, recent scans, trend. The cheapest call that says whether anything needs attention.
audit_list_findingsFindings, highest priority first. Filter by severity, status or domain.
audit_get_findingOne finding in full, with its evidence items and complete recommended_fix.
audit_update_findingChange status, severity or owner — accept, dismiss or reprioritise. Requires audit:write.
audit_list_tasksRemediation tasks opened against this project's findings.
audit_create_taskOpen a remediation task against a finding. Requires audit:write.
audit_update_taskChange a task — status, priority, owner, due date, PR link. Requires audit:write.
audit_report_summaryReport-level totals, severity split and posture over time.
audit_run_scanQueue a new scan. Consumes AI quota and counts against the billed scan limit. Non-idempotent — do not retry blindly. Requires audit:write.

Cron — cron:read (3 tools) ​

ToolDescription
cron_list_jobsAll cron jobs for your team with schedules and last-run info.
cron_get_jobDetails for a single cron job by ID.
cron_list_executionsRecent executions for a specific job.

Flags — flags:read (6 tools) ​

ToolDescription
flags_list_projectsAll feature-flag projects for your team.
flags_list_flagsFlags in a project, optionally filtered by status and tags.
flags_get_flagA single flag by ID, including its evaluation rules.
flags_evaluateWhat a flag would return for a given user or context. A dry run: it resolves exactly as the SDK would, but records no evaluation, enrols nobody in an experiment and is not billed.
flags_list_audit_logAudit log entries for your team's flags.
flags_list_experimentsA/B test experiments for a specific flag.

Forms — forms:read (3 tools) ​

ToolDescription
forms_list_formsAll forms for your team with submission and unread counts.
forms_get_formDetails for a single form by ID.
forms_list_submissionsSubmissions for a form, with spam-status filter and full-text search.

Logs — logs:read (5 tools) ​

ToolDescription
logs_list_projectsAll log projects with retention and daily-limit settings.
logs_searchSearch entries by level, stream, full-text query, and time range (max 1000).
logs_list_streamsAll streams within a project.
logs_list_alertsAlerts configured for a project.
logs_get_usageToday's ingestion usage (entry count + bytes).

Mail — mail:read, plus domains:manage / mail:manage for the writes (10 tools) ​

ToolDescription
mail_list_emailsEmails sent by your team, with status filter and search.
mail_get_emailA single email record with its delivery events.
mail_email_statsTotals by status, with optional daily breakdown.
mail_list_domainsSending domains and their verification status.
mail_list_suppressionsSuppressed addresses (bounces, complaints, manual).
mail_create_domainRegister a sending domain and return the DNS records to publish — with the note that on Cloudflare every CNAME and MX must be DNS-only (not proxied).
mail_verify_domainStart a background verification check; the result lands on the domain's status, read it back with mail_list_domains.
mail_delete_domainDelete a sending domain. Irreversible; requires confirmation.
mail_create_suppressionSuppress an address so no further mail is sent to it.
mail_delete_suppressionRelease a suppressed address. Irreversible; requires confirmation.

Push — push:read (5 tools) ​

ToolDescription
push_list_appsPush apps with subscriber counts.
push_list_subscribersSubscribers for an app, filtered by platform / status / tags.
push_list_notificationsNotifications for an app, optionally filtered by status.
push_get_notificationA single notification with targeting and delivery stats.
push_daily_statsDaily counters for an app (sent, delivered, clicked, failed, new_subscribers, unsubscribes). The delivered counter is currently always 0 — see Analytics → Response Fields for why.

Surveys — surveys:read, plus surveys:write for the writes (8 tools) ​

ToolDescription
surveys_listSurveys for your team, newest first. Cursor-paginated.
surveys_getOne survey with its sections and questions — the structure a breakdown is keyed by.
surveys_statsResponse counts and completion rate.
surveys_question_breakdownWhat people answered, aggregated per question. This is what turns a survey into findings.
surveys_completion_funnelPer-question reach, so the question that loses people is visible rather than inferred.
surveys_list_responsesIndividual responses, newest first. Cursor-paginated.
surveys_updateChange name, status or the public-results flag. active opens a survey for responses. Requires surveys:write.
surveys_update_questionEdit one question's text, help text or options. Requires surveys:write.

Webhooks — webhooks:read (6 tools) ​

These cover the webhook relay service — webhooks Krafter receives and forwards. The outgoing webhooks that mail, forms, flags, push, signal, surveys and waitlists each send are configured per service and are not these; an empty source list does not mean your team has no webhooks.

ToolDescription
webhooks_list_sourcesWebhook sources for your team.
webhooks_get_sourceDetails for a source including destination count.
webhooks_list_eventsReceived events for a source, with event_type filter.
webhooks_get_eventOne received event in full, with its delivery attempts — status, response code and error per destination. This is what answers "why did my webhook not arrive".
webhooks_replay_eventRe-deliver a received event. Sends real traffic; name destination_ids to avoid duplicating a delivery that already succeeded. Requires webhooks:write.
webhooks_list_destinationsForwarding destinations for a source.

Write tools ​

Twelve tools write. They call the same context functions the REST controllers call, so an MCP mutation gets the same validation, DNS record generation and side effects as one made through /api/v1 — there is no second path that can skip them. They use the same scopes as REST:

ToolScopeNotes
mail_create_domaindomains:manageReturns the DNS records that must be published.
mail_verify_domaindomains:manageSets status to verifying and enqueues the check. Safe to repeat.
mail_delete_domaindomains:manageIrreversible. Confirmation required.
mail_create_suppressionmail:manage
mail_delete_suppressionmail:manageIrreversible. Confirmation required.
audit_update_findingaudit:writeOnly the fields you pass change.
audit_create_taskaudit:write
audit_update_taskaudit:write
audit_run_scanaudit:writeCosts money. Spends AI quota and a billed scan; non-idempotent.
surveys_updatesurveys:writestatus: "active" makes a survey reachable by respondents.
surveys_update_questionsurveys:write
webhooks_replay_eventwebhooks:writeSends real HTTP. Non-idempotent — a retry delivers twice.

Deleting a survey and designing one from nothing are deliberately absent: a delete takes its responses with it and a response cannot be recreated, and a half-built survey shared with respondents is worse than none.

Confirmation for irreversible operations ​

The two destructive tools cannot act in a single call. The first call performs no work: it measures what would be lost, returns those numbers, and hands back a short-lived signed confirmation string. Only a second call carrying that string in confirm executes.

The confirmation is bound to the API key, the tool, the subject and the measured impact, and expires after five minutes. If what the operation would affect has changed since the preview, the confirmation is refused and a fresh preview is required — so a person who agreed to one set of numbers cannot have a different one executed.

This is a brake on a single hallucinated call, not a human in the loop: the same agent may spend the confirmation it just received. It lives on the server precisely because a gate that exists only in one client's settings protects only that client.

Audit trail ​

Every tool call leaves a row in the team's audit log — reads included, which is the half that is usually missing. The row carries the tool, the credential it came through and which kind that was (api_key or oauth), the outcome (ok, gated, denied, invalid_params, exception) and the duration.

Arguments are hashed, never stored. A SHA-256 fingerprint of the sorted arguments is recorded instead of their values, so the trail does not become a second copy of the data it watches over — while still proving two calls were identical and making a loop that repeats one call a thousand times visible as a thousand rows sharing one hash.

A confirmation preview is recorded as gated rather than ok: a destructive call the server stopped is the most interesting event in the trail, and it must not read as an ordinary success.

Hosts ​

https://mcp.krafter.dev/mcp is the endpoint. https://app.krafter.dev/mcp serves the same server for API-key callers and keeps working, so existing configurations do not break — but the dedicated host is the one to use, and the one this page documents.

OAuth works only on mcp.krafter.dev. The authorisation server and its discovery documents answer on that host alone; the app host returns 404 for them. A client pointed at app.krafter.dev will not find an authorisation server to register with.

Rate limiting ​

MCP is throttled per credential, in its own bucket, separate from the REST API's per-team one. Each credential — an API key or an OAuth connection — gets 300 requests per minute at /mcp, and the two kinds never share a counter.

That separation is the point rather than a detail: sharing the team's single REST budget meant an agent that mis-paginated and looped could spend it and take the team's transactional mail down with it. One team holds several credentials — the agent's and production's — and they must not be able to exhaust each other.

Coverage gaps ​

Writes outside the twelve above are still REST-only: sending mail, toggling flags, scheduling notifications, creating cron jobs.

Long-running work — audit_run_scan, webhooks_replay_event, mail_verify_domain — returns as soon as the work is queued rather than when it finishes. The MCP specification has an extension for this (a durable task handle the client polls), which the transport library does not yet support. Until it does, read the outcome back: audit_command_center for a scan, webhooks_get_event for a replay, mail_list_domains for a verification.

Built by Krafter Studio