MCP servers
Sendoka ships two Model Context Protocol servers:
- Keyless dev MCP (
@sendoka/dev-mcp) — explore the API with no account: docs search, the OpenAPI surface, local validators, and dry-runs. Start here when you're learning or building. - Authenticated MCP (
POST /api/mcp) — drive your workspace: send messages, query activity, check usage. Needs asok_live_*/sok_test_*key.
An assistant can connect both at once: explore + validate keylessly, then switch to the authenticated server to actually send.
Keyless dev MCP
@sendoka/dev-mcp is a zero-dependency stdio server that exposes only public surface — the docs index, the published OpenAPI schema, and local validators. No key, no account, no workspace access, and it can never send. Safe to connect anywhere.
Install (keyless)
claude mcp add sendoka-dev -- npx -y @sendoka/dev-mcp@latest
For other MCP clients, point them at the stdio command npx -y @sendoka/dev-mcp@latest.
Tools (keyless)
| Tool | What it does |
|---|---|
platform_overview |
Start here. One-call functional map of the whole platform — capability areas (live from the API), the auth model, conventions, and the webhook event catalog. |
search_docs |
Full-text search across the developer docs. |
lookup_endpoint |
Browse the public OpenAPI schema — list every operation or inspect one. |
generate_snippet |
Copy-paste code for a REST call (curl, node, python, go, ruby, php). |
validate_payload |
Check a send body against published constraints locally — no API call. |
lint_message |
Deliverability + compliance lints (TCPA opt-out, RFC 8058 unsubscribe, GSM-7 vs UCS-2, multi-segment SMS, spammy subjects). |
normalize_phone / parse_email |
Pre-flight recipient values. |
render_template_dry |
Preview a {{var}} template render without an API call. |
dry_run_send |
One-shot pre-flight: validate + lint + recipient parse. Returns ready: true/false. |
Typical flow: platform_overview → lookup_endpoint / search_docs → generate_snippet → validate_payload / lint_message / dry_run_send — then switch to the authenticated MCP below to actually send.
Source lives in the monorepo at packages/dev-mcp/. Override the target deployment with SENDOKA_DEV_BASE (defaults to https://www.sendoka.com).
Authenticated MCP
Sendoka ships a Model Context Protocol endpoint at POST /api/mcp so AI coding assistants (Claude Code, Cursor, etc.) can drive your workspace directly — send messages, query activity, check usage — without scraping the dashboard or generating REST calls.
The endpoint speaks the Streamable HTTP transport in stateless mode. JSON-RPC 2.0 over a single POST. No session state, no SSE.
Auth
Authorization: Bearer <key> — same sok_live_* / sok_test_* keys you use for the REST API. The MCP layer validates the key on every POST, then forwards each tool call to the corresponding /api/v1/* endpoint with the same header. That means scope, rate-limit, idempotency, audit, and usage metering all apply exactly as they would for a direct REST call — there is no second control plane.
Unauthenticated requests get a 401 with WWW-Authenticate: Bearer realm="Sendoka MCP", resource_metadata="…" so compliant MCP clients can discover the auth scheme automatically.
Install in Claude Code
claude mcp add --transport http sendoka https://api.sendoka.com/api/mcp \
--header "Authorization: Bearer sok_live_..."
For platform-mode keys that target a specific tenant, add the ref header:
claude mcp add --transport http sendoka https://api.sendoka.com/api/mcp \
--header "Authorization: Bearer sok_live_..." \
--header "X-Sendoka-Tenant-Ref: tenant-external-id"
Scope per tool
Keys with null scopes (full access) can use every tool. Scoped keys can only use tools whose scope is granted.
| Tool | Scope |
|---|---|
send_sms |
send:sms |
send_email |
send:email |
list_sms, list_emails, get_message |
read:messages |
activity_summary |
read:events |
usage_summary |
read:usage |
list_contacts |
read:audiences |
list_templates |
read:templates |
validate_email |
validate:email |
validate_phone |
validate:phone |
list_audiences |
read:audiences |
create_audience, send_audience |
write:audiences |
list_suppressions |
read:suppressions |
add_suppression, remove_suppression |
write:suppressions |
get_contact |
read:audiences |
create_contact, update_contact, delete_contact |
write:audiences |
send_email_batch |
send:email |
send_sms_batch |
send:sms |
list_domains, get_domain, diagnose_domain |
read:domains |
verify_domain |
write:domains |
get_template |
read:templates |
create_template, update_template, delete_template |
write:templates |
list_phone_numbers, get_phone_number |
read:phone_numbers |
provision_phone_number |
write:phone_numbers |
list_messaging_pools, get_messaging_pool |
read:pools |
create_messaging_pool |
write:pools |
list_webhooks, get_webhook, list_webhook_deliveries |
read:webhooks |
create_webhook, update_webhook, delete_webhook, replay_webhook_delivery, replay_failed_webhook_deliveries, test_fire_webhook |
write:webhooks |
list_brands, get_brand |
read:brands |
create_brand |
write:brands |
list_campaigns, get_campaign |
read:campaigns |
create_campaign |
write:campaigns |
Tools
send_sms
Send an SMS. Mirrors POST /api/v1/sms. Supports templates (template + variables), scheduling (scheduled_at or scheduled_local + scheduled_at_tz), tags, metadata, India DLT (dlt_template_id required for +91 destinations).
send_email
Send a transactional email. Mirrors POST /api/v1/emails. Supports templates, attachments (base64 content or url), open/click tracking, scheduling, headers.
list_sms / list_emails
Paginated message lists for the key's environment. Compound cursor ({ISO}|{id}) — pass next_cursor from the previous response.
get_message
Single message by id. Requires both id and channel (sms or email) so the MCP layer can route to the right sub-resource.
activity_summary
Daily counts grouped by channel + status over the last N days (max 90). Same shape as GET /api/v1/activity.
usage_summary
Current period usage vs plan limits. Set by_key: true to include the per-API-key breakdown.
list_contacts / list_templates
Cursor-paginated lists. Useful for an assistant to discover what's available before composing a send.
validate_email / validate_phone
Syntactic + MX (email) / country detection (phone). Same as the REST /api/v1/validate/* endpoints.
list_audiences / create_audience
Manage audience definitions. create_audience takes a slug (kebab-case, unique within org/tenant) and name.
send_audience
Templated bulk send to every contact in an audience. Channel-aware (email or sms), supports ramp_minutes for paced delivery, subject_variants for deterministic A/B testing (email only), and scheduling. Capped at 10,000 recipients per send. Returns counts of scheduled / suppressed and the delivery window.
list_suppressions / add_suppression / remove_suppression
Manage the suppression list. add_suppression takes channel, value, and an optional reason (manual, unsubscribe, bounce, complaint, stop). Tenant-bound keys see and edit only their own tenant's rows plus platform-wide ones.
Contacts (write) — get_contact / create_contact / update_contact / delete_contact
CRUD over contacts. create / update take email and/or phone, name, metadata, and audience slugs; update_contact.audiences replaces the full membership set. Mutations require write:audiences.
Domains — list_domains / get_domain / verify_domain / diagnose_domain
Inspect and verify sending domains. verify_domain re-checks DNS against SES (safe to repeat); diagnose_domain runs a DKIM/SPF/DMARC/MX deliverability check. Call list_domains (or get_context) before send_email to confirm the from-domain is verified.
Batch send — send_email_batch / send_sms_batch
One sender + body fanned out to up to 100 recipients, each as its own private envelope + message id. Mirror POST /api/v1/{emails,sms}/batch. Not idempotent. For saved groups or per-recipient variables, use send_audience.
Templates (write) — get_template / create_template / update_template / delete_template
Full template lifecycle. create_template fixes channel (email|sms) and a kebab-case slug; update_template patches content (pass html:null / text:null to clear). Slugs flow into the template arg on the send tools.
Phone numbers — list_phone_numbers / get_phone_number / provision_phone_number
SMS sender setup. provision_phone_number branches on kind: number allocates a real US/CA number (longcode / tollfree / shortcode, optional 10DLC campaign_id); alphanumeric registers a sender ID for UK/AU/EU. Provisioning a real number hits AWS and costs money.
Messaging pools — list_messaging_pools / get_messaging_pool / create_messaging_pool
Named groups of registered numbers. The pool slug becomes the from_pool value on send_sms / send_audience; Sendoka picks the best number per recipient.
Webhooks — list_webhooks / get_webhook / create_webhook / update_webhook / delete_webhook / list_webhook_deliveries / replay_webhook_delivery / replay_failed_webhook_deliveries / test_fire_webhook
Manage event subscriptions and debug delivery. create_webhook returns the signing secret once. list_webhook_deliveries surfaces non-2xx responses and exhausted retries; replay_webhook_delivery re-sends a single past delivery (whd_...); replay_failed_webhook_deliveries recovers a whole backlog (100/call, oldest first, optional since); test_fire_webhook sends a synthetic signed fixture so an agent can verify the user's receiver end-to-end.
10DLC brands + campaigns — list_brands / get_brand / create_brand / list_campaigns / get_campaign / create_campaign
US A2P registration. Register a brand, then a campaign under it (use_case + a description of ≥ 40 chars + sample_messages), then attach numbers for longcode throughput. Both registrations are async — poll status via the matching get_* tool.
Not exposed over MCP (by design): API-key management, tenant suspend/unsuspend, and webhook secret rotation. These are credential- or platform-destructive operations — perform them via the dashboard or a direct REST call with a narrowly-scoped key.
Discovery
GET /.well-known/oauth-protected-resource returns RFC 9728 metadata so MCP clients can auto-detect the auth scheme:
{
"resource": "https://api.sendoka.com/api/mcp",
"authorization_servers": [],
"bearer_methods_supported": ["header"],
"scopes_supported": ["send:email", "send:sms", "..."],
"resource_name": "Sendoka MCP"
}
authorization_servers: [] signals "no OAuth flow — bring your own static bearer token from the dashboard." 401 responses include a WWW-Authenticate header pointing at this document so compliant clients can wire the discovery flow automatically.
Protocol details
- Protocol version:
2025-06-18 - Transport: Streamable HTTP, stateless
GET /api/mcpreturns 405 — we don't open server-initiated streams. Clients fall back to plain request/response.OPTIONSis allowed with permissive CORS so browser-based MCP clients can connect (auth still required on POST).- Errors: tool failures return a JSON-RPC result with
isError: trueand the upstream HTTP status in_meta.upstream_status. Protocol-level errors (parse, invalid request, unknown method) use the standard JSON-RPC error codes.
Adding new tools
Edit src/app/api/mcp/tools.ts. Each tool entry needs a name, description, inputSchema (JSON Schema), and a handler that calls forward(req, method, path, body) against the v1 route. Re-use the existing v1 endpoint instead of touching service code directly — that's what keeps the auth + observability surface coherent.