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:sendsend_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
subjectstringSubject line
html_bodystringHTML 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
subjectstringSubject line
markdown_bodystringMarkdown 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
mailboxstringMailbox name
message_idnumberIMAP 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
mailboxstringMailbox name
limitnumberResults per page (default: 10, max: 1000)
offsetnumberPagination offset
filter_fromstringFilter by sender address
filter_tostringFilter by recipient address
filter_subjectstringFilter by subject
sincestringISO 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

ParameterTypeRequiredDescription
mailboxstringMailbox name
message_idnumberIMAP 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
mailboxstringMailbox name
message_idnumberIMAP UID
filenamestringFilename 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
mailboxstringMailbox 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

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_idstringUUID 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_idstringUUID 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
mailboxstringMailbox 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
mailboxstringMailbox name
pathstringFull IMAP path, /-delimited (e.g. Projects/2026)

Returns: Folder


delete_folder

Delete an IMAP folder.

Scope: folders:write

ParameterTypeRequiredDescription
mailboxstringMailbox name
folder_pathstringFull IMAP path

Returns: 204 No Content (empty result)


set_folder_subscription

Subscribe or unsubscribe from an IMAP folder.

Scope: folders:write

ParameterTypeRequiredDescription
mailboxstringMailbox name
folder_pathstringFull IMAP path
subscribedbooleantrue to subscribe, false to unsubscribe

Returns: Updated Folder


move_messages

Move or copy messages between IMAP folders.

Scope: messages:move

ParameterTypeRequiredDescription
mailboxstringMailbox name
source_folderstringSource IMAP path
target_folderstringDestination 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 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, 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.