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

ParameterTypeRequiredDescription
tostring[]✓Recipient addresses
subjectstring✓Subject line
html_bodystring✓HTML body
ccstring[]CC recipients
bccstring[]BCC recipients
text_bodystringPlain-text fallback
stream"transactional"|"marketing"Default: transactional
attachmentsAttachment[]See attachment shape below
idempotency_keystring24-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

ParameterTypeRequiredDescription
tostring[]✓Recipient addresses
subjectstring✓Subject line
markdown_bodystring✓Markdown source
ccstring[]CC recipients
bccstring[]BCC recipients
stream"transactional"|"marketing"Default: transactional
attachmentsAttachment[]
render_optionsRenderOptionsTheme + minify; see below
idempotency_keystring24-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

ParameterTypeRequiredDescription
mailboxstring✓Mailbox name
message_idnumber✓IMAP UID (from list_messages)
tostring[]✓Forward recipients

Returns: { message_ids: string[] }


Inbound reading

list_messages

List received messages in an IMAP mailbox.

Scope: messages:index

ParameterTypeRequiredDescription
mailboxstring✓Mailbox name
limitnumberResults per page (default: 10, max: 999)
offsetnumberPagination offset
filter_fromstringFilter by sender address
filter_tostringFilter by recipient address
filter_subjectstringFilter by subject
sincestringISO 8601; only messages after this instant
since_uidnumberIMAP UID cursor; only messages with a greater UID, oldest first

Returns: { data: Message[] }


get_message

Retrieve a single inbound message including both bodies and attachment metadata.

Scope: messages:show

ParameterTypeRequiredDescription
mailboxstring✓Mailbox name
message_idnumber✓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_id is an integer IMAP UID, not a UUID. Use the id field from list_messages results.


get_attachment

Download an attachment from an inbound message.

Scope: attachments:show

ParameterTypeRequiredDescription
mailboxstring✓Mailbox name
message_idnumber✓IMAP UID
filenamestring✓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

ParameterTypeRequiredDescription
mailboxstring✓Mailbox name
filter_subjectstringSubject filter
filter_fromstringSender filter
filter_tostringRecipient filter
timeoutnumberMax wait in seconds (default: 30, max: 120)
sincestringISO 8601; only match messages after this instant
since_uidnumberIMAP UID cursor; only match messages with a greater UID, oldest first

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

ParameterTypeRequiredDescription
status"queued"|"processing"|"sent"|"failed"|"bounced"Filter by status
stream"transactional"|"marketing"Filter by stream
recipientstringFilter by recipient address
limitnumberMax results (1–200, default: 100)

Returns: OutboundMessage[]


get_sent_message

Get a single outbound message with full delivery details.

Scope: messages:index

ParameterTypeRequiredDescription
message_idstring✓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

ParameterTypeRequiredDescription
message_idstring✓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

ParameterTypeRequiredDescription
mailboxstring✓Mailbox name
subscribed_onlybooleanOnly subscribed folders (default: true)
force_refreshbooleanRe-sync from live IMAP before returning (default: false)

Returns: { data: Folder[] }


create_folder

Create an IMAP folder.

Scope: folders:write

ParameterTypeRequiredDescription
mailboxstring✓Mailbox name
pathstring✓Full IMAP path, /-delimited (e.g. Projects/2026)

Returns: Folder


delete_folder

Delete an IMAP folder.

Scope: folders:write

ParameterTypeRequiredDescription
mailboxstring✓Mailbox name
folder_pathstring✓Full IMAP path

Returns: 204 No Content (empty result)


set_folder_subscription

Subscribe or unsubscribe from an IMAP folder.

Scope: folders:write

ParameterTypeRequiredDescription
mailboxstring✓Mailbox name
folder_pathstring✓Full IMAP path
subscribedboolean✓true to subscribe, false to unsubscribe

Returns: Updated Folder


move_messages

Move or copy messages between IMAP folders.

Scope: messages:move

ParameterTypeRequiredDescription
mailboxstring✓Mailbox name
source_folderstring✓Source IMAP path
target_folderstring✓Destination IMAP path
uidsnumber[]✓IMAP UIDs to move/copy
action"move"|"copy"Default: "move"

Returns: { status: "success" }


Scope reference

ToolScope required
send_emailmessages:send
send_markdownmessages:send
forward_messagemessages:send
list_messagesmessages:index
get_messagemessages:show
get_attachmentattachments:show
wait_for_messagemessages:index
list_sent_messagesmessages:index
get_sent_messagemessages:index
get_message_eventsmessages:index
list_foldersmessages:index
create_folderfolders:write
delete_folderfolders:write
set_folder_subscriptionfolders:write
move_messagesmessages: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

ChannelStatusNotes
Container image (GitLab registry)AvailableCI builds $CI_REGISTRY_IMAGE/mcp:<ref> on every MR and :latest on main/tags.
Local clone + buildAvailablenpm ci && npm run build → run dist/index.js.
npx @squibble/mcp (public npm)PlannedPublish 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.

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 Code (.mcp.json, remote HTTP)

The gateway serves MCP over HTTP at /mcp. mise run email:mcp-config generates one entry per mailbox; each headersHelper reads its JWT from pass at connect time:

{
  "mcpServers": {
    "squibble": {
      "type": "http",
      "url": "https://api.email.squibble.ch/mcp",
      "headersHelper": "export PATH=/opt/homebrew/bin:$PATH PASSWORD_STORE_DIR=$HOME/development/pass; t=$(pass show email.squibble.ch/<mailbox>/agents-read | head -1); [ -n \"$t\" ] || exit 1; printf '{\"Authorization\":\"Bearer %s\"}' \"$t\""
    }
  }
}

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, command becomes ["npx", "@squibble/mcp"] with the same env.


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_attachment returns 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.

Sourced from docs/features/mcp-server in the repo. Edits go through the same review as code.