# MCP Tool Contract This document defines the first build artifact for `mail-imap-mcp-rs`: the server-facing MCP contract. It is the source of truth for tool names, input/output shapes, validation bounds, and safety rules. ## Design Decisions - Transport: stdio by default, with optional streamable HTTP transport at `/mcp`. - Auth/config: environment variables only. - Message locator: stable `message_id` format `imap:{account_id}:{mailbox}:{uidvalidity}:{uid}`. - Destructive/write operations: disabled by default and explicitly gated. - Output style: concise summaries with bounded structured data. - Compatibility: no backward compatibility requirement with earlier implementations. - MCP input schemas must remain client-safe: plain object properties only, with conditional rules enforced at runtime rather than schema unions. ## Shared Input Types ### `account_id` - Type: string - Pattern: `^[A-Za-z0-9_-]{1,64}$` - Default: `"default"` ### `mailbox` - Type: string - Length: 1..256 ### `message_id` - Type: string - Format: `imap:{account_id}:{mailbox}:{uidvalidity}:{uid}` - Validation rules: - Prefix must be `imap`. - `uidvalidity` and `uid` must be non-negative integers. - Parsed `account_id` must match requested account. ### `limit` (message search) - Type: integer - Range: 1..100 - Default: 10 ## Shared Output Envelope All tools return: ```json { "summary": "human-readable one-line outcome", "data": {}, "meta": { "now_utc": "ISO-8601 UTC timestamp", "duration_ms": 0 } } ``` Error responses use a consistent shape: ```json { "error": { "code": "invalid_input|auth_failed|not_found|timeout|conflict|internal", "message": "actionable message", "details": {} }, "meta": { "now_utc": "ISO-8601 UTC timestamp", "duration_ms": 0 } } ``` Runtime IMAP command failures are returned in successful `data` payloads whenever possible (to preserve partial results for the LLM), using: - `status`: `ok|partial|failed` - `issues`: array of `{ code, stage, message, retryable, uid?, message_id? }` - `next_action`: `{ instruction, tool, arguments }` Hard MCP errors are reserved for validation/precondition failures (for example: invalid input, malformed ids, conflicting cursor state, write-gate disabled). ## Tool Set ### 1) `imap_list_accounts` Purpose: list configured accounts without exposing secrets. Input: - none Output `data`: - `accounts`: array (max 50) of `{ account_id, host, port, secure }` - `next_action`: `{ instruction, tool, arguments }` (recommended follow-up is `imap_list_mailboxes`) ### 2) `imap_list_mailboxes` Purpose: list visible mailboxes/folders. Input: - `account_id` (optional) - `cursor?` (string, opaque account-bound continuation token) - `limit?` (integer, 1..200, default 100) When `cursor` is present, the tool resumes the stored mailbox snapshot for the same account. The cursor is opaque and cannot be replayed against another account. Output `data`: - `status`: `ok|partial|failed` - `issues`: array of diagnostic issues - `next_action`: `{ instruction, tool, arguments }` (when another page is available, recommends `imap_list_mailboxes` with `account_id` and `next_cursor`) - `account_id` - `returned` (integer) - `total?` (integer; omitted when the server's complete mailbox count is unavailable) - `has_more` (boolean) - `next_cursor?` (string, opaque; present when `has_more=true`) - `truncated` (boolean) - `mailboxes`: array (max 200) of: - `name` - `delimiter?` - `attributes` (string[]) - `role?` (`inbox|all|archive|drafts|flagged|important|junk|sent|trash`) - `selectable` (boolean) - `provider_managed` (boolean) ### 3) `imap_search_messages` Purpose: search mailbox and return paginated message summaries. Input: - `account_id` (optional) - `mailbox` (required) - `cursor?` (string, opaque) - search criteria fields: - `query?` (1..256) - `from?` (1..256) - `to?` (1..256) - `subject?` (1..256) - `unread_only?` (boolean) - `last_days?` (1..365) - `start_date?` (`YYYY-MM-DD`) - `end_date?` (`YYYY-MM-DD`) - `limit` (optional) - `snippet_max_chars?` (50..500; when present, snippets are returned truncated to this length) Validation: - When `cursor` is present, pagination resumes the stored cursor snapshot and ignores replayed search criteria plus `snippet_max_chars`. - `last_days` cannot be combined with `start_date`/`end_date`. - `start_date <= end_date`. - Search text fields and mailbox values must not contain ASCII control characters. - Searches matching more than 1,000 messages are rejected; narrow filters and retry. Output `data`: - `status`: `ok|partial|failed` - `issues`: array of diagnostic issues - `next_action`: `{ instruction, tool, arguments }` - `account_id` - `mailbox` - `total` (integer) - `attempted` (integer) - `returned` (integer) - `failed` (integer) - `messages`: array (max 50) of: - `message_id` - `message_uri` - `message_raw_uri` - `mailbox` - `uidvalidity` - `uid` - `date?` - `from?` - `subject?` - `flags?` (string[]) - `snippet?` - `next_cursor?` (string) - `has_more` (boolean) ### 4) `imap_get_message` Purpose: return parsed message details with optional bounded enrichments. Input: - `message_id` (required) - `body_max_chars?` (1..16000, default 2000) - `body_mode?` (`text|html|both`, default `text`) - `include_headers?` (boolean, default true) - `include_all_headers?` (boolean, default false) - `attachment_mode?` (`none|metadata|extract_text`, default `metadata`) - `attachment_text_max_chars?` (1..64000, default 10000; only valid when `attachment_mode=extract_text`) Output `data`: - `status`: `ok|partial|failed` - `issues`: array of diagnostic issues - `account_id` - `message`: - `message_id` - `message_uri` - `message_raw_uri` - `mailbox` - `uidvalidity` - `uid` - `date?` - `from?` - `to?` - `cc?` - `subject?` - `flags?` - `headers?` (curated by default; full when requested) - `body_text?` (bounded; returned for `body_mode=text|both`; prefers `text/plain`, otherwise derived from sanitized HTML when no meaningful plain-text body exists) - `body_html?` (sanitized and bounded; returned for `body_mode=html|both`) - `attachments?`: array (max 50) of: - `filename?` - `content_type` - `size_bytes?` (complete decoded payload size when known; `null`/absent when unavailable or incomplete; unknown size is never represented as zero) - `part_id` - `extracted_text?` (bounded; only when `attachment_mode=extract_text`) PDF extraction rules: - only `application/pdf` - max attachment size for extraction: 5 MB - extraction failures do not fail the whole tool call - `attachment_mode=metadata` reports attachment metadata without attempting extraction - messages with more than 50 attachments return the first 50 plus a truncation issue Message processing is bounded by the server-wide fetch, decode, MIME-complexity, and attachment-extraction limits listed under [Environment Variables](#environment-variables). A conforming IMAP server is asked for at most the configured raw-message fetch budget. Oversized messages use that bounded raw prefix and return `partial` status with a fetch-budget issue. MIME depth or part-count overflow returns header-only metadata rather than recursively parsing the overflowing structure. Incomplete messages never extract attachment text, and an unknown or incomplete attachment `size_bytes` remains `null`/absent rather than zero. ### 5) `imap_get_message_raw` Purpose: return a bounded RFC822 byte range for diagnostics. Input: - `message_id` (required) - `max_bytes?` (1..64000, default 16000) - `offset_bytes?` (integer, default 0) Output `data`: - `status`: `ok|partial|failed` - `issues`: array of diagnostic issues - `account_id` - `message_id` - `message_uri` - `message_raw_uri` - `total_size_bytes` - `returned_bytes` - `offset_bytes` - `truncated` - `raw_source_base64` (byte-faithful RFC822 source, base64 encoded) - `raw_source_encoding` (`"base64"` on success) ### 6) `imap_apply_to_messages` Purpose: apply one mutation action to explicit messages. Write gate: requires `MAIL_IMAP_WRITE_ENABLED=true`. Input: - `message_ids` (required): string[] (`1..250`) - `action` (required): `move|copy|delete` - `destination_mailbox?` (required for `move` and `copy`, rejected for `delete`) Validation: - message discovery happens in read tools such as `imap_search_messages` - duplicate `message_ids` are deduplicated before execution - all message ids must belong to the same account inferred from their IDs - destination mailbox must already exist for `move` and `copy` - live mailbox `uidvalidity` is revalidated before the operation is accepted Output `data`: - `status`: `accepted|running|ok|partial|failed|canceled` - `issues`: array of diagnostic issues - `operation`: `{ operation_id, kind, state, done, cancel_supported, created_at, started_at?, finished_at?, progress }` - `result?`: final completed payload when `done=true` - `next_action?`: polling instruction for `imap_get_operation` when `done=false` ### 7) `imap_update_message_flags` Purpose: add, remove, or replace flags on explicit messages. Write gate: requires `MAIL_IMAP_WRITE_ENABLED=true`. Input: - `message_ids` (required): string[] (`1..250`) - `operation` (required): `add|remove|replace` - `flags` (required): string[] (`1..32`) Validation: - duplicate `message_ids` are deduplicated before execution - all message ids must belong to the same account inferred from their IDs - valid standard flags are `\Seen`, `\Answered`, `\Flagged`, `\Deleted`, `\Draft` - custom keywords may also be allowed if they are valid IMAP atoms and the server accepts them - invalid standard flags return a corrective hint listing the valid standard flags - live mailbox `uidvalidity` is revalidated before the operation is accepted Output `data`: - `status`: `accepted|running|ok|partial|failed|canceled` - `issues`: array of diagnostic issues - `operation`: `{ operation_id, kind, state, done, cancel_supported, created_at, started_at?, finished_at?, progress }` - `result?`: final completed payload when `done=true` - `next_action?`: polling instruction for `imap_get_operation` when `done=false` ### 8) `imap_manage_mailbox` Purpose: create, rename, or delete a mailbox. Write gate: requires `MAIL_IMAP_WRITE_ENABLED=true`. Input: - `account_id` (optional) - `action` (required): `create|rename|delete` - `mailbox` (required) - `destination_mailbox?` (required for `rename`, rejected for `create` and `delete`) Behavior: - `create` auto-creates missing parent mailboxes before the target mailbox - `rename` is the mailbox move primitive and auto-creates missing destination parents - `delete` is non-recursive and surfaces the server error for non-empty mailboxes or mailboxes with children Output `data`: - `status`: `accepted|running|ok|partial|failed|canceled` - `issues`: array of diagnostic issues - `operation`: `{ operation_id, kind, state, done, cancel_supported, created_at, started_at?, finished_at?, progress }` - `result?`: final completed payload when `done=true` - `next_action?`: polling instruction for `imap_get_operation` when `done=false` ### 9) `imap_get_operation` Purpose: poll a previously accepted write operation. Input: - `operation_id` (required) - `include_result?` (optional boolean, default `false`) Output `data`: - `status`: `accepted|running|ok|partial|failed|canceled` - `issues`: array of currently accumulated diagnostic issues - `operation`: `{ operation_id, kind, state, done, cancel_supported, created_at, started_at?, finished_at?, progress }` - `result?`: final completed payload when `done=true` and `include_result=true` - `next_action?`: polling instruction for `imap_get_operation` when `done=false`; if the operation is already complete and a result exists but `include_result=false`, `next_action` points to `imap_get_operation` with `include_result=true` ### 10) `imap_cancel_operation` Purpose: request cancellation for a running write operation. Input: - `operation_id` (required) Output `data`: - `status`: `accepted|running|ok|partial|failed|canceled` - `issues`: array of currently accumulated diagnostic issues - `operation`: `{ operation_id, kind, state, done, cancel_supported, created_at, started_at?, finished_at?, progress }` - `result?`: final completed payload when `done=true` - `next_action?`: polling instruction for `imap_get_operation` when `done=false` ## Security and Guardrails - Never return secrets (`*_PASS`, tokens, cookies, auth headers). - Redact secret-like keys in logs. - Enforce all bounds before IMAP fetch/download when possible. - Limit attachment bytes and text extraction output. - Use TLS certificate and hostname verification by default. - Reject ambiguous or conflicting inputs with explicit `invalid_input` errors. ## Environment Variables Per account: - `MAIL_IMAP__HOST` (required) - `MAIL_IMAP__PORT` (default `993`) - `MAIL_IMAP__SECURE` (default `true`) - `MAIL_IMAP__USER` (required) - `MAIL_IMAP__PASS` (required) Server-wide: - `MAIL_IMAP_WRITE_ENABLED` (default `false`) - `MAIL_IMAP_CA_CERT_PATH` (optional PEM CA bundle; adds trusted roots without disabling hostname verification) - `MAIL_IMAP_CONNECT_TIMEOUT_MS` (default `30000`) - `MAIL_IMAP_GREETING_TIMEOUT_MS` (default `15000`) - `MAIL_IMAP_SOCKET_TIMEOUT_MS` (default `300000`) - `MAIL_IMAP_READ_SESSION_CACHE_TTL_SECONDS` (default `120`) - `MAIL_IMAP_READ_SESSION_CACHE_MAX_PER_ACCOUNT` (default `4`; set `0` to disable read-session caching) - `MAIL_IMAP_OPERATION_MAX_ENTRIES` (default `256`; completed write operations retained in memory) - `MAIL_IMAP_MESSAGE_FETCH_BUDGET_BYTES` (default `8388608`; maximum bytes fetched while assembling one message; exhaustion returns bounded partial content and an issue) - `MAIL_IMAP_MESSAGE_DECODE_BUDGET_BYTES` (default `16777216`; maximum decoded message payload bytes; exhaustion returns bounded partial content and an issue) - `MAIL_IMAP_MIME_MAX_DEPTH` (default `32`; exceeding the MIME depth ceiling returns header-only metadata with a diagnostic issue) - `MAIL_IMAP_MIME_MAX_PARTS` (default `250`; exceeding the MIME part-count ceiling returns header-only metadata with a diagnostic issue) - `MAIL_IMAP_ATTACHMENT_EXTRACT_BUDGET_BYTES` (default `10485760`; maximum complete decoded attachment payload bytes processed for text extraction per message; attachments that do not fit remain metadata-only and are reported with an issue) ## Implementation Notes for Next Artifact Next artifact will generate Rust types and schema definitions from this contract, then register all tools in an `rmcp` server skeleton with unified error handling across stdio and optional streamable HTTP transport.