MCP server
AvailableThe Emithook MCP server (@emithook/mcp) exposes the management API as Model Context Protocol tools, so an AI assistant (Claude, Cursor, an internal agent) can query and operate Emithook in natural language — "show failed deliveries to ep_razorpay in the last hour", "redrive the DLQ for that endpoint", "create an endpoint with the Shopify preset".
Every tool is a thin wrapper over the same scoped key (via @emithook/sdk) — there is no capability here the API doesn't have. If you are an agent, read the Glossary before calling tools: the arguments below use those exact entity names.
Connect
Two doors, one registry — see Connecting an agent for the hosted OAuth flow (claude.ai's "Connect" button) and for choosing a scope. The local stdio setup is below.
The server runs locally over stdio and talks to the API with your key:
// add to your MCP client config (e.g. Claude Desktop)
{
"mcpServers": {
"emithook": {
"command": "npx",
"args": ["-y", "@emithook/mcp"],
"env": {
"EMITHOOK_API_KEY": "ek_live_…",
"EMITHOOK_SCOPE": "read"
}
}
}
}| Env var | Purpose |
|---|---|
EMITHOOK_API_KEY | Required. The scoped key the tools act under. |
EMITHOOK_SCOPE | Operator-declared scope (read | write | admin, default read) — caps which tools are exposed. Never widens the key; the API enforces the real scope regardless. |
EMITHOOK_BASE_URL | API base override (self-host). |
EMITHOOK_INBOX_MONGO_URL + EMITHOOK_ORG_ID | Enable the inbox tools (read the MX emails store for that org). EMITHOOK_INBOX_MONGO_DB optional. Absent ⇒ inbox tools report "inbox not configured". |
Auth, scopes & safety
- Read tools are safe by default. A
read-scoped key can call them; they never mutate state. - Write tools require a
write(oradmin) key and an explicit confirmation: the registry appends aconfirmboolean to every write tool's schema, so the call is a no-op until the client passesconfirm: true. With areadscope the write tools aren't exposed at all. - Tools inherit the key's organization; an agent can never reach across orgs.
| Tool class | Min scope | Confirmation |
|---|---|---|
list_*, get_*, search_*, summarize_inbox_thread, get_metrics, pull_events | read | none (safe) |
send_webhook, send_app_event, send_email, replay_event, replay_endpoint_events, replay_alias_emails, replay_request, redrive_dlq, create_endpoint, update_endpoint, delete_endpoint, create_destination, validate_destination, rotate_endpoint_secret, add_domain, update_domain, verify_domain, create_alias, update_alias, delete_alias | write | confirm: true required |
Which tool to reach for
The catalog below says what each tool does. This says when to reach for it — which is the part an agent gets wrong.
| The question | Start here | Not here |
|---|---|---|
| Why did this one delivery fail? | diagnose_event | get_event — it gives you the facts but not the verdict, and you will re-derive the verdict badly |
| Something was sent to us and never arrived | diagnose_request / list_requests | list_events — an event only exists once a request was accepted and routed, so a dropped request is not in there |
| Is this one endpoint or all of them? | get_metrics | listing events and counting — the rollups already did it |
| Which failures, of what kind? | get_metrics → read error_mix | sampling events by hand |
| A route has gone quiet | diagnose_endpoint | get_endpoint — "it exists and is active" is not an answer to "nothing is arriving" |
| One target fails for everything | diagnose_destination | validate_destination — probe after you know it is the destination, not before |
| Before creating a destination | check_destination | create_destination then validate_destination — you have persisted something broken |
| Before adopting a domain | preflight_domain | add_domain — it can refuse, and the refusal is the thing you wanted to know |
| Mail stopped arriving for one address | list_suppressions | list_sent_emails — a suppressed address is silently skipped, so there is nothing to find |
| Did this email actually arrive? | get_sent_email (read the SES timeline) | the 202 from send_email, which means accepted, not delivered |
The pattern: diagnose_* before get_* when something is wrong, and get_metrics before either when you do not yet know what is wrong.
Tool catalog
Query (read-safe)
| Tool | Arguments | Returns |
|---|---|---|
list_events | status? (delivered|failed|retrying|dlq), endpoint?, event_type?, since?, until?, cursor?, limit? | A page of events. |
get_event | id | One event with all its delivery attempts. |
get_endpoint | id | One inbound endpoint (route, destinations, receive URL, status). |
list_destinations | type?, validation?, cursor?, limit? | Registry destinations. |
list_domains | — | Custom domains with verification state + DNS records. |
preflight_domain | domain | What mail config a domain already has, before adopting it. Takes a domain name. blocking means add_domain will refuse without acknowledging the MX conflict. (For a domain we already host: verify_domain.) |
list_ingest_domains | — | Selectable ingest domains (platform registry ∪ verified custom). |
list_requests | endpoint?, outcome? (accepted|dropped_unknown|parked_inactive), since?, until?, cursor?, limit? | Raw inbound requests the front door received (incl. dropped). |
get_request | id | One captured inbound request in full (headers + body). |
pull_events | endpoint, cursor?, limit? | Pull queued events for a firewalled endpoint consumer. |
list_aliases | cursor?, limit? | Inbound-email aliases (address, routing, sender rules). |
get_alias | id | One inbound-email alias by id. |
get_metrics | key (an endpoint or destination id) | Pre-aggregated rollups for that id. |
get_endpoint_stats | endpoint, window? (24h|7d|30d) | One endpoint's received / delivered / failed, success rate, latency, series and per-destination numbers. See Endpoint stats. |
get_endpoint_stats_embed | endpoint, window?, theme? (auto|light|dark), ttl? | A signed, expiring URL to a self-contained stats page anyone can open without a key. Can't be revoked individually; ttl is capped at 1 day here. |
Diagnose (RCA — read-safe)
Each returns a verdict, the evidence it rests on, and next actions in both MCP and CLI form. They never mutate: a diagnosis may recommend replay_event, but you make that call. Every verdict uses the shared failure taxonomy, so a diagnosis and a report never disagree about the same event.
| Tool | Arguments | Answers |
|---|---|---|
diagnose_event | id | Why an event failed — every attempt, plus whether comparable recent events are failing too (isolated vs outage). |
diagnose_request | id | Why an inbound request never became an event — signature, route match, endpoint state. |
diagnose_endpoint | id | Why nothing is arriving. Catches no destinations bound, no verification secret, paused, or never received anything — all invisible from other views. |
diagnose_destination | id | Validation state and what it implies. Says so plainly when the destination is fine. |
diagnose_domain | domain | Verification state + expected DNS records; falls back to preflight for a domain we don't host. |
diagnose_email | id | Suppression, then the SES timeline — hard bounce vs soft vs complaint vs never-sent. |
Email inbox (agent-native)
The headline of the MX engine: hand an agent its own address and let it reason over its mail. All read-safe; they read the MX emails store directly and activate only when EMITHOOK_INBOX_MONGO_URL + EMITHOOK_ORG_ID are set (otherwise they report "inbox not configured").
| Tool | Arguments | Returns |
|---|---|---|
list_inbox_messages | alias, cursor?, limit? | An alias's messages, newest first — from/to, subject, received, auth result, attachments, parsed text body. |
get_inbox_message | message_id | One email in full: headers + parsed body + SPF/DKIM/DMARC result, plus attachment metadata (filename, content type, size). |
search_inbox_messages | query, alias?, limit? | Newest-first matches across subject / from / body, optionally within one alias. |
summarize_inbox_thread | alias, subject | The alias's messages sharing that subject (Re:/Fwd: stripped), oldest first, for the model to summarize. Returns the thread — it does not itself summarize. |
Binary payloads stay on the CLI
No MCP tool returns raw bytes (ADR-0025 §7) — a large .eml or attachment base64'd into a tool result costs more context than it's worth. get_inbox_message gives you the attachment metadata so you can decide; emithook aliases messages raw <alias> <id> and emithook aliases messages attachment <alias> <id> <index> fetch the bytes.
Outbound email (mail you sent)
Distinct from the inbox above: these read the transactional email your org sent. Both surfaces carry an explicit name so a tool list can't be misread — inbox tools say INBOUND, these say OUTBOUND (ADR-0025).
| Tool | Arguments | Returns |
|---|---|---|
list_sent_emails | status?, since?, until?, cursor?, limit? | A page of sent emails. Filter by bounced / complained / suppressed to find what didn't arrive. |
get_sent_email | id | One sent email with its attempts and the full SES lifecycle timeline — the evidence for "why did this not arrive". |
list_suppressions | cursor?, limit? | The org's do-not-send list. Check this first when mail "stops arriving" — a listed address is silently skipped by every send. reason is bounce/complaint (added by Emithook) or manual. |
Operate (write — confirm: true required)
| Tool | Arguments | Effect |
|---|---|---|
send_webhook | destination, event_type, payload, headers?, idempotency_key? | Send one webhook to a destination id or HTTPS URL. |
check_destination | type, url? | Probe reachability before creating a destination — takes a type + URL, no id. Any HTTP status counts as reachable. (For an existing destination: validate_destination { id }.) |
update_destination | id, name?, status? (active|paused) | Rename, or pause/resume. Pausing holds delivery — events park and drain on resume. |
delete_destination | id | Delete a destination. Fails while an endpoint still routes to it. Prefer status: paused to stop delivery reversibly. |
add_suppression | address, idempotency_key? | Add an address to the do-not-send list. Always recorded manual; bounce/complaint entries are added server-side and can't be created here. |
delete_suppression | id | Remove an address, re-enabling sending to it. If it was listed for a bounce or complaint this risks sending reputation — read the entry with list_suppressions first. |
send_email | from, to[], subject, html?, text?, cc?[], bcc?[], reply_to?, attachments?[], idempotency_key? | Send one transactional email. A 202 means accepted, not delivered — follow with get_sent_email. Each attachment is either inline base64 content or a public https url Emithook fetches — never both — up to 20 items and 10 MB decoded per request; a redirecting URL is refused, so pass the final one. See Attachments. send_email_batch is deliberately not exposed: one confirmation would authorise up to 100 sends. Use emithook email send-batch for scripted bulk work. |
send_app_event | app_id, event_type, payload, idempotency_key? | Emit an event to an application id — fans out to its subscribed endpoints. |
replay_event | id | Re-deliver a single event to every destination it went to, including ones that already received it (flagged webhook-replayed, same webhook-id). An already-sent email is not re-sent. |
replay_endpoint_events | endpoint, since, until, status? (delivered|failed|dlq), destinations?[], dry_run?, cursor? | Re-send an endpoint's events from a time range to its current routes, for example to fill a newly added destination. Already-delivered events are re-sent. dry_run counts; otherwise it starts a background job and returns it — poll get_replay. One replay per endpoint at a time. See Replay a time range. |
replay_alias_emails | alias, since, until, status? (delivered|failed|dlq), dry_run?, cursor? | Re-send an inbound-email alias's emails from a time range to the destinations it routes to now. Already-delivered emails are re-sent; quarantined ones never. Up to 1,000 per call — repeat with next_cursor. alias is the al_… id or the address. See Receive email into SQS → Re-deliver. |
list_replays | endpoint?, limit? | Background replays, newest first — what is running and what finished. |
get_replay | id | How far a replay has got: state, events replayed vs the total counted, deliveries queued, any error. |
stop_replay | id | Stop a running replay (between pages). Queued events still deliver. |
resume_replay | id | Continue a stopped/failed replay from where it stopped, under the same run id (no double sends). |
replay_request | id | Re-deliver one captured inbound request through the router. |
redrive_dlq | endpoint?, since? | Bulk-replay dead-lettered events. |
create_endpoint | url, name?, preset?, destinations?[] | Create an inbound endpoint by its receive URL (its identity; on a shared host it starts with your org slug — see list_ingest_domains' prefix). |
update_endpoint | id, url?, status? (active|paused), preset?, destinations?[], response_rules? | Update an inbound endpoint (route, status, preset, destinations, responses). |
delete_endpoint | id | Delete an inbound endpoint. |
create_destination | name, type, url?, config? | Register an outbound destination (created pending validation). |
validate_destination | id | Run a live probe/test-send and update the destination's validation state. |
rotate_endpoint_secret | id, secret | Set an endpoint's inbound verify secret (write-only, never echoed). |
add_domain | domain, kind? (webhook|email|email-send) | Add a custom domain — returns the DNS records to publish. email-send SENDS ONLY: no MX is published for it and no alias can route on it. |
update_domain | id, kind (email|email-send), acknowledge_mx_conflict? | Convert a domain between receiving and send-only in place. Refused while an alias still receives on it. Converting to email publishes an MX that redirects the domain's mail and returns it to pending-dns. |
verify_domain | id | Re-check a custom domain's DNS and flip it active when the records resolve. |
create_alias | kind (platform|custom), local_part, domain_id?, destination_ids?[], allowed_senders?[], max_size_bytes? | Create an inbound-email alias (address computed server-side). |
update_alias | id, destination_ids?[], allowed_senders? (null clears), max_size_bytes? (null clears) | Update an alias's routing/limits. |
delete_alias | id | Delete an inbound-email alias. |
Example session
Agent: "What's failing for ep_razorpay in the last hour?"
→ get_metrics(key="ep_razorpay")
→ list_events(endpoint="ep_razorpay", status="failed", since="2026-06-22T05:00:00Z")
→ get_event(id="evt_01JX…") # inspect the 5xx body
Agent: "Replay the dead-lettered ones."
→ redrive_dlq(endpoint="ep_razorpay", confirm=true) # ⚠ write — needs confirmSee also
- API conventions — the scopes, IDs and errors these tools share.
- CLI — the same operations from a terminal.
- Glossary — entity definitions used in every argument.