Squibble Email MCP Server
Overview
The Squibble Email MCP (Model Context Protocol) server is the official integration point for AI agents and LLMs to access the full email gateway surface. It implements the Model Context Protocol standard, making Squibble a drop-in tool for any MCP-compatible runtime.
Current version: 1.1.0 (15 tools).
What is MCP?
Model Context Protocol is an open standard (developed by Anthropic) for AI systems to interact with external tools and data sources. MCP provides:
- Tool definitions (what the tool does, what inputs it needs)
- Safe tool isolation (agents call tools through a sandboxed interface)
- Standardized error handling
Any LLM or agent framework supporting MCP (Claude, ChatGPT with tools, LangChain, CrewAI, etc.) can use Squibble tools without custom integration code.
Architecture
┌─────────────┐
│ AI Agent │ (Claude, LangChain, custom agent)
│ or LLM │
└──────┬──────┘
│ (MCP stdio protocol)
│
┌──────▼───────────────────────────┐
│ Squibble MCP Server v1.1.0 │
│ │
│ Sending │
│ ├─ send_email │
│ ├─ send_markdown │
│ └─ forward_message │
│ │
│ Inbound reading │
│ ├─ list_messages │
│ ├─ get_message │
│ ├─ get_attachment │
│ └─ wait_for_message │
│ │
│ Delivery status │
│ ├─ list_sent_messages │
│ ├─ get_sent_message │
│ └─ get_message_events │
│ │
│ Folder management │
│ ├─ list_folders │
│ ├─ create_folder │
│ ├─ delete_folder │
│ ├─ set_folder_subscription │
│ └─ move_messages │
└──────┬───────────────────────────┘
│ (HTTP/REST)
│
┌──────▼──────────────────┐
│ Squibble API Gateway │
│ (JWT auth + scopes) │
└────────────────────────┘
Key Properties
No new auth model. The MCP server takes a Squibble JWT and forwards it
as Authorization: Bearer <token> on every request. Scopes are enforced
by the gateway, not the server.
Zero additional authority. A token without messages:send → send_email
fails with the gateway’s 403, surfaced directly to the agent.
Never holds credentials. The JWT is never persisted, logged, or cached beyond a single HTTP request.
Sender binding intact. Emails sent via MCP appear in your audit log under the same jti as direct API sends. The sender address is bound to the token’s mailbox configuration.
Tools
Sending
send_email
Send an outbound email.
Scope: messages:send
| Parameter | Type | Required | Description |
|---|---|---|---|
to | string[] | ✓ | Recipient addresses |
subject | string | ✓ | Subject line |
html_body | string | ✓ | HTML body |
cc | string[] | CC recipients | |
bcc | string[] | BCC recipients | |
text_body | string | Plain-text fallback | |
stream | "transactional"|"marketing" | Default: transactional | |
attachments | Attachment[] | See attachment shape below | |
idempotency_key | string | 24-hour dedup window |
Returns: { message_ids: string[] }
Gateway-enforced: recipient-domain allowlist, send quotas, suppression list checks, idempotency dedup.
send_markdown
Server renders Markdown to themed HTML + plain-text before sending — the most LLM-native sending path.
Scope: messages:send
| Parameter | Type | Required | Description |
|---|---|---|---|
to | string[] | ✓ | Recipient addresses |
subject | string | ✓ | Subject line |
markdown_body | string | ✓ | Markdown source |
cc | string[] | CC recipients | |
bcc | string[] | BCC recipients | |
stream | "transactional"|"marketing" | Default: transactional | |
attachments | Attachment[] | ||
render_options | RenderOptions | Theme + minify; see below | |
idempotency_key | string | 24-hour dedup window |
Returns: { message_ids: string[] }
RenderOptions:
{
"minify": true,
"theme": {
"brand_color": "#0070f3",
"heading_color": "#111",
"body_color": "#333",
"background_color": "#f9f9f9",
"font_family": "Inter, sans-serif"
}
}
All theme keys are optional strings; only provided keys override the server’s default theme.
forward_message
Forward an inbound message to additional recipients.
Scope: messages:send
| Parameter | Type | Required | Description |
|---|---|---|---|
mailbox | string | ✓ | Mailbox name |
message_id | number | ✓ | IMAP UID (from list_messages) |
to | string[] | ✓ | Forward recipients |
Returns: { message_ids: string[] }
Inbound reading
list_messages
List received messages in an IMAP mailbox.
Scope: messages:index
| Parameter | Type | Required | Description |
|---|---|---|---|
mailbox | string | ✓ | Mailbox name |
limit | number | Results per page (default: 10, max: 1000) | |
offset | number | Pagination offset | |
filter_from | string | Filter by sender address | |
filter_to | string | Filter by recipient address | |
filter_subject | string | Filter by subject | |
since | string | ISO 8601; only messages after this instant |
Returns: { data: Message[] }
get_message
Retrieve a single inbound message including both bodies and attachment metadata.
Scope: messages:show
| Parameter | Type | Required | Description |
|---|---|---|---|
mailbox | string | ✓ | Mailbox name |
message_id | number | ✓ | IMAP UID (integer from list_messages) |
Returns: Full message object with from, to, cc, subject,
date, text_body, html_body, and attachments metadata list.
Note:
message_idis an integer IMAP UID, not a UUID. Use theidfield fromlist_messagesresults.
get_attachment
Download an attachment from an inbound message.
Scope: attachments:show
| Parameter | Type | Required | Description |
|---|---|---|---|
mailbox | string | ✓ | Mailbox name |
message_id | number | ✓ | IMAP UID |
filename | string | ✓ | Filename as listed in the message’s attachments |
Returns: { content_type: string, content_base64: string, size_bytes: number }
The content is base64-encoded; decode before writing to disk.
wait_for_message
Long-poll (up to 120 seconds) for a new inbound message matching optional filters. Useful for CI flows where you trigger an action and wait for the confirmation email.
Scope: messages:index
| Parameter | Type | Required | Description |
|---|---|---|---|
mailbox | string | ✓ | Mailbox name |
filter_subject | string | Subject filter | |
filter_from | string | Sender filter | |
filter_to | string | Recipient filter | |
timeout | number | Max wait in seconds (default: 30, max: 120) | |
since | string | ISO 8601; only match messages after this instant |
Returns: Message object, or a timeout error after the wait period.
Delivery status
Use these tools to close the send loop: verify delivery, detect bounces, and observe open/click events.
list_sent_messages
List outbound messages sent by this token’s mailbox.
Scope: messages:index
| Parameter | Type | Required | Description |
|---|---|---|---|
status | "queued"|"processing"|"sent"|"failed"|"bounced" | Filter by status | |
stream | "transactional"|"marketing" | Filter by stream | |
recipient | string | Filter by recipient address | |
limit | number | Max results (1–200, default: 100) |
Returns: OutboundMessage[]
get_sent_message
Get a single outbound message with full delivery details.
Scope: messages:index
| Parameter | Type | Required | Description |
|---|---|---|---|
message_id | string | ✓ | UUID returned by send_email or send_markdown |
Returns: OutboundMessage including status, sent_at,
bounced_at, bounce_type, opened_at, clicked_at,
attempts, error_log.
get_message_events
Full lifecycle event log for an outbound message.
Scope: messages:index
| Parameter | Type | Required | Description |
|---|---|---|---|
message_id | string | ✓ | UUID of the outbound message |
Returns: OutboundMessageEvent[] in chronological order.
Event types: accepted, processing, sent, failed, bounced,
opened, clicked, unsubscribed, requeued, cancelled.
Folder management
list_folders
List IMAP folders for a mailbox.
Scope: messages:index
| Parameter | Type | Required | Description |
|---|---|---|---|
mailbox | string | ✓ | Mailbox name |
subscribed_only | boolean | Only subscribed folders (default: true) | |
force_refresh | boolean | Re-sync from live IMAP before returning (default: false) |
Returns: { data: Folder[] }
create_folder
Create an IMAP folder.
Scope: folders:write
| Parameter | Type | Required | Description |
|---|---|---|---|
mailbox | string | ✓ | Mailbox name |
path | string | ✓ | Full IMAP path, /-delimited (e.g. Projects/2026) |
Returns: Folder
delete_folder
Delete an IMAP folder.
Scope: folders:write
| Parameter | Type | Required | Description |
|---|---|---|---|
mailbox | string | ✓ | Mailbox name |
folder_path | string | ✓ | Full IMAP path |
Returns: 204 No Content (empty result)
set_folder_subscription
Subscribe or unsubscribe from an IMAP folder.
Scope: folders:write
| Parameter | Type | Required | Description |
|---|---|---|---|
mailbox | string | ✓ | Mailbox name |
folder_path | string | ✓ | Full IMAP path |
subscribed | boolean | ✓ | true to subscribe, false to unsubscribe |
Returns: Updated Folder
move_messages
Move or copy messages between IMAP folders.
Scope: messages:move
| Parameter | Type | Required | Description |
|---|---|---|---|
mailbox | string | ✓ | Mailbox name |
source_folder | string | ✓ | Source IMAP path |
target_folder | string | ✓ | Destination IMAP path |
uids | number[] | ✓ | IMAP UIDs to move/copy |
action | "move"|"copy" | Default: "move" |
Returns: { status: "success" }
Scope reference
| Tool | Scope required |
|---|---|
send_email | messages:send |
send_markdown | messages:send |
forward_message | messages:send |
list_messages | messages:index |
get_message | messages:show |
get_attachment | attachments:show |
wait_for_message | messages:index |
list_sent_messages | messages:index |
get_sent_message | messages:index |
get_message_events | messages:index |
list_folders | messages:index |
create_folder | folders:write |
delete_folder | folders:write |
set_folder_subscription | folders:write |
move_messages | messages:move |
Issue tokens with the minimal set of scopes the agent needs:
# Read-only inbox agent
mise run cli tokens:issue --mailbox qa@example.com \
--scopes 'messages:index,messages:show,attachments:show'
# Send + verify delivery (no inbox access)
mise run cli tokens:issue --mailbox notify@example.com \
--scopes 'messages:send,messages:index'
# Full agent (send, read, triage)
mise run cli tokens:issue --mailbox agent@example.com \
--scopes 'messages:send,messages:index,messages:show,messages:move,attachments:show,folders:write'
Attachment shape
Used by send_email and send_markdown:
{
"filename": "report.pdf",
"content_type": "application/pdf",
"content": "<base64-encoded bytes>"
}
Gateway limits: 10 MiB per file, 25 MiB total per send.
Transport: Stdio
The MCP server uses stdio (stdin/stdout) for communication with the agent runtime:
- Runs as a subprocess on the same machine as the agent
- JSON-RPC messages over stdin/stdout
- Works with Claude Desktop, CI/CD pipelines, and agent frameworks
HTTP transport is a documented follow-up for hosted scenarios.
Deployment & Configuration
Distribution channels
| Channel | Status | Notes |
|---|---|---|
| Container image (GitLab registry) | Available | CI builds $CI_REGISTRY_IMAGE/mcp:<ref> on every MR and :latest on main/tags. |
| Local clone + build | Available | npm ci && npm run build → run dist/index.js. |
npx @squibble/mcp (public npm) | Planned | Publish job is a documented follow-up (TICKET-005). |
The MCP server is distributed to the consumer — it is not part of
the deploy/dev|prod Ansible playbooks.
Run via the prebuilt container image (recommended)
docker run -i --rm \
-e SQUIBBLE_API_BASE_URL="https://me.squibble.email" \
-e SQUIBBLE_JWT="$JWT" \
"$CI_REGISTRY_IMAGE/mcp:latest"
Build locally without registry access:
docker build -t squibble-mcp app_mcp/
docker run -i --rm -e SQUIBBLE_API_BASE_URL="..." -e SQUIBBLE_JWT="$JWT" squibble-mcp
Run from a local clone
cd app_mcp && npm ci && npm run build
export SQUIBBLE_API_BASE_URL="https://email.yourcompany.com"
export SQUIBBLE_JWT="$JWT"
node dist/index.js
Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"squibble": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "SQUIBBLE_API_BASE_URL",
"-e", "SQUIBBLE_JWT",
"registry.git.intern.squibble.me/me.squibble/applications/me.squibble.email/mcp:latest"
],
"env": {
"SQUIBBLE_API_BASE_URL": "https://me.squibble.email",
"SQUIBBLE_JWT": "$SQUIBBLE_JWT"
}
}
}
}
Or point command at node and args at the absolute path to
app_mcp/dist/index.js for a local build.
LangChain / Python agent
from langchain_community.tools import StdioMCPTool
squibble_mcp = StdioMCPTool(
name="squibble",
command=["node", "/abs/path/to/app_mcp/dist/index.js"],
env={
"SQUIBBLE_API_BASE_URL": "https://me.squibble.email",
"SQUIBBLE_JWT": os.environ["SQUIBBLE_JWT"],
},
)
Once the npm publish follow-up lands,
commandbecomes["npx", "@squibble/mcp"]with the sameenv.
Security
Scope denial. If an agent calls a tool its token lacks the scope for, the gateway returns an HTTP 403 that is surfaced verbatim:
{
"error": true,
"status": 403,
"type": "https://email.squibble.ch/docs/errors/scope-denied",
"title": "Scope Denied",
"detail": "Token does not have messages:send scope"
}
No credential storage. The JWT is passed in-memory only; never written to disk, logged, or cached after the HTTP request completes.
Audit log. All tool calls that result in a successful API call are recorded in the gateway audit log under the token’s jti — indistinguishable from direct API calls.
Known Limitations
- HTTP transport — currently stdio only; HTTP transport for hosted agents is a documented follow-up.
- Attachment size — 10 MiB per file, 25 MiB total (gateway limit);
get_attachmentreturns base64, inflating payload size by ~33%. - Suppression check — there is no pre-send suppression lookup tool; sending to a suppressed recipient returns a 422 from the gateway. A dedicated check endpoint is a planned follow-up.
Getting Started
See examples/mcp-agent/README.md
for a quick-start guide.