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
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_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 |
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. |
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 (flagged webhook-replayed). |
replay_request | id | Re-deliver one captured inbound request through the router. |
redrive_dlq | endpoint?, since? | Bulk-replay dead-lettered events. |
create_endpoint | slug, path?, preset?, destinations?[] | Create an inbound endpoint. |
update_endpoint | id, slug?, path?, 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.