SMS tools for AI agents
Read SMS threads, work an agent inbox, and send texts from MCP clients and the actions API.
The Speko MCP server exposes the shared inbox and sending as eight tools. Each tool runs the same code as its /v1/sms endpoint, so it accepts the same filters, returns the same JSON, and refuses with the same error codes. You can also call any of them over HTTP with POST /v1/actions/{tool}.
| Tool | REST twin | Scope |
|---|---|---|
sms.conversations.list | GET /v1/sms/conversations | speko:read |
sms.conversations.get | GET /v1/sms/conversations/{id} | speko:read |
sms.conversations.messages.list | GET /v1/sms/conversations/{id}/messages | speko:read |
sms.conversations.update | PATCH /v1/sms/conversations/{id} | speko:write |
sms.conversations.reply | POST /v1/sms/conversations/{id}/messages | speko:execute |
sms.messages.list | GET /v1/sms/messages | speko:read |
sms.messages.get | GET /v1/sms/messages/{id} | speko:read |
sms.messages.send | POST /v1/sms/messages | speko:execute |
Inputs and results
Path parameters become fields: conversation_id and message_id. Query filters keep their REST names. Lists take cursor and limit (1 to 100, default 50) and return { "data": [...], "next_cursor": "..." } inside the action result, with next_cursor set to null on the last page.
agent_id on sms.conversations.list is the agent inbox: the threads the agent handles now, plus every thread holding a message recorded with that agent. assigned_agent_id returns only the threads the agent handles now. See Agents and numbers.
sms.conversations.reply, sms.conversations.update and sms.messages.send accept expected_agent_id. Send it when the tool acts for one agent, and the write is refused with SMS_THREAD_AGENT_CHANGED unless that agent handles the thread, as the REST writes do.
Sending from a tool call
sms.messages.send and sms.conversations.reply take an idempotency_key field instead of the Idempotency-Key header. A retry with the same key and input returns the message already created, as it is now. That holds even when the first call failed after the message was stored, and after the thread was deleted, when the message comes back with text: null. The same key with different input returns IDEMPOTENCY_CONFLICT. sms.messages.send shares its keys with POST /v1/sms/messages, so one key never sends twice across the two. The SMS tools ignore an Idempotency-Key header.
The operation record of an SMS tool call, returned by operations.get, keeps ids and statuses only. It never holds message text, phone numbers, metadata, schedules, timezones, or list filters, and it keeps an idempotency_key only as its SHA-256 hash, so deleting a thread or the retention window leaves no copy behind.
Both tools send only once the workspace has a number on an approved 10DLC campaign. Before that they return SMS_SENDING_NOT_APPROVED. The read tools work for every workspace.
sms.messages.send is always proactive. It needs an active consent record (CONSENT_REQUIRED) and a resolvable recipient timezone (RECIPIENT_TIMEZONE_REQUIRED). sms.conversations.reply needs neither when the contact texted in the last 24 hours. A proactive message requested during quiet hours is not refused. It returns status: "scheduled" with effective_send_at set to the next allowed time.
Both tools queue the message rather than send it. Speko checks it again just before submission, and quiet hours or throughput limits can still defer it, so status: "queued" does not mean sent. Read the message with sms.messages.get to see whether it went out.
Hosts that serve them
mcp.speko.ai serves all eight tools. The Anthropic MCP Directory host and Meta Muse serve the five reads. Muse also serves sms.conversations.update. No directory host serves sms.messages.send or sms.conversations.reply, because a text reaches a real person's phone. The ChatGPT, builder, and Replit hosts serve none of them.