@ractermx/mcp-server 2.0.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 +95 -0
- package/package.json +45 -0
- package/src/index.js +469 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @ractermx/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for managing RacterMX email forwarding via AI assistants.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Set environment variables:
|
|
14
|
+
|
|
15
|
+
- `RACTERMX_API_KEY` (required) — your `sk_*` API key from RacterMX
|
|
16
|
+
- `RACTERMX_API_URL` (optional) — defaults to `https://ractermx.com`
|
|
17
|
+
|
|
18
|
+
## Usage with Kiro / Claude Desktop
|
|
19
|
+
|
|
20
|
+
Add to your MCP config:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"ractermx": {
|
|
26
|
+
"command": "node",
|
|
27
|
+
"args": ["/path/to/ractermx-mcp/src/index.js"],
|
|
28
|
+
"env": {
|
|
29
|
+
"RACTERMX_API_KEY": "sk_your_key_here"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Available Tools (37 total)
|
|
37
|
+
|
|
38
|
+
All tools target the V2 API (`/api/v2`).
|
|
39
|
+
|
|
40
|
+
### Domains (8)
|
|
41
|
+
- `list_domains` — List all forwarding domains
|
|
42
|
+
- `get_domain` — Get domain details
|
|
43
|
+
- `add_domain` — Add a new domain
|
|
44
|
+
- `update_domain` — Update domain settings (active, catch-all, max aliases)
|
|
45
|
+
- `delete_domain` — Remove a domain
|
|
46
|
+
- `verify_domain_dns` — Trigger DNS verification
|
|
47
|
+
- `get_domain_dns_records` — Get required DNS records
|
|
48
|
+
- `get_domain_statistics` — Get email stats
|
|
49
|
+
|
|
50
|
+
### Aliases (5)
|
|
51
|
+
- `list_aliases` — List aliases for a domain
|
|
52
|
+
- `get_alias` — Get alias details
|
|
53
|
+
- `create_alias` — Create a new alias
|
|
54
|
+
- `update_alias` — Update an alias
|
|
55
|
+
- `delete_alias` — Delete an alias
|
|
56
|
+
|
|
57
|
+
### Email Logs (2)
|
|
58
|
+
- `list_email_logs` — Search email logs with filters
|
|
59
|
+
- `get_email_log` — Get a specific log entry
|
|
60
|
+
|
|
61
|
+
### Email Sending (1)
|
|
62
|
+
- `send_email` — Send an email
|
|
63
|
+
|
|
64
|
+
### Webhooks (7)
|
|
65
|
+
- `list_webhooks` — List webhook endpoints
|
|
66
|
+
- `create_webhook` — Create a webhook
|
|
67
|
+
- `update_webhook` — Update a webhook
|
|
68
|
+
- `delete_webhook` — Delete a webhook
|
|
69
|
+
- `test_webhook` — Send a test event
|
|
70
|
+
- `list_webhook_delivery_logs` — View delivery history
|
|
71
|
+
- `retry_webhook_delivery` — Retry a failed delivery
|
|
72
|
+
|
|
73
|
+
### Blocklist (3)
|
|
74
|
+
- `list_blocklist` — List blocked senders
|
|
75
|
+
- `add_blocklist_entry` — Block a sender/pattern
|
|
76
|
+
- `remove_blocklist_entry` — Unblock a sender
|
|
77
|
+
|
|
78
|
+
### API Keys (3)
|
|
79
|
+
- `list_api_keys` — List active keys
|
|
80
|
+
- `create_api_key` — Create a new key
|
|
81
|
+
- `revoke_api_key` — Revoke a key
|
|
82
|
+
|
|
83
|
+
### SMTP Credentials (4)
|
|
84
|
+
- `list_smtp_credentials` — List SMTP credentials for a domain
|
|
85
|
+
- `create_smtp_credential` — Create SMTP credentials
|
|
86
|
+
- `delete_smtp_credential` — Delete SMTP credentials
|
|
87
|
+
- `reset_smtp_password` — Reset SMTP credential password
|
|
88
|
+
|
|
89
|
+
### Retention Policy (2)
|
|
90
|
+
- `get_retention_policy` — View retention settings
|
|
91
|
+
- `update_retention_policy` — Update retention (metadata days, content days, per-event overrides)
|
|
92
|
+
|
|
93
|
+
### Anonymous Replies (2)
|
|
94
|
+
- `list_anonymous_replies` — List anonymous reply proxies
|
|
95
|
+
- `disable_anonymous_reply` — Disable a proxy address
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ractermx/mcp-server",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "MCP server for managing RacterMX email forwarding via AI assistants",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ractermx-mcp": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node src/index.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
19
|
+
"zod": "^3.23.0"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"email",
|
|
25
|
+
"email-forwarding",
|
|
26
|
+
"ractermx",
|
|
27
|
+
"privacy",
|
|
28
|
+
"ai-agent",
|
|
29
|
+
"claude",
|
|
30
|
+
"llm"
|
|
31
|
+
],
|
|
32
|
+
"author": "Racter Holdings",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"homepage": "https://ractermx.com/developers",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/racter-ciso/ractermx-mcp.git"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
const API_BASE = process.env.RACTERMX_API_URL || "https://ractermx.com";
|
|
8
|
+
const API_KEY = process.env.RACTERMX_API_KEY;
|
|
9
|
+
|
|
10
|
+
if (!API_KEY) {
|
|
11
|
+
console.error("RACTERMX_API_KEY environment variable is required");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ── HTTP helpers ──
|
|
16
|
+
|
|
17
|
+
async function api(method, path, body = null) {
|
|
18
|
+
const url = `${API_BASE}${path}`;
|
|
19
|
+
const opts = {
|
|
20
|
+
method,
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
23
|
+
Accept: "application/json",
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
if (body) opts.body = JSON.stringify(body);
|
|
28
|
+
|
|
29
|
+
const res = await fetch(url, opts);
|
|
30
|
+
const text = await res.text();
|
|
31
|
+
|
|
32
|
+
let data;
|
|
33
|
+
try {
|
|
34
|
+
data = JSON.parse(text);
|
|
35
|
+
} catch {
|
|
36
|
+
data = { raw: text };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`API ${method} ${path} → ${res.status}: ${JSON.stringify(data)}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function v2(method, path, body) {
|
|
48
|
+
return api(method, `/api/v2${path}`, body);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function result(data) {
|
|
52
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Server ──
|
|
56
|
+
|
|
57
|
+
const server = new McpServer({
|
|
58
|
+
name: "ractermx",
|
|
59
|
+
version: "2.0.0",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
63
|
+
// Domain tools
|
|
64
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
65
|
+
|
|
66
|
+
server.registerTool("list_domains", {
|
|
67
|
+
description: "List all email forwarding domains in your account",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
per_page: z.number().optional().describe("Results per page (default 15)"),
|
|
70
|
+
},
|
|
71
|
+
annotations: { readOnlyHint: true },
|
|
72
|
+
}, async ({ per_page }) => {
|
|
73
|
+
const qs = per_page ? `?per_page=${per_page}` : "";
|
|
74
|
+
return result(await v2("GET", `/domains${qs}`));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
server.registerTool("get_domain", {
|
|
78
|
+
description: "Get details of a specific domain including DNS records and statistics",
|
|
79
|
+
inputSchema: { domain_id: z.number().describe("Domain ID") },
|
|
80
|
+
annotations: { readOnlyHint: true },
|
|
81
|
+
}, async ({ domain_id }) => result(await v2("GET", `/domains/${domain_id}`)));
|
|
82
|
+
|
|
83
|
+
server.registerTool("add_domain", {
|
|
84
|
+
description: "Add a new forwarding domain to your account",
|
|
85
|
+
inputSchema: {
|
|
86
|
+
name: z.string().describe("Domain name (e.g. example.com)"),
|
|
87
|
+
catch_all_enabled: z.boolean().optional().describe("Enable catch-all forwarding"),
|
|
88
|
+
catch_all_forward_to: z.string().optional().describe("Email to forward catch-all mail to"),
|
|
89
|
+
max_aliases: z.number().optional().describe("Maximum aliases allowed (1-1000, default 100)"),
|
|
90
|
+
},
|
|
91
|
+
}, async ({ name, catch_all_enabled, catch_all_forward_to, max_aliases }) => {
|
|
92
|
+
const body = { name };
|
|
93
|
+
if (catch_all_enabled !== undefined) body.catch_all_enabled = catch_all_enabled;
|
|
94
|
+
if (catch_all_forward_to) body.catch_all_forward_to = catch_all_forward_to;
|
|
95
|
+
if (max_aliases !== undefined) body.max_aliases = max_aliases;
|
|
96
|
+
return result(await v2("POST", "/domains", body));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
server.registerTool("update_domain", {
|
|
100
|
+
description: "Update a domain's settings (active status, catch-all, max aliases)",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
domain_id: z.number().describe("Domain ID"),
|
|
103
|
+
is_active: z.boolean().optional().describe("Enable or disable the domain"),
|
|
104
|
+
catch_all_enabled: z.boolean().optional().describe("Enable or disable catch-all"),
|
|
105
|
+
catch_all_forward_to: z.string().optional().describe("Catch-all forward address"),
|
|
106
|
+
max_aliases: z.number().optional().describe("Maximum aliases allowed (1-1000)"),
|
|
107
|
+
},
|
|
108
|
+
}, async ({ domain_id, ...updates }) => {
|
|
109
|
+
const body = {};
|
|
110
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
111
|
+
if (v !== undefined) body[k] = v;
|
|
112
|
+
}
|
|
113
|
+
return result(await v2("PATCH", `/domains/${domain_id}`, body));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
server.registerTool("delete_domain", {
|
|
117
|
+
description: "Remove a domain from your account",
|
|
118
|
+
inputSchema: { domain_id: z.number().describe("Domain ID") },
|
|
119
|
+
annotations: { destructiveHint: true },
|
|
120
|
+
}, async ({ domain_id }) => result(await v2("DELETE", `/domains/${domain_id}`)));
|
|
121
|
+
|
|
122
|
+
server.registerTool("verify_domain_dns", {
|
|
123
|
+
description: "Trigger DNS verification for a domain",
|
|
124
|
+
inputSchema: { domain_id: z.number().describe("Domain ID") },
|
|
125
|
+
}, async ({ domain_id }) => result(await v2("POST", `/domains/${domain_id}/verify-dns`)));
|
|
126
|
+
|
|
127
|
+
server.registerTool("get_domain_dns_records", {
|
|
128
|
+
description: "Get required DNS records for a domain",
|
|
129
|
+
inputSchema: { domain_id: z.number().describe("Domain ID") },
|
|
130
|
+
annotations: { readOnlyHint: true },
|
|
131
|
+
}, async ({ domain_id }) => result(await v2("GET", `/domains/${domain_id}/dns-records`)));
|
|
132
|
+
|
|
133
|
+
server.registerTool("get_domain_statistics", {
|
|
134
|
+
description: "Get email statistics for a domain",
|
|
135
|
+
inputSchema: { domain_id: z.number().describe("Domain ID") },
|
|
136
|
+
annotations: { readOnlyHint: true },
|
|
137
|
+
}, async ({ domain_id }) => result(await v2("GET", `/domains/${domain_id}/statistics`)));
|
|
138
|
+
|
|
139
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
140
|
+
// Alias tools
|
|
141
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
142
|
+
|
|
143
|
+
server.registerTool("list_aliases", {
|
|
144
|
+
description: "List all email aliases for a domain",
|
|
145
|
+
inputSchema: { domain_id: z.number().describe("Domain ID") },
|
|
146
|
+
annotations: { readOnlyHint: true },
|
|
147
|
+
}, async ({ domain_id }) => result(await v2("GET", `/domains/${domain_id}/aliases`)));
|
|
148
|
+
|
|
149
|
+
server.registerTool("get_alias", {
|
|
150
|
+
description: "Get details of a specific alias",
|
|
151
|
+
inputSchema: { alias_id: z.number().describe("Alias ID") },
|
|
152
|
+
annotations: { readOnlyHint: true },
|
|
153
|
+
}, async ({ alias_id }) => result(await v2("GET", `/aliases/${alias_id}`)));
|
|
154
|
+
|
|
155
|
+
server.registerTool("create_alias", {
|
|
156
|
+
description: "Create a new email alias on a domain",
|
|
157
|
+
inputSchema: {
|
|
158
|
+
domain_id: z.number().describe("Domain ID"),
|
|
159
|
+
local_part: z.string().describe("Local part of the alias (e.g. 'info' for info@domain.com)"),
|
|
160
|
+
forward_to: z.string().describe("Email address to forward to"),
|
|
161
|
+
is_catchall: z.boolean().optional().describe("Create as catch-all alias (uses * as local_part)"),
|
|
162
|
+
description: z.string().optional().describe("Description of the alias"),
|
|
163
|
+
},
|
|
164
|
+
}, async ({ domain_id, local_part, forward_to, is_catchall, description }) => {
|
|
165
|
+
const body = { local_part, forward_to };
|
|
166
|
+
if (is_catchall) body.is_catchall = true;
|
|
167
|
+
if (description) body.description = description;
|
|
168
|
+
return result(await v2("POST", `/domains/${domain_id}/aliases`, body));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
server.registerTool("update_alias", {
|
|
172
|
+
description: "Update an existing alias (forward_to, is_active, description)",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
alias_id: z.number().describe("Alias ID"),
|
|
175
|
+
forward_to: z.string().optional().describe("New forward-to address"),
|
|
176
|
+
is_active: z.boolean().optional().describe("Enable or disable the alias"),
|
|
177
|
+
description: z.string().optional().describe("Updated description"),
|
|
178
|
+
},
|
|
179
|
+
}, async ({ alias_id, ...updates }) => {
|
|
180
|
+
const body = {};
|
|
181
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
182
|
+
if (v !== undefined) body[k] = v;
|
|
183
|
+
}
|
|
184
|
+
return result(await v2("PATCH", `/aliases/${alias_id}`, body));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
server.registerTool("delete_alias", {
|
|
188
|
+
description: "Delete an email alias",
|
|
189
|
+
inputSchema: { alias_id: z.number().describe("Alias ID") },
|
|
190
|
+
annotations: { destructiveHint: true },
|
|
191
|
+
}, async ({ alias_id }) => result(await v2("DELETE", `/aliases/${alias_id}`)));
|
|
192
|
+
|
|
193
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
194
|
+
// Email Log tools
|
|
195
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
196
|
+
|
|
197
|
+
server.registerTool("list_email_logs", {
|
|
198
|
+
description: "Search and list email logs with optional filters",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
domain_id: z.number().optional().describe("Filter by domain ID"),
|
|
201
|
+
alias_id: z.number().optional().describe("Filter by alias ID"),
|
|
202
|
+
sender: z.string().optional().describe("Filter by sender address (partial match)"),
|
|
203
|
+
recipient: z.string().optional().describe("Filter by recipient address (partial match)"),
|
|
204
|
+
status: z.string().optional().describe("Filter by status (forwarded, bounced, rejected, spam)"),
|
|
205
|
+
start_date: z.string().optional().describe("Start date (YYYY-MM-DD), max 30 day range"),
|
|
206
|
+
end_date: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
207
|
+
per_page: z.number().optional().describe("Results per page (default 50)"),
|
|
208
|
+
},
|
|
209
|
+
annotations: { readOnlyHint: true },
|
|
210
|
+
}, async (params) => {
|
|
211
|
+
const qs = new URLSearchParams();
|
|
212
|
+
for (const [k, v] of Object.entries(params)) {
|
|
213
|
+
if (v !== undefined) qs.set(k, String(v));
|
|
214
|
+
}
|
|
215
|
+
const q = qs.toString();
|
|
216
|
+
return result(await v2("GET", `/email-logs${q ? "?" + q : ""}`));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
server.registerTool("get_email_log", {
|
|
220
|
+
description: "Get details of a specific email log entry",
|
|
221
|
+
inputSchema: { log_id: z.number().describe("Email log ID") },
|
|
222
|
+
annotations: { readOnlyHint: true },
|
|
223
|
+
}, async ({ log_id }) => result(await v2("GET", `/email-logs/${log_id}`)));
|
|
224
|
+
|
|
225
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
226
|
+
// Email sending
|
|
227
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
228
|
+
|
|
229
|
+
server.registerTool("send_email", {
|
|
230
|
+
description: "Send an email through a configured domain",
|
|
231
|
+
inputSchema: {
|
|
232
|
+
from: z.string().describe("Sender address (must be an alias on a verified domain)"),
|
|
233
|
+
to: z.string().describe("Recipient email address"),
|
|
234
|
+
subject: z.string().describe("Email subject"),
|
|
235
|
+
body: z.string().describe("Email body (plain text)"),
|
|
236
|
+
html: z.string().optional().describe("HTML body (optional)"),
|
|
237
|
+
},
|
|
238
|
+
}, async ({ from, to, subject, body, html }) => {
|
|
239
|
+
const payload = { from, to: [to], subject, text: body };
|
|
240
|
+
if (html) payload.html = html;
|
|
241
|
+
return result(await v2("POST", "/emails/send", payload));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
245
|
+
// Webhook tools
|
|
246
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
247
|
+
|
|
248
|
+
server.registerTool("list_webhooks", {
|
|
249
|
+
description: "List all webhook endpoints",
|
|
250
|
+
annotations: { readOnlyHint: true },
|
|
251
|
+
}, async () => result(await v2("GET", "/webhooks")));
|
|
252
|
+
|
|
253
|
+
server.registerTool("create_webhook", {
|
|
254
|
+
description: "Create a new webhook endpoint",
|
|
255
|
+
inputSchema: {
|
|
256
|
+
url: z.string().describe("Webhook URL to receive events"),
|
|
257
|
+
events: z.array(z.string()).describe("Events to subscribe to: sent, delivered, bounced, failed, unsubscribed"),
|
|
258
|
+
custom_headers: z.record(z.string()).optional().describe("Custom headers to include"),
|
|
259
|
+
timeout_seconds: z.number().optional().describe("Request timeout 5-30s (default 10)"),
|
|
260
|
+
batch_enabled: z.boolean().optional().describe("Enable batch delivery"),
|
|
261
|
+
},
|
|
262
|
+
}, async ({ url, events, custom_headers, timeout_seconds, batch_enabled }) => {
|
|
263
|
+
const body = { url, events };
|
|
264
|
+
if (custom_headers) body.custom_headers = custom_headers;
|
|
265
|
+
if (timeout_seconds) body.timeout_seconds = timeout_seconds;
|
|
266
|
+
if (batch_enabled !== undefined) body.batch_enabled = batch_enabled;
|
|
267
|
+
return result(await v2("POST", "/webhooks", body));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
server.registerTool("update_webhook", {
|
|
271
|
+
description: "Update a webhook endpoint",
|
|
272
|
+
inputSchema: {
|
|
273
|
+
webhook_id: z.number().describe("Webhook ID"),
|
|
274
|
+
url: z.string().optional().describe("New URL"),
|
|
275
|
+
events: z.array(z.string()).optional().describe("Updated event subscriptions"),
|
|
276
|
+
enabled: z.boolean().optional().describe("Enable or disable"),
|
|
277
|
+
timeout_seconds: z.number().optional().describe("Request timeout 5-30s"),
|
|
278
|
+
},
|
|
279
|
+
}, async ({ webhook_id, ...updates }) => {
|
|
280
|
+
const body = {};
|
|
281
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
282
|
+
if (v !== undefined) body[k] = v;
|
|
283
|
+
}
|
|
284
|
+
return result(await v2("PATCH", `/webhooks/${webhook_id}`, body));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
server.registerTool("delete_webhook", {
|
|
288
|
+
description: "Delete a webhook endpoint",
|
|
289
|
+
inputSchema: { webhook_id: z.number().describe("Webhook ID") },
|
|
290
|
+
annotations: { destructiveHint: true },
|
|
291
|
+
}, async ({ webhook_id }) => result(await v2("DELETE", `/webhooks/${webhook_id}`)));
|
|
292
|
+
|
|
293
|
+
server.registerTool("test_webhook", {
|
|
294
|
+
description: "Send a test event to a webhook endpoint",
|
|
295
|
+
inputSchema: { webhook_id: z.number().describe("Webhook ID") },
|
|
296
|
+
}, async ({ webhook_id }) => result(await v2("POST", `/webhooks/${webhook_id}/test`)));
|
|
297
|
+
|
|
298
|
+
server.registerTool("list_webhook_delivery_logs", {
|
|
299
|
+
description: "List delivery logs for a webhook endpoint",
|
|
300
|
+
inputSchema: {
|
|
301
|
+
webhook_id: z.number().describe("Webhook ID"),
|
|
302
|
+
status: z.string().optional().describe("Filter: success, failure, or all"),
|
|
303
|
+
date_from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
304
|
+
date_to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
305
|
+
per_page: z.number().optional().describe("Results per page (default 50)"),
|
|
306
|
+
},
|
|
307
|
+
annotations: { readOnlyHint: true },
|
|
308
|
+
}, async ({ webhook_id, ...params }) => {
|
|
309
|
+
const qs = new URLSearchParams();
|
|
310
|
+
for (const [k, v] of Object.entries(params)) {
|
|
311
|
+
if (v !== undefined) qs.set(k, String(v));
|
|
312
|
+
}
|
|
313
|
+
const q = qs.toString();
|
|
314
|
+
return result(await v2("GET", `/webhooks/${webhook_id}/delivery-logs${q ? "?" + q : ""}`));
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
server.registerTool("retry_webhook_delivery", {
|
|
318
|
+
description: "Retry a failed webhook delivery",
|
|
319
|
+
inputSchema: { delivery_log_id: z.number().describe("Delivery log ID") },
|
|
320
|
+
}, async ({ delivery_log_id }) =>
|
|
321
|
+
result(await v2("POST", `/webhooks/delivery-logs/${delivery_log_id}/retry`))
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
325
|
+
// Blocklist tools
|
|
326
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
327
|
+
|
|
328
|
+
server.registerTool("list_blocklist", {
|
|
329
|
+
description: "List all sender blocklist entries",
|
|
330
|
+
annotations: { readOnlyHint: true },
|
|
331
|
+
}, async () => result(await v2("GET", "/blocklist")));
|
|
332
|
+
|
|
333
|
+
server.registerTool("add_blocklist_entry", {
|
|
334
|
+
description: "Block a sender address or pattern (e.g. *@spam.com)",
|
|
335
|
+
inputSchema: { pattern: z.string().describe("Email address or wildcard pattern to block") },
|
|
336
|
+
}, async ({ pattern }) => result(await v2("POST", "/blocklist", { pattern })));
|
|
337
|
+
|
|
338
|
+
server.registerTool("remove_blocklist_entry", {
|
|
339
|
+
description: "Remove a sender from the blocklist",
|
|
340
|
+
inputSchema: { entry_id: z.number().describe("Blocklist entry ID") },
|
|
341
|
+
annotations: { destructiveHint: true },
|
|
342
|
+
}, async ({ entry_id }) => result(await v2("DELETE", `/blocklist/${entry_id}`)));
|
|
343
|
+
|
|
344
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
345
|
+
// API Key tools
|
|
346
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
347
|
+
|
|
348
|
+
server.registerTool("list_api_keys", {
|
|
349
|
+
description: "List all active API keys",
|
|
350
|
+
annotations: { readOnlyHint: true },
|
|
351
|
+
}, async () => result(await v2("GET", "/api-keys")));
|
|
352
|
+
|
|
353
|
+
server.registerTool("create_api_key", {
|
|
354
|
+
description: "Create a new API key",
|
|
355
|
+
inputSchema: {
|
|
356
|
+
name: z.string().describe("Descriptive name for the key"),
|
|
357
|
+
scopes: z.array(z.string()).describe("Scopes: email:send, email:read, webhooks:manage"),
|
|
358
|
+
expires_at: z.string().optional().describe("Expiration date (ISO 8601)"),
|
|
359
|
+
},
|
|
360
|
+
}, async ({ name, scopes, expires_at }) => {
|
|
361
|
+
const body = { name, scopes };
|
|
362
|
+
if (expires_at) body.expires_at = expires_at;
|
|
363
|
+
return result(await v2("POST", "/api-keys", body));
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
server.registerTool("revoke_api_key", {
|
|
367
|
+
description: "Revoke an API key",
|
|
368
|
+
inputSchema: { key_id: z.number().describe("API key ID") },
|
|
369
|
+
annotations: { destructiveHint: true },
|
|
370
|
+
}, async ({ key_id }) => result(await v2("DELETE", `/api-keys/${key_id}`)));
|
|
371
|
+
|
|
372
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
373
|
+
// SMTP Credentials
|
|
374
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
375
|
+
|
|
376
|
+
server.registerTool("list_smtp_credentials", {
|
|
377
|
+
description: "List SMTP credentials for a domain",
|
|
378
|
+
inputSchema: { domain_id: z.number().describe("Domain ID") },
|
|
379
|
+
annotations: { readOnlyHint: true },
|
|
380
|
+
}, async ({ domain_id }) => result(await v2("GET", `/domains/${domain_id}/smtp-credentials`)));
|
|
381
|
+
|
|
382
|
+
server.registerTool("create_smtp_credential", {
|
|
383
|
+
description: "Create SMTP credentials for a domain",
|
|
384
|
+
inputSchema: {
|
|
385
|
+
domain_id: z.number().describe("Domain ID"),
|
|
386
|
+
daily_limit: z.number().optional().describe("Daily send limit (1-100000, default 1000)"),
|
|
387
|
+
},
|
|
388
|
+
}, async ({ domain_id, daily_limit }) => {
|
|
389
|
+
const body = {};
|
|
390
|
+
if (daily_limit !== undefined) body.daily_limit = daily_limit;
|
|
391
|
+
return result(await v2("POST", `/domains/${domain_id}/smtp-credentials`, body));
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
server.registerTool("delete_smtp_credential", {
|
|
395
|
+
description: "Delete SMTP credentials",
|
|
396
|
+
inputSchema: { credential_id: z.number().describe("SMTP credential ID") },
|
|
397
|
+
annotations: { destructiveHint: true },
|
|
398
|
+
}, async ({ credential_id }) => result(await v2("DELETE", `/smtp-credentials/${credential_id}`)));
|
|
399
|
+
|
|
400
|
+
server.registerTool("reset_smtp_password", {
|
|
401
|
+
description: "Reset the password for an SMTP credential",
|
|
402
|
+
inputSchema: { credential_id: z.number().describe("SMTP credential ID") },
|
|
403
|
+
}, async ({ credential_id }) =>
|
|
404
|
+
result(await v2("POST", `/smtp-credentials/${credential_id}/reset-password`))
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
408
|
+
// Retention Policy
|
|
409
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
410
|
+
|
|
411
|
+
server.registerTool("get_retention_policy", {
|
|
412
|
+
description: "Get the current email log retention policy",
|
|
413
|
+
annotations: { readOnlyHint: true },
|
|
414
|
+
}, async () => result(await v2("GET", "/retention-policy")));
|
|
415
|
+
|
|
416
|
+
server.registerTool("update_retention_policy", {
|
|
417
|
+
description: "Update the email log retention policy",
|
|
418
|
+
inputSchema: {
|
|
419
|
+
metadata_retention_days: z.number().describe("Days to retain email metadata (7-2555)"),
|
|
420
|
+
content_retention_days: z.number().describe("Days to retain email content (7-2555)"),
|
|
421
|
+
event_specific_retention: z.record(z.number()).optional().describe("Per-event retention overrides (event_name → days)"),
|
|
422
|
+
},
|
|
423
|
+
}, async ({ metadata_retention_days, content_retention_days, event_specific_retention }) => {
|
|
424
|
+
const body = { metadata_retention_days, content_retention_days };
|
|
425
|
+
if (event_specific_retention) body.event_specific_retention = event_specific_retention;
|
|
426
|
+
return result(await v2("PUT", "/retention-policy", body));
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
430
|
+
// Anonymous Replies
|
|
431
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
432
|
+
|
|
433
|
+
server.registerTool("list_anonymous_replies", {
|
|
434
|
+
description: "List anonymous reply proxy addresses",
|
|
435
|
+
inputSchema: {
|
|
436
|
+
status: z.string().optional().describe("Filter: active, expired, or disabled"),
|
|
437
|
+
domain_id: z.number().optional().describe("Filter by domain ID"),
|
|
438
|
+
per_page: z.number().optional().describe("Results per page (default 15)"),
|
|
439
|
+
},
|
|
440
|
+
annotations: { readOnlyHint: true },
|
|
441
|
+
}, async (params) => {
|
|
442
|
+
const qs = new URLSearchParams();
|
|
443
|
+
for (const [k, v] of Object.entries(params)) {
|
|
444
|
+
if (v !== undefined) qs.set(k, String(v));
|
|
445
|
+
}
|
|
446
|
+
const q = qs.toString();
|
|
447
|
+
return result(await v2("GET", `/anonymous-replies${q ? "?" + q : ""}`));
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
server.registerTool("disable_anonymous_reply", {
|
|
451
|
+
description: "Disable an anonymous reply proxy address",
|
|
452
|
+
inputSchema: { proxy_address: z.string().describe("The proxy email address to disable") },
|
|
453
|
+
}, async ({ proxy_address }) =>
|
|
454
|
+
result(await v2("POST", `/anonymous-replies/${encodeURIComponent(proxy_address)}/disable`))
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
458
|
+
// Start server
|
|
459
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
460
|
+
|
|
461
|
+
async function main() {
|
|
462
|
+
const transport = new StdioServerTransport();
|
|
463
|
+
await server.connect(transport);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
main().catch((err) => {
|
|
467
|
+
console.error("Fatal:", err);
|
|
468
|
+
process.exit(1);
|
|
469
|
+
});
|