@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 +89 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +132 -0
- package/package.json +40 -0
- package/src/index.ts +184 -0
- package/tsconfig.json +16 -0
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
|
package/dist/index.d.ts
ADDED
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
|
+
}
|