← back to Purelymail Mcp
index.js
276 lines
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
const API = "https://purelymail.com/api/v0";
function loadToken() {
if (process.env.PURELYMAIL_API_TOKEN) return process.env.PURELYMAIL_API_TOKEN.trim();
try {
const txt = readFileSync(join(homedir(), "Projects/secrets-manager/.env"), "utf8");
const m = txt.match(/^PURELYMAIL_API_TOKEN=(.+)$/m);
if (m) return m[1].trim();
} catch {}
return null;
}
const TOKEN = loadToken();
if (!TOKEN) {
console.error(
"purelymail-mcp: no PURELYMAIL_API_TOKEN. Set the env var, or run:\n" +
" node ~/Projects/secrets-manager/cli.js add PURELYMAIL_API_TOKEN <value>\n" +
"Generate one at https://purelymail.com/manage → API"
);
process.exit(1);
}
async function call(endpoint, body = {}) {
const url = API + (endpoint.startsWith("/") ? endpoint : "/" + endpoint);
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Purelymail-Api-Token": TOKEN,
},
body: JSON.stringify(body),
});
const text = await res.text();
let data;
try { data = JSON.parse(text); } catch { data = { raw: text }; }
if (!res.ok || (data && data.type === "error")) {
const err = new Error(
`Purelymail ${endpoint} → ${res.status}: ${data?.message || data?.error || text.slice(0, 200)}`
);
err.status = res.status;
err.body = data;
throw err;
}
return data;
}
const TOOLS = [
{
name: "purelymail_list_domains",
description:
"List every domain on the Purelymail account, with their MX/SPF/DKIM verification state. No args.",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
},
{
name: "purelymail_add_domain",
description:
"Add a new domain to the Purelymail account. Returns the verification records that need to be set on DNS.",
inputSchema: {
type: "object",
properties: {
domainName: { type: "string", description: "Apex domain, e.g. agentabrams.com" },
},
required: ["domainName"],
additionalProperties: false,
},
},
{
name: "purelymail_delete_domain",
description: "Remove a domain from the Purelymail account. Destructive — confirm before calling.",
inputSchema: {
type: "object",
properties: { domainName: { type: "string" } },
required: ["domainName"],
additionalProperties: false,
},
},
{
name: "purelymail_list_routing_rules",
description:
"List every routing rule (alias / forwarder) for a domain. Each rule maps matchUser@domain to one or more targetAddresses.",
inputSchema: {
type: "object",
properties: { domainName: { type: "string" } },
required: ["domainName"],
additionalProperties: false,
},
},
{
name: "purelymail_add_routing_rule",
description:
"Create a routing rule (alias / forwarder). matchUser is the local-part to match (use '*' for catch-all, or 'info' to forward 'info@domainName'). targetAddresses is one or more destinations.",
inputSchema: {
type: "object",
properties: {
domainName: { type: "string", description: "Apex domain that owns the rule, e.g. agentabrams.com" },
matchUser: { type: "string", description: "Local part to match. '*' = catch-all. 'info' = info@<domainName>." },
targetAddresses: {
type: "array",
items: { type: "string" },
minItems: 1,
description: "One or more destination addresses. Plain email strings.",
},
prefix: { type: "boolean", description: "If true, treat matchUser as a prefix match (e.g. 'sales' matches sales+*).", default: false },
catchAll: { type: "boolean", description: "If true, this rule catches any unmatched address on the domain.", default: false },
},
required: ["domainName", "matchUser", "targetAddresses"],
additionalProperties: false,
},
},
{
name: "purelymail_delete_routing_rule",
description: "Delete a routing rule by its id. Get the id from purelymail_list_routing_rules first.",
inputSchema: {
type: "object",
properties: { routingRuleId: { type: ["integer", "string"] } },
required: ["routingRuleId"],
additionalProperties: false,
},
},
{
name: "purelymail_create_user",
description:
"Create a new mailbox. userName is the full address (info@domain.com). Password should be strong and stored via /secrets after creation.",
inputSchema: {
type: "object",
properties: {
userName: { type: "string", description: "Full address, e.g. info@venturablvd.agentabrams.com" },
password: { type: "string" },
enablePasswordReset: { type: "boolean", default: true },
recoveryEmail: { type: "string" },
},
required: ["userName", "password"],
additionalProperties: false,
},
},
{
name: "purelymail_delete_user",
description: "Delete a mailbox. Destructive.",
inputSchema: {
type: "object",
properties: { userName: { type: "string" } },
required: ["userName"],
additionalProperties: false,
},
},
{
name: "purelymail_modify_user",
description: "Modify a user (rename, change password, change recovery email, toggle reset).",
inputSchema: {
type: "object",
properties: {
userName: { type: "string" },
newUserName: { type: "string" },
newPassword: { type: "string" },
recoveryEmail: { type: "string" },
enablePasswordReset: { type: "boolean" },
},
required: ["userName"],
additionalProperties: false,
},
},
{
name: "purelymail_account_credit",
description: "Return the current account credit balance and renewal info.",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
},
{
name: "purelymail_call_raw",
description:
"Escape hatch — call any Purelymail v0 endpoint by name with an arbitrary JSON body. Use only if a tool above doesn't fit.",
inputSchema: {
type: "object",
properties: {
endpoint: { type: "string", description: "Endpoint name without leading slash, e.g. 'listDomains'." },
body: { type: "object", additionalProperties: true },
},
required: ["endpoint"],
additionalProperties: false,
},
},
];
const server = new Server(
{ name: "purelymail", version: "0.1.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args = {} } = req.params;
try {
let data;
switch (name) {
case "purelymail_list_domains":
data = await call("listDomains");
break;
case "purelymail_add_domain":
data = await call("addDomain", { domainName: args.domainName });
break;
case "purelymail_delete_domain":
data = await call("deleteDomain", { domainName: args.domainName });
break;
case "purelymail_list_routing_rules":
data = await call("listRoutingRules", { domainName: args.domainName });
break;
case "purelymail_add_routing_rule":
data = await call("createRoutingRule", {
domainName: args.domainName,
matchUser: args.matchUser,
targetAddresses: args.targetAddresses,
prefix: args.prefix ?? false,
catchAll: args.catchAll ?? false,
});
break;
case "purelymail_delete_routing_rule":
data = await call("deleteRoutingRule", { routingRuleId: args.routingRuleId });
break;
case "purelymail_create_user": {
// Purelymail createUser expects userName = LOCAL PART + domainName separately,
// NOT the full address (full address -> "Request could not be parsed"). Split it.
const addr = String(args.userName);
const at = addr.lastIndexOf("@");
const body = {
userName: at >= 0 ? addr.slice(0, at) : addr,
domainName: at >= 0 ? addr.slice(at + 1) : args.domainName,
password: args.password,
enablePasswordReset: args.enablePasswordReset ?? true,
};
if (args.recoveryEmail) body.recoveryEmail = args.recoveryEmail;
data = await call("createUser", body);
break;
}
case "purelymail_delete_user":
data = await call("deleteUser", { userName: args.userName });
break;
case "purelymail_modify_user": {
const body = { userName: args.userName };
if (args.newUserName) body.newUserName = args.newUserName;
if (args.newPassword) body.newPassword = args.newPassword;
if (args.recoveryEmail) body.recoveryEmail = args.recoveryEmail;
if (args.enablePasswordReset !== undefined) body.enablePasswordReset = args.enablePasswordReset;
data = await call("modifyUser", body);
break;
}
case "purelymail_account_credit":
data = await call("checkAccountCredit");
break;
case "purelymail_call_raw":
data = await call(args.endpoint, args.body || {});
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
} catch (e) {
const raw = e.message + (e.body ? "\n\n" + JSON.stringify(e.body, null, 2) : "");
const safe = TOKEN ? raw.split(TOKEN).join("[REDACTED]") : raw;
return { isError: true, content: [{ type: "text", text: safe }] };
}
});
const transport = new StdioServerTransport();
await server.connect(transport);