@multimail/mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @multimail/mcp-server
2
+
3
+ MCP server for [MultiMail](https://multimail.dev). Give any AI agent email capabilities through the Model Context Protocol.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npx @multimail/mcp-server
9
+ ```
10
+
11
+ Requires `MULTIMAIL_API_KEY` environment variable. Get one at [multimail.dev](https://multimail.dev).
12
+
13
+ ## Setup
14
+
15
+ Any MCP-compatible client uses the same config. Add MultiMail to your client's MCP configuration:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "multimail": {
21
+ "command": "npx",
22
+ "args": ["-y", "@multimail/mcp-server"],
23
+ "env": {
24
+ "MULTIMAIL_API_KEY": "mm_live_...",
25
+ "MULTIMAIL_MAILBOX_ID": "01KJ1NHN8J..."
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ### Where to add this
33
+
34
+ | Client | Config file |
35
+ |--------|------------|
36
+ | Claude Code | `~/.claude/.mcp.json` |
37
+ | Claude Desktop | `claude_desktop_config.json` |
38
+ | Cursor | `.cursor/mcp.json` in your project |
39
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
40
+ | Copilot (VS Code) | `.vscode/mcp.json` in your project |
41
+ | OpenCode | `mcp.json` in your project |
42
+ | ChatGPT Desktop | Settings > MCP Servers |
43
+ | Any MCP client | Consult your client's docs for config location |
44
+
45
+ ## Environment variables
46
+
47
+ | Variable | Required | Description |
48
+ |----------|----------|-------------|
49
+ | `MULTIMAIL_API_KEY` | Yes | Your MultiMail API key (`mm_live_...`) |
50
+ | `MULTIMAIL_MAILBOX_ID` | No | Default mailbox ID. If not set, pass `mailbox_id` to each tool or call `list_mailboxes` first. |
51
+ | `MULTIMAIL_API_URL` | No | API base URL. Defaults to `https://api.multimail.dev`. |
52
+
53
+ ## Tools
54
+
55
+ | Tool | Description |
56
+ |------|-------------|
57
+ | `list_mailboxes` | List all mailboxes available to this API key |
58
+ | `send_email` | Send an email with a markdown body |
59
+ | `check_inbox` | List emails (filterable by unread/read/archived) |
60
+ | `read_email` | Get the full content of a specific email |
61
+ | `reply_email` | Reply to an email in its existing thread |
62
+ | `search_identity` | Look up the public identity of any MultiMail address |
63
+
64
+ ## How it works
65
+
66
+ - You write email bodies in **markdown**. MultiMail converts to formatted HTML for delivery.
67
+ - Incoming email arrives as **clean markdown**. No HTML parsing or MIME decoding.
68
+ - Threading is automatic. Reply to an email and headers are set correctly.
69
+ - If your mailbox uses gated oversight, sends return `pending_approval` status. Do not retry.
70
+ - Verify other agents before communicating using `search_identity`.
71
+
72
+ ## Development
73
+
74
+ ```bash
75
+ npm install
76
+ npm run dev # Run with tsx (no build needed)
77
+ npm run build # Compile TypeScript
78
+ npm start # Run compiled version
79
+ ```
80
+
81
+ ## Testing
82
+
83
+ ```bash
84
+ echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | MULTIMAIL_API_KEY=mm_live_... node dist/index.js
85
+ ```
86
+
87
+ ## License
88
+
89
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ // --- Config ---
6
+ const API_KEY = process.env.MULTIMAIL_API_KEY;
7
+ const DEFAULT_MAILBOX_ID = process.env.MULTIMAIL_MAILBOX_ID;
8
+ const BASE_URL = (process.env.MULTIMAIL_API_URL || "https://api.multimail.dev").replace(/\/$/, "");
9
+ if (!API_KEY) {
10
+ console.error("MULTIMAIL_API_KEY environment variable is required.");
11
+ process.exit(1);
12
+ }
13
+ // --- API Client ---
14
+ async function apiCall(method, path, body) {
15
+ const url = `${BASE_URL}${path}`;
16
+ const headers = {
17
+ "Authorization": `Bearer ${API_KEY}`,
18
+ "Content-Type": "application/json",
19
+ };
20
+ const res = await fetch(url, {
21
+ method,
22
+ headers,
23
+ body: body ? JSON.stringify(body) : undefined,
24
+ });
25
+ const data = await res.json();
26
+ if (!res.ok) {
27
+ if (res.status === 401) {
28
+ throw new Error("Invalid API key. Check MULTIMAIL_API_KEY environment variable.");
29
+ }
30
+ if (res.status === 403) {
31
+ throw new Error(`API key lacks required scope for this operation. ${data.error || ""}`);
32
+ }
33
+ if (res.status === 429) {
34
+ const retryAfter = res.headers.get("retry-after") || "unknown";
35
+ throw new Error(`Rate limit exceeded. Retry after ${retryAfter} seconds.`);
36
+ }
37
+ throw new Error(`API error ${res.status}: ${JSON.stringify(data)}`);
38
+ }
39
+ return data;
40
+ }
41
+ async function publicFetch(path) {
42
+ const url = `${BASE_URL}${path}`;
43
+ const res = await fetch(url, { headers: { "Accept": "application/json" } });
44
+ const data = await res.json();
45
+ if (!res.ok) {
46
+ throw new Error(`API error ${res.status}: ${JSON.stringify(data)}`);
47
+ }
48
+ return data;
49
+ }
50
+ function getMailboxId(argsMailboxId) {
51
+ const id = argsMailboxId || DEFAULT_MAILBOX_ID;
52
+ if (!id) {
53
+ throw new Error("No mailbox_id provided and MULTIMAIL_MAILBOX_ID is not set. " +
54
+ "Either pass mailbox_id or set the MULTIMAIL_MAILBOX_ID environment variable. " +
55
+ "Use list_mailboxes to discover available mailboxes.");
56
+ }
57
+ return id;
58
+ }
59
+ // --- Server ---
60
+ const server = new McpServer({
61
+ name: "multimail",
62
+ version: "0.1.0",
63
+ });
64
+ // Tool 1: list_mailboxes
65
+ server.tool("list_mailboxes", "List all mailboxes available to this API key. Returns each mailbox's ID, email address, oversight mode, and display name. Use this to discover your mailbox ID if MULTIMAIL_MAILBOX_ID is not set.", {}, async () => {
66
+ const data = await apiCall("GET", "/v1/mailboxes");
67
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
68
+ });
69
+ // Tool 2: send_email
70
+ server.tool("send_email", "Send an email from your MultiMail address. The body is written in markdown and automatically converted to formatted HTML for delivery. If the mailbox uses gated oversight, the response status will be 'pending_approval' — this means the email is queued for human review. Do not retry or resend when you see pending_approval.", {
71
+ to: z.array(z.string()).describe("Recipient email addresses"),
72
+ subject: z.string().describe("Email subject line"),
73
+ markdown: z.string().describe("Email body in markdown format"),
74
+ cc: z.array(z.string()).optional().describe("CC email addresses"),
75
+ mailbox_id: z.string().optional().describe("Mailbox ID (uses MULTIMAIL_MAILBOX_ID env var if not provided)"),
76
+ }, async ({ to, subject, markdown, cc, mailbox_id }) => {
77
+ const id = getMailboxId(mailbox_id);
78
+ const body = { to, subject, markdown };
79
+ if (cc?.length)
80
+ body.cc = cc;
81
+ const data = await apiCall("POST", `/v1/mailboxes/${encodeURIComponent(id)}/send`, body);
82
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
83
+ });
84
+ // Tool 3: check_inbox
85
+ server.tool("check_inbox", "List emails in your inbox. Returns email summaries including id, from, to, subject, status, received_at, and has_attachments. Does NOT include the email body — call read_email with the email ID to get the full message content.", {
86
+ status: z.enum(["unread", "read", "archived"]).optional().describe("Filter by email status (default: all)"),
87
+ mailbox_id: z.string().optional().describe("Mailbox ID (uses MULTIMAIL_MAILBOX_ID env var if not provided)"),
88
+ }, async ({ status, mailbox_id }) => {
89
+ const id = getMailboxId(mailbox_id);
90
+ const query = status ? `?status=${status}` : "";
91
+ const data = await apiCall("GET", `/v1/mailboxes/${encodeURIComponent(id)}/emails${query}`);
92
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
93
+ });
94
+ // Tool 4: read_email
95
+ server.tool("read_email", "Get the full content of a specific email, including the markdown body and attachment metadata. Automatically marks unread emails as read. Use the email ID from check_inbox results.", {
96
+ email_id: z.string().describe("The email ID to read"),
97
+ mailbox_id: z.string().optional().describe("Mailbox ID (uses MULTIMAIL_MAILBOX_ID env var if not provided)"),
98
+ }, async ({ email_id, mailbox_id }) => {
99
+ const id = getMailboxId(mailbox_id);
100
+ const data = await apiCall("GET", `/v1/mailboxes/${encodeURIComponent(id)}/emails/${encodeURIComponent(email_id)}`);
101
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
102
+ });
103
+ // Tool 5: reply_email
104
+ server.tool("reply_email", "Reply to an email in its existing thread. Threading headers (In-Reply-To, References) are set automatically. The body is written in markdown. If the mailbox uses gated oversight, the response status will be 'pending_approval' — the reply is queued for human review. Do not retry or resend when you see pending_approval.", {
105
+ email_id: z.string().describe("The email ID to reply to"),
106
+ markdown: z.string().describe("Reply body in markdown format"),
107
+ cc: z.array(z.string()).optional().describe("CC email addresses"),
108
+ mailbox_id: z.string().optional().describe("Mailbox ID (uses MULTIMAIL_MAILBOX_ID env var if not provided)"),
109
+ }, async ({ email_id, markdown, cc, mailbox_id }) => {
110
+ const id = getMailboxId(mailbox_id);
111
+ const body = { markdown };
112
+ if (cc?.length)
113
+ body.cc = cc;
114
+ const data = await apiCall("POST", `/v1/mailboxes/${encodeURIComponent(id)}/reply/${encodeURIComponent(email_id)}`, body);
115
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
116
+ });
117
+ // Tool 6: search_identity
118
+ server.tool("search_identity", "Look up the public identity document for any MultiMail email address. Returns the agent's operator, oversight mode, capabilities, and whether the operator is verified. No authentication required. Use this to verify another agent's identity before sending sensitive information.", {
119
+ address: z.string().describe("The email address to look up (e.g. sandy@multimail.dev)"),
120
+ }, async ({ address }) => {
121
+ const data = await publicFetch(`/.well-known/agent/${encodeURIComponent(address)}`);
122
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
123
+ });
124
+ // --- Start ---
125
+ async function main() {
126
+ const transport = new StdioServerTransport();
127
+ await server.connect(transport);
128
+ }
129
+ main().catch((err) => {
130
+ console.error("Failed to start MCP server:", err);
131
+ process.exit(1);
132
+ });
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@multimail/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for MultiMail — email for AI agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "multimail-mcp": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "tsx src/index.ts"
14
+ },
15
+ "keywords": ["mcp", "email", "ai-agents", "multimail", "model-context-protocol"],
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "MultiMail",
19
+ "email": "dev@multimail.dev",
20
+ "url": "https://multimail.dev"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/H179922/multimail.git",
25
+ "directory": "mcp"
26
+ },
27
+ "homepage": "https://multimail.dev",
28
+ "bugs": {
29
+ "email": "dev@multimail.dev"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.12.1",
33
+ "zod": "^3.24.2"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.15.0",
37
+ "tsx": "^4.19.0",
38
+ "typescript": "^5.9.3"
39
+ }
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+
6
+ // --- Config ---
7
+
8
+ const API_KEY = process.env.MULTIMAIL_API_KEY;
9
+ const DEFAULT_MAILBOX_ID = process.env.MULTIMAIL_MAILBOX_ID;
10
+ const BASE_URL = (process.env.MULTIMAIL_API_URL || "https://api.multimail.dev").replace(/\/$/, "");
11
+
12
+ if (!API_KEY) {
13
+ console.error("MULTIMAIL_API_KEY environment variable is required.");
14
+ process.exit(1);
15
+ }
16
+
17
+ // --- API Client ---
18
+
19
+ async function apiCall(method: string, path: string, body?: unknown): Promise<unknown> {
20
+ const url = `${BASE_URL}${path}`;
21
+ const headers: Record<string, string> = {
22
+ "Authorization": `Bearer ${API_KEY}`,
23
+ "Content-Type": "application/json",
24
+ };
25
+
26
+ const res = await fetch(url, {
27
+ method,
28
+ headers,
29
+ body: body ? JSON.stringify(body) : undefined,
30
+ });
31
+
32
+ const data = await res.json() as Record<string, unknown>;
33
+
34
+ if (!res.ok) {
35
+ if (res.status === 401) {
36
+ throw new Error("Invalid API key. Check MULTIMAIL_API_KEY environment variable.");
37
+ }
38
+ if (res.status === 403) {
39
+ throw new Error(`API key lacks required scope for this operation. ${(data as Record<string, unknown>).error || ""}`);
40
+ }
41
+ if (res.status === 429) {
42
+ const retryAfter = res.headers.get("retry-after") || "unknown";
43
+ throw new Error(`Rate limit exceeded. Retry after ${retryAfter} seconds.`);
44
+ }
45
+ throw new Error(`API error ${res.status}: ${JSON.stringify(data)}`);
46
+ }
47
+
48
+ return data;
49
+ }
50
+
51
+ async function publicFetch(path: string): Promise<unknown> {
52
+ const url = `${BASE_URL}${path}`;
53
+ const res = await fetch(url, { headers: { "Accept": "application/json" } });
54
+ const data = await res.json();
55
+ if (!res.ok) {
56
+ throw new Error(`API error ${res.status}: ${JSON.stringify(data)}`);
57
+ }
58
+ return data;
59
+ }
60
+
61
+ function getMailboxId(argsMailboxId?: string): string {
62
+ const id = argsMailboxId || DEFAULT_MAILBOX_ID;
63
+ if (!id) {
64
+ throw new Error(
65
+ "No mailbox_id provided and MULTIMAIL_MAILBOX_ID is not set. " +
66
+ "Either pass mailbox_id or set the MULTIMAIL_MAILBOX_ID environment variable. " +
67
+ "Use list_mailboxes to discover available mailboxes."
68
+ );
69
+ }
70
+ return id;
71
+ }
72
+
73
+ // --- Server ---
74
+
75
+ const server = new McpServer({
76
+ name: "multimail",
77
+ version: "0.1.0",
78
+ });
79
+
80
+ // Tool 1: list_mailboxes
81
+ server.tool(
82
+ "list_mailboxes",
83
+ "List all mailboxes available to this API key. Returns each mailbox's ID, email address, oversight mode, and display name. Use this to discover your mailbox ID if MULTIMAIL_MAILBOX_ID is not set.",
84
+ {},
85
+ async () => {
86
+ const data = await apiCall("GET", "/v1/mailboxes");
87
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
88
+ }
89
+ );
90
+
91
+ // Tool 2: send_email
92
+ server.tool(
93
+ "send_email",
94
+ "Send an email from your MultiMail address. The body is written in markdown and automatically converted to formatted HTML for delivery. If the mailbox uses gated oversight, the response status will be 'pending_approval' — this means the email is queued for human review. Do not retry or resend when you see pending_approval.",
95
+ {
96
+ to: z.array(z.string()).describe("Recipient email addresses"),
97
+ subject: z.string().describe("Email subject line"),
98
+ markdown: z.string().describe("Email body in markdown format"),
99
+ cc: z.array(z.string()).optional().describe("CC email addresses"),
100
+ mailbox_id: z.string().optional().describe("Mailbox ID (uses MULTIMAIL_MAILBOX_ID env var if not provided)"),
101
+ },
102
+ async ({ to, subject, markdown, cc, mailbox_id }) => {
103
+ const id = getMailboxId(mailbox_id);
104
+ const body: Record<string, unknown> = { to, subject, markdown };
105
+ if (cc?.length) body.cc = cc;
106
+ const data = await apiCall("POST", `/v1/mailboxes/${encodeURIComponent(id)}/send`, body);
107
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
108
+ }
109
+ );
110
+
111
+ // Tool 3: check_inbox
112
+ server.tool(
113
+ "check_inbox",
114
+ "List emails in your inbox. Returns email summaries including id, from, to, subject, status, received_at, and has_attachments. Does NOT include the email body — call read_email with the email ID to get the full message content.",
115
+ {
116
+ status: z.enum(["unread", "read", "archived"]).optional().describe("Filter by email status (default: all)"),
117
+ mailbox_id: z.string().optional().describe("Mailbox ID (uses MULTIMAIL_MAILBOX_ID env var if not provided)"),
118
+ },
119
+ async ({ status, mailbox_id }) => {
120
+ const id = getMailboxId(mailbox_id);
121
+ const query = status ? `?status=${status}` : "";
122
+ const data = await apiCall("GET", `/v1/mailboxes/${encodeURIComponent(id)}/emails${query}`);
123
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
124
+ }
125
+ );
126
+
127
+ // Tool 4: read_email
128
+ server.tool(
129
+ "read_email",
130
+ "Get the full content of a specific email, including the markdown body and attachment metadata. Automatically marks unread emails as read. Use the email ID from check_inbox results.",
131
+ {
132
+ email_id: z.string().describe("The email ID to read"),
133
+ mailbox_id: z.string().optional().describe("Mailbox ID (uses MULTIMAIL_MAILBOX_ID env var if not provided)"),
134
+ },
135
+ async ({ email_id, mailbox_id }) => {
136
+ const id = getMailboxId(mailbox_id);
137
+ const data = await apiCall("GET", `/v1/mailboxes/${encodeURIComponent(id)}/emails/${encodeURIComponent(email_id)}`);
138
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
139
+ }
140
+ );
141
+
142
+ // Tool 5: reply_email
143
+ server.tool(
144
+ "reply_email",
145
+ "Reply to an email in its existing thread. Threading headers (In-Reply-To, References) are set automatically. The body is written in markdown. If the mailbox uses gated oversight, the response status will be 'pending_approval' — the reply is queued for human review. Do not retry or resend when you see pending_approval.",
146
+ {
147
+ email_id: z.string().describe("The email ID to reply to"),
148
+ markdown: z.string().describe("Reply body in markdown format"),
149
+ cc: z.array(z.string()).optional().describe("CC email addresses"),
150
+ mailbox_id: z.string().optional().describe("Mailbox ID (uses MULTIMAIL_MAILBOX_ID env var if not provided)"),
151
+ },
152
+ async ({ email_id, markdown, cc, mailbox_id }) => {
153
+ const id = getMailboxId(mailbox_id);
154
+ const body: Record<string, unknown> = { markdown };
155
+ if (cc?.length) body.cc = cc;
156
+ const data = await apiCall("POST", `/v1/mailboxes/${encodeURIComponent(id)}/reply/${encodeURIComponent(email_id)}`, body);
157
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
158
+ }
159
+ );
160
+
161
+ // Tool 6: search_identity
162
+ server.tool(
163
+ "search_identity",
164
+ "Look up the public identity document for any MultiMail email address. Returns the agent's operator, oversight mode, capabilities, and whether the operator is verified. No authentication required. Use this to verify another agent's identity before sending sensitive information.",
165
+ {
166
+ address: z.string().describe("The email address to look up (e.g. sandy@multimail.dev)"),
167
+ },
168
+ async ({ address }) => {
169
+ const data = await publicFetch(`/.well-known/agent/${encodeURIComponent(address)}`);
170
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
171
+ }
172
+ );
173
+
174
+ // --- Start ---
175
+
176
+ async function main() {
177
+ const transport = new StdioServerTransport();
178
+ await server.connect(transport);
179
+ }
180
+
181
+ main().catch((err) => {
182
+ console.error("Failed to start MCP server:", err);
183
+ process.exit(1);
184
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "declaration": true
14
+ },
15
+ "include": ["src/**/*.ts"]
16
+ }