@multimail/mcp-server 0.1.1 → 0.1.3
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/dist/index.js +18 -9
- package/package.json +19 -3
- package/src/index.ts +0 -184
- package/tsconfig.json +0 -16
package/dist/index.js
CHANGED
|
@@ -11,6 +11,15 @@ if (!API_KEY) {
|
|
|
11
11
|
process.exit(1);
|
|
12
12
|
}
|
|
13
13
|
// --- API Client ---
|
|
14
|
+
async function parseResponse(res) {
|
|
15
|
+
const text = await res.text();
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(text);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error(`API returned non-JSON response (${res.status}): ${text.slice(0, 200)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
async function apiCall(method, path, body) {
|
|
15
24
|
const url = `${BASE_URL}${path}`;
|
|
16
25
|
const headers = {
|
|
@@ -22,7 +31,7 @@ async function apiCall(method, path, body) {
|
|
|
22
31
|
headers,
|
|
23
32
|
body: body ? JSON.stringify(body) : undefined,
|
|
24
33
|
});
|
|
25
|
-
const data = await res
|
|
34
|
+
const data = await parseResponse(res);
|
|
26
35
|
if (!res.ok) {
|
|
27
36
|
if (res.status === 401) {
|
|
28
37
|
throw new Error("Invalid API key. Check MULTIMAIL_API_KEY environment variable.");
|
|
@@ -34,16 +43,16 @@ async function apiCall(method, path, body) {
|
|
|
34
43
|
const retryAfter = res.headers.get("retry-after") || "unknown";
|
|
35
44
|
throw new Error(`Rate limit exceeded. Retry after ${retryAfter} seconds.`);
|
|
36
45
|
}
|
|
37
|
-
throw new Error(`API error ${res.status}: ${JSON.stringify(data)}`);
|
|
46
|
+
throw new Error(`API error ${res.status}: ${data.error || JSON.stringify(data)}`);
|
|
38
47
|
}
|
|
39
48
|
return data;
|
|
40
49
|
}
|
|
41
50
|
async function publicFetch(path) {
|
|
42
51
|
const url = `${BASE_URL}${path}`;
|
|
43
52
|
const res = await fetch(url, { headers: { "Accept": "application/json" } });
|
|
44
|
-
const data = await res
|
|
53
|
+
const data = await parseResponse(res);
|
|
45
54
|
if (!res.ok) {
|
|
46
|
-
throw new Error(`API error ${res.status}: ${JSON.stringify(data)}`);
|
|
55
|
+
throw new Error(`API error ${res.status}: ${data.error || JSON.stringify(data)}`);
|
|
47
56
|
}
|
|
48
57
|
return data;
|
|
49
58
|
}
|
|
@@ -59,7 +68,7 @@ function getMailboxId(argsMailboxId) {
|
|
|
59
68
|
// --- Server ---
|
|
60
69
|
const server = new McpServer({
|
|
61
70
|
name: "multimail",
|
|
62
|
-
version: "0.1.
|
|
71
|
+
version: "0.1.3",
|
|
63
72
|
});
|
|
64
73
|
// Tool 1: list_mailboxes
|
|
65
74
|
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 () => {
|
|
@@ -68,10 +77,10 @@ server.tool("list_mailboxes", "List all mailboxes available to this API key. Ret
|
|
|
68
77
|
});
|
|
69
78
|
// Tool 2: send_email
|
|
70
79
|
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"),
|
|
80
|
+
to: z.array(z.string().email()).describe("Recipient email addresses"),
|
|
72
81
|
subject: z.string().describe("Email subject line"),
|
|
73
82
|
markdown: z.string().describe("Email body in markdown format"),
|
|
74
|
-
cc: z.array(z.string()).optional().describe("CC email addresses"),
|
|
83
|
+
cc: z.array(z.string().email()).optional().describe("CC email addresses"),
|
|
75
84
|
mailbox_id: z.string().optional().describe("Mailbox ID (uses MULTIMAIL_MAILBOX_ID env var if not provided)"),
|
|
76
85
|
}, async ({ to, subject, markdown, cc, mailbox_id }) => {
|
|
77
86
|
const id = getMailboxId(mailbox_id);
|
|
@@ -104,7 +113,7 @@ server.tool("read_email", "Get the full content of a specific email, including t
|
|
|
104
113
|
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
114
|
email_id: z.string().describe("The email ID to reply to"),
|
|
106
115
|
markdown: z.string().describe("Reply body in markdown format"),
|
|
107
|
-
cc: z.array(z.string()).optional().describe("CC email addresses"),
|
|
116
|
+
cc: z.array(z.string().email()).optional().describe("CC email addresses"),
|
|
108
117
|
mailbox_id: z.string().optional().describe("Mailbox ID (uses MULTIMAIL_MAILBOX_ID env var if not provided)"),
|
|
109
118
|
}, async ({ email_id, markdown, cc, mailbox_id }) => {
|
|
110
119
|
const id = getMailboxId(mailbox_id);
|
|
@@ -116,7 +125,7 @@ server.tool("reply_email", "Reply to an email in its existing thread. Threading
|
|
|
116
125
|
});
|
|
117
126
|
// Tool 6: search_identity
|
|
118
127
|
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)"),
|
|
128
|
+
address: z.string().email().describe("The email address to look up (e.g. sandy@multimail.dev)"),
|
|
120
129
|
}, async ({ address }) => {
|
|
121
130
|
const data = await publicFetch(`/.well-known/agent/${encodeURIComponent(address)}`);
|
|
122
131
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
package/package.json
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multimail/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "MCP server for MultiMail — email for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"multimail-mcp": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
10
15
|
"scripts": {
|
|
11
16
|
"build": "tsc",
|
|
12
17
|
"start": "node dist/index.js",
|
|
13
|
-
"dev": "tsx src/index.ts"
|
|
18
|
+
"dev": "tsx src/index.ts",
|
|
19
|
+
"prepublishOnly": "tsc"
|
|
14
20
|
},
|
|
15
|
-
"keywords": [
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"email",
|
|
24
|
+
"ai-agents",
|
|
25
|
+
"multimail",
|
|
26
|
+
"model-context-protocol"
|
|
27
|
+
],
|
|
16
28
|
"license": "MIT",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
},
|
|
17
32
|
"author": {
|
|
18
33
|
"name": "MultiMail",
|
|
19
34
|
"email": "dev@multimail.dev",
|
|
@@ -23,6 +38,7 @@
|
|
|
23
38
|
"type": "git",
|
|
24
39
|
"url": "https://github.com/multimail-dev/mcp-server.git"
|
|
25
40
|
},
|
|
41
|
+
"mcpName": "io.github.multimail-dev/mcp-server",
|
|
26
42
|
"homepage": "https://multimail.dev",
|
|
27
43
|
"bugs": {
|
|
28
44
|
"email": "dev@multimail.dev"
|
package/src/index.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
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
|
-
}
|