@eusend_dev/mcp
Advanced tools
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { Eusend } from "@eusend_dev/sdk"; | ||
| import { z } from "zod"; | ||
| import { createServer } from "node:http"; | ||
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | ||
| //#region src/respond.ts | ||
| /** Wrap a successful payload as pretty-printed JSON text content. */ | ||
| function ok(data) { | ||
| return { content: [{ | ||
| type: "text", | ||
| text: JSON.stringify(data, null, 2) | ||
| }] }; | ||
| } | ||
| /** | ||
| * Surface an SDK error as an MCP tool error. `isError: true` tells the client | ||
| * the call failed, and the error `name` (e.g. DOMAIN_NOT_VERIFIED) gives the | ||
| * agent enough to self-correct — e.g. verify the domain, then retry. | ||
| */ | ||
| function fail(error) { | ||
| return { | ||
| content: [{ | ||
| type: "text", | ||
| text: `Error [${error.name}]: ${error.message}` | ||
| }], | ||
| isError: true | ||
| }; | ||
| } | ||
| /** Plain text error for validation the tool does itself before hitting the API. */ | ||
| function failText(message) { | ||
| return { | ||
| content: [{ | ||
| type: "text", | ||
| text: message | ||
| }], | ||
| isError: true | ||
| }; | ||
| } | ||
| /** Turn any SDK `EusendResponse` into a tool result. */ | ||
| function respond(res) { | ||
| return res.error ? fail(res.error) : ok(res.data); | ||
| } | ||
| //#endregion | ||
| //#region src/tools/emails.ts | ||
| const emailStatus = z.enum([ | ||
| "queued", | ||
| "scheduled", | ||
| "canceled", | ||
| "sending", | ||
| "sent", | ||
| "delivered", | ||
| "bounced", | ||
| "complained", | ||
| "suppressed", | ||
| "failed" | ||
| ]); | ||
| const recipients = z.union([z.string(), z.array(z.string())]); | ||
| function registerEmailTools(server, client, config) { | ||
| server.registerTool("send_email", { | ||
| title: "Send an email", | ||
| description: "Send a transactional email through Eusend. Provide `html` or `text` (or both). The sender domain must be verified on the account — use list_domains to check. If `from` is omitted, the server-configured default sender is used.", | ||
| inputSchema: { | ||
| from: z.string().optional().describe("Sender, e.g. \"Acme <hi@acme.com>\". Defaults to the configured sender."), | ||
| to: recipients.describe("Recipient address or list of addresses."), | ||
| subject: z.string().describe("Email subject line."), | ||
| text: z.string().optional().describe("Plain-text body."), | ||
| html: z.string().optional().describe("HTML body. Takes precedence over text for display."), | ||
| cc: recipients.optional(), | ||
| bcc: recipients.optional(), | ||
| replyTo: recipients.optional().describe("Reply-To address(es)."), | ||
| scheduledAt: z.string().optional().describe("Future send time: ISO 8601 or natural language like \"in 1 hour\". Max 30 days out.") | ||
| } | ||
| }, async (args) => { | ||
| const from = args.from ?? config.defaultFrom; | ||
| if (!from) return failText("No sender specified and no default sender configured. Pass `from`, or set EUSEND_FROM / --from on the server."); | ||
| if (!args.html && !args.text) return failText("Provide at least one of `html` or `text`."); | ||
| return respond(await client.emails.send({ | ||
| from, | ||
| to: args.to, | ||
| subject: args.subject, | ||
| text: args.text, | ||
| html: args.html, | ||
| cc: args.cc, | ||
| bcc: args.bcc, | ||
| replyTo: args.replyTo ?? config.defaultReplyTo, | ||
| scheduledAt: args.scheduledAt | ||
| })); | ||
| }); | ||
| server.registerTool("list_emails", { | ||
| title: "List emails", | ||
| description: "List recently sent emails, most recent first. Supports cursor pagination and filtering.", | ||
| inputSchema: { | ||
| limit: z.number().int().min(1).max(100).optional().describe("Max results (1–100)."), | ||
| cursor: z.string().optional().describe("Pagination cursor from a previous nextCursor."), | ||
| status: emailStatus.optional().describe("Filter by delivery status."), | ||
| from: z.string().optional().describe("Filter by sender address."), | ||
| to: z.string().optional().describe("Filter by recipient address.") | ||
| } | ||
| }, async (args) => respond(await client.emails.list(args))); | ||
| server.registerTool("get_email", { | ||
| title: "Get an email", | ||
| description: "Fetch a single email by id, including its full delivery event history.", | ||
| inputSchema: { id: z.string().describe("The email id returned by send_email or list_emails.") } | ||
| }, async ({ id }) => respond(await client.emails.get(id))); | ||
| server.registerTool("cancel_email", { | ||
| title: "Cancel a scheduled email", | ||
| description: "Cancel a scheduled email before it sends. Fails once the email has started sending.", | ||
| inputSchema: { id: z.string().describe("The id of a scheduled email.") } | ||
| }, async ({ id }) => respond(await client.emails.cancel(id))); | ||
| } | ||
| //#endregion | ||
| //#region src/tools/domains.ts | ||
| function registerDomainTools(server, client) { | ||
| server.registerTool("list_domains", { | ||
| title: "List domains", | ||
| description: "List sending domains on the account and their verification status.", | ||
| inputSchema: {} | ||
| }, async () => respond(await client.domains.list())); | ||
| server.registerTool("get_domain", { | ||
| title: "Get a domain", | ||
| description: "Fetch a single domain by id, including its DKIM selector and verification state.", | ||
| inputSchema: { id: z.string().describe("The domain id.") } | ||
| }, async ({ id }) => respond(await client.domains.get(id))); | ||
| server.registerTool("create_domain", { | ||
| title: "Add a sending domain", | ||
| description: "Register a new sending domain. Returns the DKIM, SPF, and DMARC DNS records that must be added at the domain's DNS provider before it can send. Present these records to the user.", | ||
| inputSchema: { name: z.string().describe("The domain to add, e.g. \"mail.acme.com\".") } | ||
| }, async ({ name }) => respond(await client.domains.create(name))); | ||
| } | ||
| //#endregion | ||
| //#region src/tools/audiences.ts | ||
| function registerAudienceTools(server, client) { | ||
| server.registerTool("list_audiences", { | ||
| title: "List audiences", | ||
| description: "List contact audiences (lists) on the account, with per-audience contact counts.", | ||
| inputSchema: {} | ||
| }, async () => respond(await client.audiences.list())); | ||
| server.registerTool("create_audience", { | ||
| title: "Create an audience", | ||
| description: "Create a new audience (contact list) to hold contacts for broadcasts.", | ||
| inputSchema: { name: z.string().describe("Human-readable name for the audience.") } | ||
| }, async ({ name }) => respond(await client.audiences.create(name))); | ||
| server.registerTool("list_contacts", { | ||
| title: "List contacts", | ||
| description: "List contacts in an audience. Supports search and subscription filtering.", | ||
| inputSchema: { | ||
| audienceId: z.string().describe("The audience to list contacts from."), | ||
| limit: z.number().int().min(1).max(100).optional(), | ||
| cursor: z.string().optional().describe("Pagination cursor from a previous nextCursor."), | ||
| search: z.string().optional().describe("Filter by email or name substring."), | ||
| subscribed: z.boolean().optional().describe("true = only subscribed, false = only unsubscribed.") | ||
| } | ||
| }, async ({ audienceId, ...opts }) => respond(await client.audiences.listContacts(audienceId, opts))); | ||
| server.registerTool("create_contact", { | ||
| title: "Add a contact", | ||
| description: "Add a contact to an audience.", | ||
| inputSchema: { | ||
| audienceId: z.string().describe("The audience to add the contact to."), | ||
| email: z.string().describe("Contact email address."), | ||
| firstName: z.string().optional(), | ||
| lastName: z.string().optional() | ||
| } | ||
| }, async ({ audienceId, email, firstName, lastName }) => respond(await client.audiences.createContact(audienceId, { | ||
| email, | ||
| firstName, | ||
| lastName | ||
| }))); | ||
| } | ||
| //#endregion | ||
| //#region src/tools/broadcasts.ts | ||
| function registerBroadcastTools(server, client, config) { | ||
| server.registerTool("list_broadcasts", { | ||
| title: "List broadcasts", | ||
| description: "List broadcasts (bulk sends to an audience) with their status and send counts.", | ||
| inputSchema: {} | ||
| }, async () => respond(await client.broadcasts.list())); | ||
| server.registerTool("get_broadcast", { | ||
| title: "Get a broadcast", | ||
| description: "Fetch a broadcast by id, including recipient/sent counts and per-event stats.", | ||
| inputSchema: { id: z.string().describe("The broadcast id.") } | ||
| }, async ({ id }) => respond(await client.broadcasts.get(id))); | ||
| server.registerTool("create_broadcast", { | ||
| title: "Create a broadcast (draft)", | ||
| description: "Create a broadcast in draft state, targeting an audience. Does NOT send it — use send_broadcast to send or schedule after reviewing. If `from` is omitted the configured default sender is used.", | ||
| inputSchema: { | ||
| name: z.string().describe("Internal name for the broadcast."), | ||
| audienceId: z.string().describe("The audience to send to."), | ||
| subject: z.string().describe("Email subject line."), | ||
| from: z.string().optional().describe("Sender. Defaults to the configured sender."), | ||
| html: z.string().optional().describe("HTML body of the broadcast.") | ||
| } | ||
| }, async (args) => { | ||
| const from = args.from ?? config.defaultFrom; | ||
| if (!from) return failText("No sender specified and no default sender configured. Pass `from`, or set EUSEND_FROM / --from on the server."); | ||
| return respond(await client.broadcasts.create({ | ||
| name: args.name, | ||
| audienceId: args.audienceId, | ||
| subject: args.subject, | ||
| from, | ||
| html: args.html | ||
| })); | ||
| }); | ||
| server.registerTool("send_broadcast", { | ||
| title: "Send or schedule a broadcast", | ||
| description: "Send a draft broadcast to its entire audience now, or schedule it for later. This is a bulk send — confirm with the user before calling. Omit `scheduledAt` to send immediately.", | ||
| inputSchema: { | ||
| id: z.string().describe("The draft broadcast id."), | ||
| scheduledAt: z.string().optional().describe("Optional future send time (ISO 8601). Omit to send immediately.") | ||
| } | ||
| }, async ({ id, scheduledAt }) => respond(await client.broadcasts.send(id, { scheduledAt }))); | ||
| } | ||
| //#endregion | ||
| //#region src/server.ts | ||
| const SERVER_NAME = "eusend"; | ||
| const SERVER_VERSION = "0.1.1"; | ||
| /** | ||
| * Build a fully-wired MCP server for one Eusend account. This is | ||
| * transport-agnostic: the stdio entry (index.ts) and the HTTP entry (http.ts) | ||
| * both call this and then attach their own transport. All tool definitions live | ||
| * in ./tools and are registered here exactly once. | ||
| */ | ||
| function createServer$1(config) { | ||
| const server = new McpServer({ | ||
| name: SERVER_NAME, | ||
| version: SERVER_VERSION | ||
| }); | ||
| const client = new Eusend(config.apiKey, { baseUrl: config.baseUrl }); | ||
| registerEmailTools(server, client, config); | ||
| registerDomainTools(server, client); | ||
| registerAudienceTools(server, client); | ||
| registerBroadcastTools(server, client, config); | ||
| return server; | ||
| } | ||
| //#endregion | ||
| //#region src/ratelimit.ts | ||
| const MAX_BUCKETS = 5e4; | ||
| /** | ||
| * Trusted client IP. Coolify's Traefik sits in front and OVERWRITES X-Real-Ip with the | ||
| * real connection address, so it can't be spoofed by a client-sent header. We deliberately | ||
| * do NOT trust X-Forwarded-For here: Traefik appends the real IP to any client-supplied | ||
| * XFF, so its leftmost entry is attacker-controlled and would let a caller mint a fresh | ||
| * bucket per request. Falls back to the socket address when there's no proxy (local runs). | ||
| */ | ||
| function clientIp(req) { | ||
| const realIp = req.headers["x-real-ip"]; | ||
| if (typeof realIp === "string" && realIp.length > 0) return realIp.trim(); | ||
| return req.socket.remoteAddress ?? "unknown"; | ||
| } | ||
| function createRateLimiter(limit, windowMs) { | ||
| const buckets = /* @__PURE__ */ new Map(); | ||
| setInterval(() => { | ||
| const now = Date.now(); | ||
| for (const [key, b] of buckets) if (now > b.resetAt) buckets.delete(key); | ||
| }, windowMs).unref(); | ||
| return { check(req) { | ||
| const key = clientIp(req); | ||
| const now = Date.now(); | ||
| const bucket = buckets.get(key); | ||
| if (!bucket || now > bucket.resetAt) { | ||
| if (!bucket && buckets.size >= MAX_BUCKETS) { | ||
| for (const [k, b] of buckets) if (now > b.resetAt) buckets.delete(k); | ||
| if (buckets.size >= MAX_BUCKETS) return false; | ||
| } | ||
| buckets.set(key, { | ||
| count: 1, | ||
| resetAt: now + windowMs | ||
| }); | ||
| return true; | ||
| } | ||
| bucket.count += 1; | ||
| return bucket.count <= limit; | ||
| } }; | ||
| } | ||
| //#endregion | ||
| //#region src/http.ts | ||
| const MCP_PATH = "/mcp"; | ||
| const rateLimiter = createRateLimiter(Number(process.env.RATE_LIMIT_PER_MIN ?? "120"), 6e4); | ||
| function json(res, status, body) { | ||
| res.writeHead(status, { "Content-Type": "application/json" }); | ||
| res.end(JSON.stringify(body)); | ||
| } | ||
| /** Pull the API key out of `Authorization: Bearer <key>`. */ | ||
| function bearerKey(req) { | ||
| const header = req.headers.authorization; | ||
| if (!header) return void 0; | ||
| const [scheme, value] = header.split(" "); | ||
| return scheme?.toLowerCase() === "bearer" && value ? value : void 0; | ||
| } | ||
| async function readBody(req) { | ||
| const chunks = []; | ||
| for await (const chunk of req) chunks.push(chunk); | ||
| if (chunks.length === 0) return void 0; | ||
| return JSON.parse(Buffer.concat(chunks).toString("utf8")); | ||
| } | ||
| /** | ||
| * Run the Eusend MCP server over Streamable HTTP — the hosted/remote transport. | ||
| * Stateless: every request builds a fresh server bound to the caller's API key, | ||
| * so one endpoint safely serves many accounts. Mount this behind TLS at, e.g., | ||
| * https://mcp.eusend.dev/mcp. | ||
| */ | ||
| async function runHttp(options) { | ||
| const httpServer = createServer((req, res) => { | ||
| handle(req, res, options); | ||
| }); | ||
| await new Promise((resolve) => { | ||
| httpServer.listen(options.port, options.host, resolve); | ||
| }); | ||
| console.error(`Eusend MCP server (HTTP) listening on http://${options.host}:${options.port}${MCP_PATH}`); | ||
| } | ||
| async function handle(req, res, options) { | ||
| const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); | ||
| if (url.pathname === "/health") return json(res, 200, { status: "ok" }); | ||
| if (url.pathname !== MCP_PATH) return json(res, 404, { error: "Not found" }); | ||
| if (req.method !== "POST") { | ||
| res.writeHead(405, { Allow: "POST" }); | ||
| res.end(); | ||
| return; | ||
| } | ||
| if (!rateLimiter.check(req)) return json(res, 429, { | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32029, | ||
| message: "Too many requests. Slow down." | ||
| }, | ||
| id: null | ||
| }); | ||
| const apiKey = bearerKey(req); | ||
| if (!apiKey) return json(res, 401, { | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32001, | ||
| message: "Missing or malformed Authorization: Bearer <api-key> header." | ||
| }, | ||
| id: null | ||
| }); | ||
| const server = createServer$1({ | ||
| apiKey, | ||
| ...options.defaults | ||
| }); | ||
| const transport = new StreamableHTTPServerTransport({ | ||
| sessionIdGenerator: void 0, | ||
| enableDnsRebindingProtection: true, | ||
| allowedHosts: options.allowedHosts | ||
| }); | ||
| res.on("close", () => { | ||
| transport.close(); | ||
| server.close(); | ||
| }); | ||
| try { | ||
| await server.connect(transport); | ||
| const body = await readBody(req); | ||
| await transport.handleRequest(req, res, body); | ||
| } catch (err) { | ||
| console.error("Error handling MCP request:", err); | ||
| if (!res.headersSent) json(res, 500, { | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32603, | ||
| message: "Internal server error" | ||
| }, | ||
| id: null | ||
| }); | ||
| } | ||
| } | ||
| //#endregion | ||
| export { createServer$1 as n, runHttp as t }; | ||
| //# sourceMappingURL=http-BiRaRNNh.mjs.map |
| {"version":3,"file":"http-BiRaRNNh.mjs","names":["createServer","createHttpServer","createServer"],"sources":["../src/respond.ts","../src/tools/emails.ts","../src/tools/domains.ts","../src/tools/audiences.ts","../src/tools/broadcasts.ts","../src/server.ts","../src/ratelimit.ts","../src/http.ts"],"sourcesContent":["import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { EusendError, EusendResponse } from '@eusend_dev/sdk';\n\n/** MCP tool return shape. */\nexport type ToolResult = CallToolResult;\n\n/** Wrap a successful payload as pretty-printed JSON text content. */\nexport function ok(data: unknown): ToolResult {\n return {\n content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],\n };\n}\n\n/**\n * Surface an SDK error as an MCP tool error. `isError: true` tells the client\n * the call failed, and the error `name` (e.g. DOMAIN_NOT_VERIFIED) gives the\n * agent enough to self-correct — e.g. verify the domain, then retry.\n */\nexport function fail(error: EusendError): ToolResult {\n return {\n content: [{ type: 'text', text: `Error [${error.name}]: ${error.message}` }],\n isError: true,\n };\n}\n\n/** Plain text error for validation the tool does itself before hitting the API. */\nexport function failText(message: string): ToolResult {\n return { content: [{ type: 'text', text: message }], isError: true };\n}\n\n/** Turn any SDK `EusendResponse` into a tool result. */\nexport function respond<T>(res: EusendResponse<T>): ToolResult {\n return res.error ? fail(res.error) : ok(res.data);\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Eusend } from '@eusend_dev/sdk';\nimport { z } from 'zod';\nimport type { McpConfig } from '../config.js';\nimport { respond, failText } from '../respond.js';\n\nconst emailStatus = z.enum([\n 'queued',\n 'scheduled',\n 'canceled',\n 'sending',\n 'sent',\n 'delivered',\n 'bounced',\n 'complained',\n 'suppressed',\n 'failed',\n]);\n\nconst recipients = z.union([z.string(), z.array(z.string())]);\n\nexport function registerEmailTools(server: McpServer, client: Eusend, config: McpConfig): void {\n server.registerTool(\n 'send_email',\n {\n title: 'Send an email',\n description:\n 'Send a transactional email through Eusend. Provide `html` or `text` (or both). ' +\n 'The sender domain must be verified on the account — use list_domains to check. ' +\n 'If `from` is omitted, the server-configured default sender is used.',\n inputSchema: {\n from: z\n .string()\n .optional()\n .describe('Sender, e.g. \"Acme <hi@acme.com>\". Defaults to the configured sender.'),\n to: recipients.describe('Recipient address or list of addresses.'),\n subject: z.string().describe('Email subject line.'),\n text: z.string().optional().describe('Plain-text body.'),\n html: z.string().optional().describe('HTML body. Takes precedence over text for display.'),\n cc: recipients.optional(),\n bcc: recipients.optional(),\n replyTo: recipients.optional().describe('Reply-To address(es).'),\n scheduledAt: z\n .string()\n .optional()\n .describe('Future send time: ISO 8601 or natural language like \"in 1 hour\". Max 30 days out.'),\n },\n },\n async (args) => {\n const from = args.from ?? config.defaultFrom;\n if (!from) {\n return failText(\n 'No sender specified and no default sender configured. Pass `from`, or set EUSEND_FROM / --from on the server.',\n );\n }\n if (!args.html && !args.text) {\n return failText('Provide at least one of `html` or `text`.');\n }\n return respond(\n await client.emails.send({\n from,\n to: args.to,\n subject: args.subject,\n text: args.text,\n html: args.html,\n cc: args.cc,\n bcc: args.bcc,\n replyTo: args.replyTo ?? config.defaultReplyTo,\n scheduledAt: args.scheduledAt,\n }),\n );\n },\n );\n\n server.registerTool(\n 'list_emails',\n {\n title: 'List emails',\n description: 'List recently sent emails, most recent first. Supports cursor pagination and filtering.',\n inputSchema: {\n limit: z.number().int().min(1).max(100).optional().describe('Max results (1–100).'),\n cursor: z.string().optional().describe('Pagination cursor from a previous nextCursor.'),\n status: emailStatus.optional().describe('Filter by delivery status.'),\n from: z.string().optional().describe('Filter by sender address.'),\n to: z.string().optional().describe('Filter by recipient address.'),\n },\n },\n async (args) => respond(await client.emails.list(args)),\n );\n\n server.registerTool(\n 'get_email',\n {\n title: 'Get an email',\n description: 'Fetch a single email by id, including its full delivery event history.',\n inputSchema: {\n id: z.string().describe('The email id returned by send_email or list_emails.'),\n },\n },\n async ({ id }) => respond(await client.emails.get(id)),\n );\n\n server.registerTool(\n 'cancel_email',\n {\n title: 'Cancel a scheduled email',\n description: 'Cancel a scheduled email before it sends. Fails once the email has started sending.',\n inputSchema: {\n id: z.string().describe('The id of a scheduled email.'),\n },\n },\n async ({ id }) => respond(await client.emails.cancel(id)),\n );\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Eusend } from '@eusend_dev/sdk';\nimport { z } from 'zod';\nimport { respond } from '../respond.js';\n\nexport function registerDomainTools(server: McpServer, client: Eusend): void {\n server.registerTool(\n 'list_domains',\n {\n title: 'List domains',\n description: 'List sending domains on the account and their verification status.',\n inputSchema: {},\n },\n async () => respond(await client.domains.list()),\n );\n\n server.registerTool(\n 'get_domain',\n {\n title: 'Get a domain',\n description: 'Fetch a single domain by id, including its DKIM selector and verification state.',\n inputSchema: {\n id: z.string().describe('The domain id.'),\n },\n },\n async ({ id }) => respond(await client.domains.get(id)),\n );\n\n server.registerTool(\n 'create_domain',\n {\n title: 'Add a sending domain',\n description:\n 'Register a new sending domain. Returns the DKIM, SPF, and DMARC DNS records that must be ' +\n 'added at the domain\\'s DNS provider before it can send. Present these records to the user.',\n inputSchema: {\n name: z.string().describe('The domain to add, e.g. \"mail.acme.com\".'),\n },\n },\n async ({ name }) => respond(await client.domains.create(name)),\n );\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Eusend } from '@eusend_dev/sdk';\nimport { z } from 'zod';\nimport { respond } from '../respond.js';\n\nexport function registerAudienceTools(server: McpServer, client: Eusend): void {\n server.registerTool(\n 'list_audiences',\n {\n title: 'List audiences',\n description: 'List contact audiences (lists) on the account, with per-audience contact counts.',\n inputSchema: {},\n },\n async () => respond(await client.audiences.list()),\n );\n\n server.registerTool(\n 'create_audience',\n {\n title: 'Create an audience',\n description: 'Create a new audience (contact list) to hold contacts for broadcasts.',\n inputSchema: {\n name: z.string().describe('Human-readable name for the audience.'),\n },\n },\n async ({ name }) => respond(await client.audiences.create(name)),\n );\n\n server.registerTool(\n 'list_contacts',\n {\n title: 'List contacts',\n description: 'List contacts in an audience. Supports search and subscription filtering.',\n inputSchema: {\n audienceId: z.string().describe('The audience to list contacts from.'),\n limit: z.number().int().min(1).max(100).optional(),\n cursor: z.string().optional().describe('Pagination cursor from a previous nextCursor.'),\n search: z.string().optional().describe('Filter by email or name substring.'),\n subscribed: z.boolean().optional().describe('true = only subscribed, false = only unsubscribed.'),\n },\n },\n async ({ audienceId, ...opts }) => respond(await client.audiences.listContacts(audienceId, opts)),\n );\n\n server.registerTool(\n 'create_contact',\n {\n title: 'Add a contact',\n description: 'Add a contact to an audience.',\n inputSchema: {\n audienceId: z.string().describe('The audience to add the contact to.'),\n email: z.string().describe('Contact email address.'),\n firstName: z.string().optional(),\n lastName: z.string().optional(),\n },\n },\n async ({ audienceId, email, firstName, lastName }) =>\n respond(await client.audiences.createContact(audienceId, { email, firstName, lastName })),\n );\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Eusend } from '@eusend_dev/sdk';\nimport { z } from 'zod';\nimport type { McpConfig } from '../config.js';\nimport { respond, failText } from '../respond.js';\n\nexport function registerBroadcastTools(server: McpServer, client: Eusend, config: McpConfig): void {\n server.registerTool(\n 'list_broadcasts',\n {\n title: 'List broadcasts',\n description: 'List broadcasts (bulk sends to an audience) with their status and send counts.',\n inputSchema: {},\n },\n async () => respond(await client.broadcasts.list()),\n );\n\n server.registerTool(\n 'get_broadcast',\n {\n title: 'Get a broadcast',\n description: 'Fetch a broadcast by id, including recipient/sent counts and per-event stats.',\n inputSchema: {\n id: z.string().describe('The broadcast id.'),\n },\n },\n async ({ id }) => respond(await client.broadcasts.get(id)),\n );\n\n server.registerTool(\n 'create_broadcast',\n {\n title: 'Create a broadcast (draft)',\n description:\n 'Create a broadcast in draft state, targeting an audience. Does NOT send it — use send_broadcast ' +\n 'to send or schedule after reviewing. If `from` is omitted the configured default sender is used.',\n inputSchema: {\n name: z.string().describe('Internal name for the broadcast.'),\n audienceId: z.string().describe('The audience to send to.'),\n subject: z.string().describe('Email subject line.'),\n from: z.string().optional().describe('Sender. Defaults to the configured sender.'),\n html: z.string().optional().describe('HTML body of the broadcast.'),\n },\n },\n async (args) => {\n const from = args.from ?? config.defaultFrom;\n if (!from) {\n return failText(\n 'No sender specified and no default sender configured. Pass `from`, or set EUSEND_FROM / --from on the server.',\n );\n }\n return respond(\n await client.broadcasts.create({\n name: args.name,\n audienceId: args.audienceId,\n subject: args.subject,\n from,\n html: args.html,\n }),\n );\n },\n );\n\n server.registerTool(\n 'send_broadcast',\n {\n title: 'Send or schedule a broadcast',\n description:\n 'Send a draft broadcast to its entire audience now, or schedule it for later. This is a bulk send — ' +\n 'confirm with the user before calling. Omit `scheduledAt` to send immediately.',\n inputSchema: {\n id: z.string().describe('The draft broadcast id.'),\n scheduledAt: z\n .string()\n .optional()\n .describe('Optional future send time (ISO 8601). Omit to send immediately.'),\n },\n },\n async ({ id, scheduledAt }) => respond(await client.broadcasts.send(id, { scheduledAt })),\n );\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { Eusend } from '@eusend_dev/sdk';\nimport type { McpConfig } from './config.js';\nimport { registerEmailTools } from './tools/emails.js';\nimport { registerDomainTools } from './tools/domains.js';\nimport { registerAudienceTools } from './tools/audiences.js';\nimport { registerBroadcastTools } from './tools/broadcasts.js';\n\nexport const SERVER_NAME = 'eusend';\nexport const SERVER_VERSION = '0.1.1';\n\n/**\n * Build a fully-wired MCP server for one Eusend account. This is\n * transport-agnostic: the stdio entry (index.ts) and the HTTP entry (http.ts)\n * both call this and then attach their own transport. All tool definitions live\n * in ./tools and are registered here exactly once.\n */\nexport function createServer(config: McpConfig): McpServer {\n const server = new McpServer({\n name: SERVER_NAME,\n version: SERVER_VERSION,\n });\n\n const client = new Eusend(config.apiKey, { baseUrl: config.baseUrl });\n\n registerEmailTools(server, client, config);\n registerDomainTools(server, client);\n registerAudienceTools(server, client);\n registerBroadcastTools(server, client, config);\n\n return server;\n}\n","import type { IncomingMessage } from 'node:http';\n\n/**\n * Minimal in-memory fixed-window IP rate limiter for the public /mcp endpoint.\n *\n * The endpoint is unauthenticated at the transport layer (initialize/tools/list\n * work before any API key is validated), so it needs its own basic DoS ceiling\n * independent of the API's rate limiting. In-memory on purpose: it keeps the box\n * free of any external dependency or credential. Per-container state is fine here\n * — the limit is a coarse abuse ceiling, not an accounting mechanism.\n */\nexport interface RateLimiter {\n /** Returns true if the request is allowed, false if it should be rejected (429). */\n check(req: IncomingMessage): boolean;\n}\n\n// Hard cap on distinct tracked IPs so a high-cardinality flood can't grow the map\n// unbounded within a window. When exceeded, new IPs are denied until the next sweep\n// (fail closed under abuse) rather than consuming memory.\nconst MAX_BUCKETS = 50_000;\n\n/**\n * Trusted client IP. Coolify's Traefik sits in front and OVERWRITES X-Real-Ip with the\n * real connection address, so it can't be spoofed by a client-sent header. We deliberately\n * do NOT trust X-Forwarded-For here: Traefik appends the real IP to any client-supplied\n * XFF, so its leftmost entry is attacker-controlled and would let a caller mint a fresh\n * bucket per request. Falls back to the socket address when there's no proxy (local runs).\n */\nfunction clientIp(req: IncomingMessage): string {\n const realIp = req.headers['x-real-ip'];\n if (typeof realIp === 'string' && realIp.length > 0) {\n return realIp.trim();\n }\n return req.socket.remoteAddress ?? 'unknown';\n}\n\nexport function createRateLimiter(limit: number, windowMs: number): RateLimiter {\n const buckets = new Map<string, { count: number; resetAt: number }>();\n\n // Evict stale buckets so the map can't grow unbounded under a churn of IPs.\n const sweep = setInterval(() => {\n const now = Date.now();\n for (const [key, b] of buckets) {\n if (now > b.resetAt) buckets.delete(key);\n }\n }, windowMs);\n sweep.unref();\n\n return {\n check(req) {\n const key = clientIp(req);\n const now = Date.now();\n const bucket = buckets.get(key);\n if (!bucket || now > bucket.resetAt) {\n if (!bucket && buckets.size >= MAX_BUCKETS) {\n // Try to reclaim expired buckets before refusing.\n for (const [k, b] of buckets) if (now > b.resetAt) buckets.delete(k);\n if (buckets.size >= MAX_BUCKETS) return false;\n }\n buckets.set(key, { count: 1, resetAt: now + windowMs });\n return true;\n }\n bucket.count += 1;\n return bucket.count <= limit;\n },\n };\n}\n","import { createServer as createHttpServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport type { McpDefaults } from './config.js';\nimport { createServer } from './server.js';\nimport { createRateLimiter } from './ratelimit.js';\n\nexport interface RunHttpOptions {\n /** Account-agnostic defaults; the API key comes per-request from the caller. */\n defaults: McpDefaults;\n port: number;\n host: string;\n /** Hostnames allowed in the Host header (DNS-rebinding protection). */\n allowedHosts: string[];\n}\n\nconst MCP_PATH = '/mcp';\n\n// Coarse per-IP DoS ceiling for the public endpoint. Each JSON-RPC call over an\n// MCP session is one request, so this is generous for a real agent while still\n// capping an anonymous flood. Tune via RATE_LIMIT_PER_MIN if needed.\nconst RATE_LIMIT_PER_MIN = Number(process.env.RATE_LIMIT_PER_MIN ?? '120');\nconst rateLimiter = createRateLimiter(RATE_LIMIT_PER_MIN, 60_000);\n\nfunction json(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n}\n\n/** Pull the API key out of `Authorization: Bearer <key>`. */\nfunction bearerKey(req: IncomingMessage): string | undefined {\n const header = req.headers.authorization;\n if (!header) return undefined;\n const [scheme, value] = header.split(' ');\n return scheme?.toLowerCase() === 'bearer' && value ? value : undefined;\n}\n\nasync function readBody(req: IncomingMessage): Promise<unknown> {\n const chunks: Buffer[] = [];\n for await (const chunk of req) chunks.push(chunk as Buffer);\n if (chunks.length === 0) return undefined;\n return JSON.parse(Buffer.concat(chunks).toString('utf8'));\n}\n\n/**\n * Run the Eusend MCP server over Streamable HTTP — the hosted/remote transport.\n * Stateless: every request builds a fresh server bound to the caller's API key,\n * so one endpoint safely serves many accounts. Mount this behind TLS at, e.g.,\n * https://mcp.eusend.dev/mcp.\n */\nexport async function runHttp(options: RunHttpOptions): Promise<void> {\n const httpServer = createHttpServer((req, res) => {\n void handle(req, res, options);\n });\n\n await new Promise<void>((resolve) => {\n httpServer.listen(options.port, options.host, resolve);\n });\n console.error(`Eusend MCP server (HTTP) listening on http://${options.host}:${options.port}${MCP_PATH}`);\n}\n\nasync function handle(req: IncomingMessage, res: ServerResponse, options: RunHttpOptions): Promise<void> {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);\n\n if (url.pathname === '/health') {\n return json(res, 200, { status: 'ok' });\n }\n\n if (url.pathname !== MCP_PATH) {\n return json(res, 404, { error: 'Not found' });\n }\n\n // Stateless server: sessions aren't retained, so GET (server-push stream) and\n // DELETE (session teardown) have nothing to act on.\n if (req.method !== 'POST') {\n res.writeHead(405, { Allow: 'POST' });\n return void res.end();\n }\n\n if (!rateLimiter.check(req)) {\n return json(res, 429, {\n jsonrpc: '2.0',\n error: { code: -32029, message: 'Too many requests. Slow down.' },\n id: null,\n });\n }\n\n const apiKey = bearerKey(req);\n if (!apiKey) {\n return json(res, 401, {\n jsonrpc: '2.0',\n error: { code: -32001, message: 'Missing or malformed Authorization: Bearer <api-key> header.' },\n id: null,\n });\n }\n\n const server = createServer({ apiKey, ...options.defaults });\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // stateless\n enableDnsRebindingProtection: true,\n allowedHosts: options.allowedHosts,\n });\n\n res.on('close', () => {\n void transport.close();\n void server.close();\n });\n\n try {\n await server.connect(transport);\n const body = await readBody(req);\n await transport.handleRequest(req, res, body);\n } catch (err) {\n console.error('Error handling MCP request:', err);\n if (!res.headersSent) {\n json(res, 500, {\n jsonrpc: '2.0',\n error: { code: -32603, message: 'Internal server error' },\n id: null,\n });\n }\n }\n}\n"],"mappings":"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"} |
+1
-1
@@ -1,2 +0,2 @@ | ||
| import { t as runHttp } from "./http-BqJuS8Cy.mjs"; | ||
| import { t as runHttp } from "./http-BiRaRNNh.mjs"; | ||
| export { runHttp }; |
+2
-2
| #!/usr/bin/env node | ||
| import { n as createServer, t as runHttp } from "./http-BqJuS8Cy.mjs"; | ||
| import { n as createServer, t as runHttp } from "./http-BiRaRNNh.mjs"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
@@ -74,3 +74,3 @@ //#region src/config.ts | ||
| if (!options.config.apiKey) { | ||
| console.error("Missing Eusend API key. Set EUSEND_API_KEY (or pass --key). Get one at https://eusend.dev/settings/api-keys"); | ||
| console.error("Missing Eusend API key. Set EUSEND_API_KEY (or pass --key). Get one at https://eusend.dev/api-keys"); | ||
| process.exit(1); | ||
@@ -77,0 +77,0 @@ } |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.mjs","names":[],"sources":["../src/config.ts","../src/index.ts"],"sourcesContent":["/**\n * Runtime configuration for the Eusend MCP server.\n *\n * In stdio mode this is resolved once, at startup, from CLI flags and the\n * environment. In HTTP mode the `apiKey` is supplied per-request from the\n * caller's `Authorization: Bearer` header, while the defaults (`defaultFrom`,\n * `baseUrl`, …) come from the process environment shared across all callers.\n */\nexport interface McpConfig {\n /** Eusend API key used to authenticate every call the server makes. */\n apiKey: string;\n /** Override the API base URL (defaults to https://api.eusend.dev). */\n baseUrl?: string;\n /**\n * Sender address used when `send_email` / `create_broadcast` are called\n * without an explicit `from`. Pinning this here keeps an agent from\n * inventing an unverified sender on its first call.\n */\n defaultFrom?: string;\n /** Default reply-to used when a send omits one. */\n defaultReplyTo?: string;\n}\n\n/** Shared defaults with no API key — used by the HTTP server, which takes the key per-request. */\nexport type McpDefaults = Omit<McpConfig, 'apiKey'>;\n\nexport interface CliOptions {\n http: boolean;\n port: number;\n host: string;\n /** Hostnames permitted in HTTP mode, for DNS-rebinding protection. */\n allowedHosts: string[];\n config: McpConfig;\n}\n\nfunction readEnv(name: string): string | undefined {\n const v = process.env[name];\n return v && v.length > 0 ? v : undefined;\n}\n\n/**\n * Parse `process.argv` and the environment into resolved options.\n * Flags take precedence over environment variables.\n */\nexport function parseCliOptions(argv: string[]): CliOptions {\n const flags = new Map<string, string | boolean>();\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (!arg.startsWith('--')) continue;\n const key = arg.slice(2);\n const next = argv[i + 1];\n if (next === undefined || next.startsWith('--')) {\n flags.set(key, true);\n } else {\n flags.set(key, next);\n i++;\n }\n }\n\n const str = (key: string): string | undefined => {\n const v = flags.get(key);\n return typeof v === 'string' ? v : undefined;\n };\n\n const http = flags.get('http') === true || flags.get('http') === 'true';\n\n const apiKey = str('key') ?? readEnv('EUSEND_API_KEY') ?? '';\n const baseUrl = str('base-url') ?? readEnv('EUSEND_BASE_URL');\n const defaultFrom =\n str('from') ?? readEnv('EUSEND_FROM') ?? readEnv('SENDER_EMAIL_ADDRESS');\n const defaultReplyTo = str('reply-to') ?? readEnv('EUSEND_REPLY_TO');\n\n // PORT is read last as the platform-injected fallback (Coolify sets it to 3000).\n const port = Number(str('port') ?? readEnv('MCP_PORT') ?? readEnv('PORT') ?? '3000');\n const host = str('host') ?? readEnv('MCP_HOST') ?? '127.0.0.1';\n\n // The DNS-rebinding check matches the Host header verbatim, which includes the\n // port. Default the allowlist to both bare and port-qualified local forms.\n // Behind a reverse proxy at mcp.eusend.dev, pass --allowed-hosts explicitly.\n const allowedHostsRaw = str('allowed-hosts') ?? readEnv('MCP_ALLOWED_HOSTS');\n const allowedHosts = allowedHostsRaw\n ? allowedHostsRaw.split(',').map((h) => h.trim()).filter(Boolean)\n : ['127.0.0.1', 'localhost', `127.0.0.1:${port}`, `localhost:${port}`];\n\n return {\n http,\n port,\n host,\n allowedHosts,\n config: { apiKey, baseUrl, defaultFrom, defaultReplyTo },\n };\n}\n","#!/usr/bin/env node\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { parseCliOptions } from './config.js';\nimport { createServer } from './server.js';\nimport { runHttp } from './http.js';\n\nasync function main(): Promise<void> {\n const options = parseCliOptions(process.argv.slice(2));\n\n if (options.http) {\n // Remote/hosted mode: one HTTP endpoint, API key taken per-request from\n // the Authorization header. Same tools, different transport.\n await runHttp({\n defaults: {\n baseUrl: options.config.baseUrl,\n defaultFrom: options.config.defaultFrom,\n defaultReplyTo: options.config.defaultReplyTo,\n },\n port: options.port,\n host: options.host,\n allowedHosts: options.allowedHosts,\n });\n return;\n }\n\n // Local mode: the client launched us as a subprocess and talks over stdio.\n if (!options.config.apiKey) {\n // stderr, not stdout — stdout is the MCP protocol channel and must stay clean.\n console.error(\n 'Missing Eusend API key. Set EUSEND_API_KEY (or pass --key). Get one at https://eusend.dev/settings/api-keys',\n );\n process.exit(1);\n }\n\n const server = createServer(options.config);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error('Eusend MCP server running on stdio.');\n}\n\nmain().catch((err) => {\n console.error('Fatal error starting Eusend MCP server:', err);\n process.exit(1);\n});\n"],"mappings":";;;;AAmCA,SAAS,QAAQ,MAAkC;CACjD,MAAM,IAAI,QAAQ,IAAI;CACtB,OAAO,KAAK,EAAE,SAAS,IAAI,IAAI,KAAA;AACjC;;;;;AAMA,SAAgB,gBAAgB,MAA4B;CAC1D,MAAM,wBAAQ,IAAI,IAA8B;CAChD,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,IAAI,CAAC,IAAI,WAAW,IAAI,GAAG;EAC3B,MAAM,MAAM,IAAI,MAAM,CAAC;EACvB,MAAM,OAAO,KAAK,IAAI;EACtB,IAAI,SAAS,KAAA,KAAa,KAAK,WAAW,IAAI,GAC5C,MAAM,IAAI,KAAK,IAAI;OACd;GACL,MAAM,IAAI,KAAK,IAAI;GACnB;EACF;CACF;CAEA,MAAM,OAAO,QAAoC;EAC/C,MAAM,IAAI,MAAM,IAAI,GAAG;EACvB,OAAO,OAAO,MAAM,WAAW,IAAI,KAAA;CACrC;CAEA,MAAM,OAAO,MAAM,IAAI,MAAM,MAAM,QAAQ,MAAM,IAAI,MAAM,MAAM;CAEjE,MAAM,SAAS,IAAI,KAAK,KAAK,QAAQ,gBAAgB,KAAK;CAC1D,MAAM,UAAU,IAAI,UAAU,KAAK,QAAQ,iBAAiB;CAC5D,MAAM,cACJ,IAAI,MAAM,KAAK,QAAQ,aAAa,KAAK,QAAQ,sBAAsB;CACzE,MAAM,iBAAiB,IAAI,UAAU,KAAK,QAAQ,iBAAiB;CAGnE,MAAM,OAAO,OAAO,IAAI,MAAM,KAAK,QAAQ,UAAU,KAAK,QAAQ,MAAM,KAAK,MAAM;CACnF,MAAM,OAAO,IAAI,MAAM,KAAK,QAAQ,UAAU,KAAK;CAKnD,MAAM,kBAAkB,IAAI,eAAe,KAAK,QAAQ,mBAAmB;CAK3E,OAAO;EACL;EACA;EACA;EACA,cARmB,kBACjB,gBAAgB,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,OAAO,IAC9D;GAAC;GAAa;GAAa,aAAa;GAAQ,aAAa;EAAM;EAOrE,QAAQ;GAAE;GAAQ;GAAS;GAAa;EAAe;CACzD;AACF;;;ACrFA,eAAe,OAAsB;CACnC,MAAM,UAAU,gBAAgB,QAAQ,KAAK,MAAM,CAAC,CAAC;CAErD,IAAI,QAAQ,MAAM;EAGhB,MAAM,QAAQ;GACZ,UAAU;IACR,SAAS,QAAQ,OAAO;IACxB,aAAa,QAAQ,OAAO;IAC5B,gBAAgB,QAAQ,OAAO;GACjC;GACA,MAAM,QAAQ;GACd,MAAM,QAAQ;GACd,cAAc,QAAQ;EACxB,CAAC;EACD;CACF;CAGA,IAAI,CAAC,QAAQ,OAAO,QAAQ;EAE1B,QAAQ,MACN,6GACF;EACA,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,SAAS,aAAa,QAAQ,MAAM;CAC1C,MAAM,YAAY,IAAI,qBAAqB;CAC3C,MAAM,OAAO,QAAQ,SAAS;CAC9B,QAAQ,MAAM,qCAAqC;AACrD;AAEA,KAAK,CAAC,CAAC,OAAO,QAAQ;CACpB,QAAQ,MAAM,2CAA2C,GAAG;CAC5D,QAAQ,KAAK,CAAC;AAChB,CAAC"} | ||
| {"version":3,"file":"index.mjs","names":[],"sources":["../src/config.ts","../src/index.ts"],"sourcesContent":["/**\n * Runtime configuration for the Eusend MCP server.\n *\n * In stdio mode this is resolved once, at startup, from CLI flags and the\n * environment. In HTTP mode the `apiKey` is supplied per-request from the\n * caller's `Authorization: Bearer` header, while the defaults (`defaultFrom`,\n * `baseUrl`, …) come from the process environment shared across all callers.\n */\nexport interface McpConfig {\n /** Eusend API key used to authenticate every call the server makes. */\n apiKey: string;\n /** Override the API base URL (defaults to https://api.eusend.dev). */\n baseUrl?: string;\n /**\n * Sender address used when `send_email` / `create_broadcast` are called\n * without an explicit `from`. Pinning this here keeps an agent from\n * inventing an unverified sender on its first call.\n */\n defaultFrom?: string;\n /** Default reply-to used when a send omits one. */\n defaultReplyTo?: string;\n}\n\n/** Shared defaults with no API key — used by the HTTP server, which takes the key per-request. */\nexport type McpDefaults = Omit<McpConfig, 'apiKey'>;\n\nexport interface CliOptions {\n http: boolean;\n port: number;\n host: string;\n /** Hostnames permitted in HTTP mode, for DNS-rebinding protection. */\n allowedHosts: string[];\n config: McpConfig;\n}\n\nfunction readEnv(name: string): string | undefined {\n const v = process.env[name];\n return v && v.length > 0 ? v : undefined;\n}\n\n/**\n * Parse `process.argv` and the environment into resolved options.\n * Flags take precedence over environment variables.\n */\nexport function parseCliOptions(argv: string[]): CliOptions {\n const flags = new Map<string, string | boolean>();\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (!arg.startsWith('--')) continue;\n const key = arg.slice(2);\n const next = argv[i + 1];\n if (next === undefined || next.startsWith('--')) {\n flags.set(key, true);\n } else {\n flags.set(key, next);\n i++;\n }\n }\n\n const str = (key: string): string | undefined => {\n const v = flags.get(key);\n return typeof v === 'string' ? v : undefined;\n };\n\n const http = flags.get('http') === true || flags.get('http') === 'true';\n\n const apiKey = str('key') ?? readEnv('EUSEND_API_KEY') ?? '';\n const baseUrl = str('base-url') ?? readEnv('EUSEND_BASE_URL');\n const defaultFrom =\n str('from') ?? readEnv('EUSEND_FROM') ?? readEnv('SENDER_EMAIL_ADDRESS');\n const defaultReplyTo = str('reply-to') ?? readEnv('EUSEND_REPLY_TO');\n\n // PORT is read last as the platform-injected fallback (Coolify sets it to 3000).\n const port = Number(str('port') ?? readEnv('MCP_PORT') ?? readEnv('PORT') ?? '3000');\n const host = str('host') ?? readEnv('MCP_HOST') ?? '127.0.0.1';\n\n // The DNS-rebinding check matches the Host header verbatim, which includes the\n // port. Default the allowlist to both bare and port-qualified local forms.\n // Behind a reverse proxy at mcp.eusend.dev, pass --allowed-hosts explicitly.\n const allowedHostsRaw = str('allowed-hosts') ?? readEnv('MCP_ALLOWED_HOSTS');\n const allowedHosts = allowedHostsRaw\n ? allowedHostsRaw.split(',').map((h) => h.trim()).filter(Boolean)\n : ['127.0.0.1', 'localhost', `127.0.0.1:${port}`, `localhost:${port}`];\n\n return {\n http,\n port,\n host,\n allowedHosts,\n config: { apiKey, baseUrl, defaultFrom, defaultReplyTo },\n };\n}\n","#!/usr/bin/env node\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { parseCliOptions } from './config.js';\nimport { createServer } from './server.js';\nimport { runHttp } from './http.js';\n\nasync function main(): Promise<void> {\n const options = parseCliOptions(process.argv.slice(2));\n\n if (options.http) {\n // Remote/hosted mode: one HTTP endpoint, API key taken per-request from\n // the Authorization header. Same tools, different transport.\n await runHttp({\n defaults: {\n baseUrl: options.config.baseUrl,\n defaultFrom: options.config.defaultFrom,\n defaultReplyTo: options.config.defaultReplyTo,\n },\n port: options.port,\n host: options.host,\n allowedHosts: options.allowedHosts,\n });\n return;\n }\n\n // Local mode: the client launched us as a subprocess and talks over stdio.\n if (!options.config.apiKey) {\n // stderr, not stdout — stdout is the MCP protocol channel and must stay clean.\n console.error(\n 'Missing Eusend API key. Set EUSEND_API_KEY (or pass --key). Get one at https://eusend.dev/api-keys',\n );\n process.exit(1);\n }\n\n const server = createServer(options.config);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error('Eusend MCP server running on stdio.');\n}\n\nmain().catch((err) => {\n console.error('Fatal error starting Eusend MCP server:', err);\n process.exit(1);\n});\n"],"mappings":";;;;AAmCA,SAAS,QAAQ,MAAkC;CACjD,MAAM,IAAI,QAAQ,IAAI;CACtB,OAAO,KAAK,EAAE,SAAS,IAAI,IAAI,KAAA;AACjC;;;;;AAMA,SAAgB,gBAAgB,MAA4B;CAC1D,MAAM,wBAAQ,IAAI,IAA8B;CAChD,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,IAAI,CAAC,IAAI,WAAW,IAAI,GAAG;EAC3B,MAAM,MAAM,IAAI,MAAM,CAAC;EACvB,MAAM,OAAO,KAAK,IAAI;EACtB,IAAI,SAAS,KAAA,KAAa,KAAK,WAAW,IAAI,GAC5C,MAAM,IAAI,KAAK,IAAI;OACd;GACL,MAAM,IAAI,KAAK,IAAI;GACnB;EACF;CACF;CAEA,MAAM,OAAO,QAAoC;EAC/C,MAAM,IAAI,MAAM,IAAI,GAAG;EACvB,OAAO,OAAO,MAAM,WAAW,IAAI,KAAA;CACrC;CAEA,MAAM,OAAO,MAAM,IAAI,MAAM,MAAM,QAAQ,MAAM,IAAI,MAAM,MAAM;CAEjE,MAAM,SAAS,IAAI,KAAK,KAAK,QAAQ,gBAAgB,KAAK;CAC1D,MAAM,UAAU,IAAI,UAAU,KAAK,QAAQ,iBAAiB;CAC5D,MAAM,cACJ,IAAI,MAAM,KAAK,QAAQ,aAAa,KAAK,QAAQ,sBAAsB;CACzE,MAAM,iBAAiB,IAAI,UAAU,KAAK,QAAQ,iBAAiB;CAGnE,MAAM,OAAO,OAAO,IAAI,MAAM,KAAK,QAAQ,UAAU,KAAK,QAAQ,MAAM,KAAK,MAAM;CACnF,MAAM,OAAO,IAAI,MAAM,KAAK,QAAQ,UAAU,KAAK;CAKnD,MAAM,kBAAkB,IAAI,eAAe,KAAK,QAAQ,mBAAmB;CAK3E,OAAO;EACL;EACA;EACA;EACA,cARmB,kBACjB,gBAAgB,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,OAAO,IAC9D;GAAC;GAAa;GAAa,aAAa;GAAQ,aAAa;EAAM;EAOrE,QAAQ;GAAE;GAAQ;GAAS;GAAa;EAAe;CACzD;AACF;;;ACrFA,eAAe,OAAsB;CACnC,MAAM,UAAU,gBAAgB,QAAQ,KAAK,MAAM,CAAC,CAAC;CAErD,IAAI,QAAQ,MAAM;EAGhB,MAAM,QAAQ;GACZ,UAAU;IACR,SAAS,QAAQ,OAAO;IACxB,aAAa,QAAQ,OAAO;IAC5B,gBAAgB,QAAQ,OAAO;GACjC;GACA,MAAM,QAAQ;GACd,MAAM,QAAQ;GACd,cAAc,QAAQ;EACxB,CAAC;EACD;CACF;CAGA,IAAI,CAAC,QAAQ,OAAO,QAAQ;EAE1B,QAAQ,MACN,oGACF;EACA,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,SAAS,aAAa,QAAQ,MAAM;CAC1C,MAAM,YAAY,IAAI,qBAAqB;CAC3C,MAAM,OAAO,QAAQ,SAAS;CAC9B,QAAQ,MAAM,qCAAqC;AACrD;AAEA,KAAK,CAAC,CAAC,OAAO,QAAQ;CACpB,QAAQ,MAAM,2CAA2C,GAAG;CAC5D,QAAQ,KAAK,CAAC;AAChB,CAAC"} |
+1
-1
| { | ||
| "name": "@eusend_dev/mcp", | ||
| "version": "0.1.0", | ||
| "version": "0.1.1", | ||
| "description": "Model Context Protocol server for the Eusend API — send email and manage domains, audiences, and broadcasts from any MCP client.", | ||
@@ -5,0 +5,0 @@ "author": "Eusend", |
+1
-1
@@ -25,3 +25,3 @@ # @eusend_dev/mcp | ||
| Get an API key at <https://eusend.dev/settings/api-keys>. | ||
| Get an API key at <https://eusend.dev/api-keys>. | ||
@@ -28,0 +28,0 @@ ### Claude Code |
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { Eusend } from "@eusend_dev/sdk"; | ||
| import { z } from "zod"; | ||
| import { createServer } from "node:http"; | ||
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | ||
| //#region src/respond.ts | ||
| /** Wrap a successful payload as pretty-printed JSON text content. */ | ||
| function ok(data) { | ||
| return { content: [{ | ||
| type: "text", | ||
| text: JSON.stringify(data, null, 2) | ||
| }] }; | ||
| } | ||
| /** | ||
| * Surface an SDK error as an MCP tool error. `isError: true` tells the client | ||
| * the call failed, and the error `name` (e.g. DOMAIN_NOT_VERIFIED) gives the | ||
| * agent enough to self-correct — e.g. verify the domain, then retry. | ||
| */ | ||
| function fail(error) { | ||
| return { | ||
| content: [{ | ||
| type: "text", | ||
| text: `Error [${error.name}]: ${error.message}` | ||
| }], | ||
| isError: true | ||
| }; | ||
| } | ||
| /** Plain text error for validation the tool does itself before hitting the API. */ | ||
| function failText(message) { | ||
| return { | ||
| content: [{ | ||
| type: "text", | ||
| text: message | ||
| }], | ||
| isError: true | ||
| }; | ||
| } | ||
| /** Turn any SDK `EusendResponse` into a tool result. */ | ||
| function respond(res) { | ||
| return res.error ? fail(res.error) : ok(res.data); | ||
| } | ||
| //#endregion | ||
| //#region src/tools/emails.ts | ||
| const emailStatus = z.enum([ | ||
| "queued", | ||
| "scheduled", | ||
| "canceled", | ||
| "sending", | ||
| "sent", | ||
| "delivered", | ||
| "bounced", | ||
| "complained", | ||
| "suppressed", | ||
| "failed" | ||
| ]); | ||
| const recipients = z.union([z.string(), z.array(z.string())]); | ||
| function registerEmailTools(server, client, config) { | ||
| server.registerTool("send_email", { | ||
| title: "Send an email", | ||
| description: "Send a transactional email through Eusend. Provide `html` or `text` (or both). The sender domain must be verified on the account — use list_domains to check. If `from` is omitted, the server-configured default sender is used.", | ||
| inputSchema: { | ||
| from: z.string().optional().describe("Sender, e.g. \"Acme <hi@acme.com>\". Defaults to the configured sender."), | ||
| to: recipients.describe("Recipient address or list of addresses."), | ||
| subject: z.string().describe("Email subject line."), | ||
| text: z.string().optional().describe("Plain-text body."), | ||
| html: z.string().optional().describe("HTML body. Takes precedence over text for display."), | ||
| cc: recipients.optional(), | ||
| bcc: recipients.optional(), | ||
| replyTo: recipients.optional().describe("Reply-To address(es)."), | ||
| scheduledAt: z.string().optional().describe("Future send time: ISO 8601 or natural language like \"in 1 hour\". Max 30 days out.") | ||
| } | ||
| }, async (args) => { | ||
| const from = args.from ?? config.defaultFrom; | ||
| if (!from) return failText("No sender specified and no default sender configured. Pass `from`, or set EUSEND_FROM / --from on the server."); | ||
| if (!args.html && !args.text) return failText("Provide at least one of `html` or `text`."); | ||
| return respond(await client.emails.send({ | ||
| from, | ||
| to: args.to, | ||
| subject: args.subject, | ||
| text: args.text, | ||
| html: args.html, | ||
| cc: args.cc, | ||
| bcc: args.bcc, | ||
| replyTo: args.replyTo ?? config.defaultReplyTo, | ||
| scheduledAt: args.scheduledAt | ||
| })); | ||
| }); | ||
| server.registerTool("list_emails", { | ||
| title: "List emails", | ||
| description: "List recently sent emails, most recent first. Supports cursor pagination and filtering.", | ||
| inputSchema: { | ||
| limit: z.number().int().min(1).max(100).optional().describe("Max results (1–100)."), | ||
| cursor: z.string().optional().describe("Pagination cursor from a previous nextCursor."), | ||
| status: emailStatus.optional().describe("Filter by delivery status."), | ||
| from: z.string().optional().describe("Filter by sender address."), | ||
| to: z.string().optional().describe("Filter by recipient address.") | ||
| } | ||
| }, async (args) => respond(await client.emails.list(args))); | ||
| server.registerTool("get_email", { | ||
| title: "Get an email", | ||
| description: "Fetch a single email by id, including its full delivery event history.", | ||
| inputSchema: { id: z.string().describe("The email id returned by send_email or list_emails.") } | ||
| }, async ({ id }) => respond(await client.emails.get(id))); | ||
| server.registerTool("cancel_email", { | ||
| title: "Cancel a scheduled email", | ||
| description: "Cancel a scheduled email before it sends. Fails once the email has started sending.", | ||
| inputSchema: { id: z.string().describe("The id of a scheduled email.") } | ||
| }, async ({ id }) => respond(await client.emails.cancel(id))); | ||
| } | ||
| //#endregion | ||
| //#region src/tools/domains.ts | ||
| function registerDomainTools(server, client) { | ||
| server.registerTool("list_domains", { | ||
| title: "List domains", | ||
| description: "List sending domains on the account and their verification status.", | ||
| inputSchema: {} | ||
| }, async () => respond(await client.domains.list())); | ||
| server.registerTool("get_domain", { | ||
| title: "Get a domain", | ||
| description: "Fetch a single domain by id, including its DKIM selector and verification state.", | ||
| inputSchema: { id: z.string().describe("The domain id.") } | ||
| }, async ({ id }) => respond(await client.domains.get(id))); | ||
| server.registerTool("create_domain", { | ||
| title: "Add a sending domain", | ||
| description: "Register a new sending domain. Returns the DKIM, SPF, and DMARC DNS records that must be added at the domain's DNS provider before it can send. Present these records to the user.", | ||
| inputSchema: { name: z.string().describe("The domain to add, e.g. \"mail.acme.com\".") } | ||
| }, async ({ name }) => respond(await client.domains.create(name))); | ||
| } | ||
| //#endregion | ||
| //#region src/tools/audiences.ts | ||
| function registerAudienceTools(server, client) { | ||
| server.registerTool("list_audiences", { | ||
| title: "List audiences", | ||
| description: "List contact audiences (lists) on the account, with per-audience contact counts.", | ||
| inputSchema: {} | ||
| }, async () => respond(await client.audiences.list())); | ||
| server.registerTool("create_audience", { | ||
| title: "Create an audience", | ||
| description: "Create a new audience (contact list) to hold contacts for broadcasts.", | ||
| inputSchema: { name: z.string().describe("Human-readable name for the audience.") } | ||
| }, async ({ name }) => respond(await client.audiences.create(name))); | ||
| server.registerTool("list_contacts", { | ||
| title: "List contacts", | ||
| description: "List contacts in an audience. Supports search and subscription filtering.", | ||
| inputSchema: { | ||
| audienceId: z.string().describe("The audience to list contacts from."), | ||
| limit: z.number().int().min(1).max(100).optional(), | ||
| cursor: z.string().optional().describe("Pagination cursor from a previous nextCursor."), | ||
| search: z.string().optional().describe("Filter by email or name substring."), | ||
| subscribed: z.boolean().optional().describe("true = only subscribed, false = only unsubscribed.") | ||
| } | ||
| }, async ({ audienceId, ...opts }) => respond(await client.audiences.listContacts(audienceId, opts))); | ||
| server.registerTool("create_contact", { | ||
| title: "Add a contact", | ||
| description: "Add a contact to an audience.", | ||
| inputSchema: { | ||
| audienceId: z.string().describe("The audience to add the contact to."), | ||
| email: z.string().describe("Contact email address."), | ||
| firstName: z.string().optional(), | ||
| lastName: z.string().optional() | ||
| } | ||
| }, async ({ audienceId, email, firstName, lastName }) => respond(await client.audiences.createContact(audienceId, { | ||
| email, | ||
| firstName, | ||
| lastName | ||
| }))); | ||
| } | ||
| //#endregion | ||
| //#region src/tools/broadcasts.ts | ||
| function registerBroadcastTools(server, client, config) { | ||
| server.registerTool("list_broadcasts", { | ||
| title: "List broadcasts", | ||
| description: "List broadcasts (bulk sends to an audience) with their status and send counts.", | ||
| inputSchema: {} | ||
| }, async () => respond(await client.broadcasts.list())); | ||
| server.registerTool("get_broadcast", { | ||
| title: "Get a broadcast", | ||
| description: "Fetch a broadcast by id, including recipient/sent counts and per-event stats.", | ||
| inputSchema: { id: z.string().describe("The broadcast id.") } | ||
| }, async ({ id }) => respond(await client.broadcasts.get(id))); | ||
| server.registerTool("create_broadcast", { | ||
| title: "Create a broadcast (draft)", | ||
| description: "Create a broadcast in draft state, targeting an audience. Does NOT send it — use send_broadcast to send or schedule after reviewing. If `from` is omitted the configured default sender is used.", | ||
| inputSchema: { | ||
| name: z.string().describe("Internal name for the broadcast."), | ||
| audienceId: z.string().describe("The audience to send to."), | ||
| subject: z.string().describe("Email subject line."), | ||
| from: z.string().optional().describe("Sender. Defaults to the configured sender."), | ||
| html: z.string().optional().describe("HTML body of the broadcast.") | ||
| } | ||
| }, async (args) => { | ||
| const from = args.from ?? config.defaultFrom; | ||
| if (!from) return failText("No sender specified and no default sender configured. Pass `from`, or set EUSEND_FROM / --from on the server."); | ||
| return respond(await client.broadcasts.create({ | ||
| name: args.name, | ||
| audienceId: args.audienceId, | ||
| subject: args.subject, | ||
| from, | ||
| html: args.html | ||
| })); | ||
| }); | ||
| server.registerTool("send_broadcast", { | ||
| title: "Send or schedule a broadcast", | ||
| description: "Send a draft broadcast to its entire audience now, or schedule it for later. This is a bulk send — confirm with the user before calling. Omit `scheduledAt` to send immediately.", | ||
| inputSchema: { | ||
| id: z.string().describe("The draft broadcast id."), | ||
| scheduledAt: z.string().optional().describe("Optional future send time (ISO 8601). Omit to send immediately.") | ||
| } | ||
| }, async ({ id, scheduledAt }) => respond(await client.broadcasts.send(id, { scheduledAt }))); | ||
| } | ||
| //#endregion | ||
| //#region src/server.ts | ||
| const SERVER_NAME = "eusend"; | ||
| const SERVER_VERSION = "0.1.0"; | ||
| /** | ||
| * Build a fully-wired MCP server for one Eusend account. This is | ||
| * transport-agnostic: the stdio entry (index.ts) and the HTTP entry (http.ts) | ||
| * both call this and then attach their own transport. All tool definitions live | ||
| * in ./tools and are registered here exactly once. | ||
| */ | ||
| function createServer$1(config) { | ||
| const server = new McpServer({ | ||
| name: SERVER_NAME, | ||
| version: SERVER_VERSION | ||
| }); | ||
| const client = new Eusend(config.apiKey, { baseUrl: config.baseUrl }); | ||
| registerEmailTools(server, client, config); | ||
| registerDomainTools(server, client); | ||
| registerAudienceTools(server, client); | ||
| registerBroadcastTools(server, client, config); | ||
| return server; | ||
| } | ||
| //#endregion | ||
| //#region src/ratelimit.ts | ||
| /** Best-effort client IP, honoring the proxy's X-Forwarded-For (Coolify/Traefik sits in front). */ | ||
| function clientIp(req) { | ||
| const xff = req.headers["x-forwarded-for"]; | ||
| if (typeof xff === "string" && xff.length > 0) return xff.split(",")[0].trim(); | ||
| return req.socket.remoteAddress ?? "unknown"; | ||
| } | ||
| function createRateLimiter(limit, windowMs) { | ||
| const buckets = /* @__PURE__ */ new Map(); | ||
| setInterval(() => { | ||
| const now = Date.now(); | ||
| for (const [key, b] of buckets) if (now > b.resetAt) buckets.delete(key); | ||
| }, windowMs).unref(); | ||
| return { check(req) { | ||
| const key = clientIp(req); | ||
| const now = Date.now(); | ||
| const bucket = buckets.get(key); | ||
| if (!bucket || now > bucket.resetAt) { | ||
| buckets.set(key, { | ||
| count: 1, | ||
| resetAt: now + windowMs | ||
| }); | ||
| return true; | ||
| } | ||
| bucket.count += 1; | ||
| return bucket.count <= limit; | ||
| } }; | ||
| } | ||
| //#endregion | ||
| //#region src/http.ts | ||
| const MCP_PATH = "/mcp"; | ||
| const rateLimiter = createRateLimiter(Number(process.env.RATE_LIMIT_PER_MIN ?? "120"), 6e4); | ||
| function json(res, status, body) { | ||
| res.writeHead(status, { "Content-Type": "application/json" }); | ||
| res.end(JSON.stringify(body)); | ||
| } | ||
| /** Pull the API key out of `Authorization: Bearer <key>`. */ | ||
| function bearerKey(req) { | ||
| const header = req.headers.authorization; | ||
| if (!header) return void 0; | ||
| const [scheme, value] = header.split(" "); | ||
| return scheme?.toLowerCase() === "bearer" && value ? value : void 0; | ||
| } | ||
| async function readBody(req) { | ||
| const chunks = []; | ||
| for await (const chunk of req) chunks.push(chunk); | ||
| if (chunks.length === 0) return void 0; | ||
| return JSON.parse(Buffer.concat(chunks).toString("utf8")); | ||
| } | ||
| /** | ||
| * Run the Eusend MCP server over Streamable HTTP — the hosted/remote transport. | ||
| * Stateless: every request builds a fresh server bound to the caller's API key, | ||
| * so one endpoint safely serves many accounts. Mount this behind TLS at, e.g., | ||
| * https://mcp.eusend.dev/mcp. | ||
| */ | ||
| async function runHttp(options) { | ||
| const httpServer = createServer((req, res) => { | ||
| handle(req, res, options); | ||
| }); | ||
| await new Promise((resolve) => { | ||
| httpServer.listen(options.port, options.host, resolve); | ||
| }); | ||
| console.error(`Eusend MCP server (HTTP) listening on http://${options.host}:${options.port}${MCP_PATH}`); | ||
| } | ||
| async function handle(req, res, options) { | ||
| const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); | ||
| if (url.pathname === "/health") return json(res, 200, { status: "ok" }); | ||
| if (url.pathname !== MCP_PATH) return json(res, 404, { error: "Not found" }); | ||
| if (req.method !== "POST") { | ||
| res.writeHead(405, { Allow: "POST" }); | ||
| res.end(); | ||
| return; | ||
| } | ||
| if (!rateLimiter.check(req)) return json(res, 429, { | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32029, | ||
| message: "Too many requests. Slow down." | ||
| }, | ||
| id: null | ||
| }); | ||
| const apiKey = bearerKey(req); | ||
| if (!apiKey) return json(res, 401, { | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32001, | ||
| message: "Missing or malformed Authorization: Bearer <api-key> header." | ||
| }, | ||
| id: null | ||
| }); | ||
| const server = createServer$1({ | ||
| apiKey, | ||
| ...options.defaults | ||
| }); | ||
| const transport = new StreamableHTTPServerTransport({ | ||
| sessionIdGenerator: void 0, | ||
| enableDnsRebindingProtection: true, | ||
| allowedHosts: options.allowedHosts | ||
| }); | ||
| res.on("close", () => { | ||
| transport.close(); | ||
| server.close(); | ||
| }); | ||
| try { | ||
| await server.connect(transport); | ||
| const body = await readBody(req); | ||
| await transport.handleRequest(req, res, body); | ||
| } catch (err) { | ||
| console.error("Error handling MCP request:", err); | ||
| if (!res.headersSent) json(res, 500, { | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32603, | ||
| message: "Internal server error" | ||
| }, | ||
| id: null | ||
| }); | ||
| } | ||
| } | ||
| //#endregion | ||
| export { createServer$1 as n, runHttp as t }; | ||
| //# sourceMappingURL=http-BqJuS8Cy.mjs.map |
| {"version":3,"file":"http-BqJuS8Cy.mjs","names":["createServer","createHttpServer","createServer"],"sources":["../src/respond.ts","../src/tools/emails.ts","../src/tools/domains.ts","../src/tools/audiences.ts","../src/tools/broadcasts.ts","../src/server.ts","../src/ratelimit.ts","../src/http.ts"],"sourcesContent":["import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { EusendError, EusendResponse } from '@eusend_dev/sdk';\n\n/** MCP tool return shape. */\nexport type ToolResult = CallToolResult;\n\n/** Wrap a successful payload as pretty-printed JSON text content. */\nexport function ok(data: unknown): ToolResult {\n return {\n content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],\n };\n}\n\n/**\n * Surface an SDK error as an MCP tool error. `isError: true` tells the client\n * the call failed, and the error `name` (e.g. DOMAIN_NOT_VERIFIED) gives the\n * agent enough to self-correct — e.g. verify the domain, then retry.\n */\nexport function fail(error: EusendError): ToolResult {\n return {\n content: [{ type: 'text', text: `Error [${error.name}]: ${error.message}` }],\n isError: true,\n };\n}\n\n/** Plain text error for validation the tool does itself before hitting the API. */\nexport function failText(message: string): ToolResult {\n return { content: [{ type: 'text', text: message }], isError: true };\n}\n\n/** Turn any SDK `EusendResponse` into a tool result. */\nexport function respond<T>(res: EusendResponse<T>): ToolResult {\n return res.error ? fail(res.error) : ok(res.data);\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Eusend } from '@eusend_dev/sdk';\nimport { z } from 'zod';\nimport type { McpConfig } from '../config.js';\nimport { respond, failText } from '../respond.js';\n\nconst emailStatus = z.enum([\n 'queued',\n 'scheduled',\n 'canceled',\n 'sending',\n 'sent',\n 'delivered',\n 'bounced',\n 'complained',\n 'suppressed',\n 'failed',\n]);\n\nconst recipients = z.union([z.string(), z.array(z.string())]);\n\nexport function registerEmailTools(server: McpServer, client: Eusend, config: McpConfig): void {\n server.registerTool(\n 'send_email',\n {\n title: 'Send an email',\n description:\n 'Send a transactional email through Eusend. Provide `html` or `text` (or both). ' +\n 'The sender domain must be verified on the account — use list_domains to check. ' +\n 'If `from` is omitted, the server-configured default sender is used.',\n inputSchema: {\n from: z\n .string()\n .optional()\n .describe('Sender, e.g. \"Acme <hi@acme.com>\". Defaults to the configured sender.'),\n to: recipients.describe('Recipient address or list of addresses.'),\n subject: z.string().describe('Email subject line.'),\n text: z.string().optional().describe('Plain-text body.'),\n html: z.string().optional().describe('HTML body. Takes precedence over text for display.'),\n cc: recipients.optional(),\n bcc: recipients.optional(),\n replyTo: recipients.optional().describe('Reply-To address(es).'),\n scheduledAt: z\n .string()\n .optional()\n .describe('Future send time: ISO 8601 or natural language like \"in 1 hour\". Max 30 days out.'),\n },\n },\n async (args) => {\n const from = args.from ?? config.defaultFrom;\n if (!from) {\n return failText(\n 'No sender specified and no default sender configured. Pass `from`, or set EUSEND_FROM / --from on the server.',\n );\n }\n if (!args.html && !args.text) {\n return failText('Provide at least one of `html` or `text`.');\n }\n return respond(\n await client.emails.send({\n from,\n to: args.to,\n subject: args.subject,\n text: args.text,\n html: args.html,\n cc: args.cc,\n bcc: args.bcc,\n replyTo: args.replyTo ?? config.defaultReplyTo,\n scheduledAt: args.scheduledAt,\n }),\n );\n },\n );\n\n server.registerTool(\n 'list_emails',\n {\n title: 'List emails',\n description: 'List recently sent emails, most recent first. Supports cursor pagination and filtering.',\n inputSchema: {\n limit: z.number().int().min(1).max(100).optional().describe('Max results (1–100).'),\n cursor: z.string().optional().describe('Pagination cursor from a previous nextCursor.'),\n status: emailStatus.optional().describe('Filter by delivery status.'),\n from: z.string().optional().describe('Filter by sender address.'),\n to: z.string().optional().describe('Filter by recipient address.'),\n },\n },\n async (args) => respond(await client.emails.list(args)),\n );\n\n server.registerTool(\n 'get_email',\n {\n title: 'Get an email',\n description: 'Fetch a single email by id, including its full delivery event history.',\n inputSchema: {\n id: z.string().describe('The email id returned by send_email or list_emails.'),\n },\n },\n async ({ id }) => respond(await client.emails.get(id)),\n );\n\n server.registerTool(\n 'cancel_email',\n {\n title: 'Cancel a scheduled email',\n description: 'Cancel a scheduled email before it sends. Fails once the email has started sending.',\n inputSchema: {\n id: z.string().describe('The id of a scheduled email.'),\n },\n },\n async ({ id }) => respond(await client.emails.cancel(id)),\n );\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Eusend } from '@eusend_dev/sdk';\nimport { z } from 'zod';\nimport { respond } from '../respond.js';\n\nexport function registerDomainTools(server: McpServer, client: Eusend): void {\n server.registerTool(\n 'list_domains',\n {\n title: 'List domains',\n description: 'List sending domains on the account and their verification status.',\n inputSchema: {},\n },\n async () => respond(await client.domains.list()),\n );\n\n server.registerTool(\n 'get_domain',\n {\n title: 'Get a domain',\n description: 'Fetch a single domain by id, including its DKIM selector and verification state.',\n inputSchema: {\n id: z.string().describe('The domain id.'),\n },\n },\n async ({ id }) => respond(await client.domains.get(id)),\n );\n\n server.registerTool(\n 'create_domain',\n {\n title: 'Add a sending domain',\n description:\n 'Register a new sending domain. Returns the DKIM, SPF, and DMARC DNS records that must be ' +\n 'added at the domain\\'s DNS provider before it can send. Present these records to the user.',\n inputSchema: {\n name: z.string().describe('The domain to add, e.g. \"mail.acme.com\".'),\n },\n },\n async ({ name }) => respond(await client.domains.create(name)),\n );\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Eusend } from '@eusend_dev/sdk';\nimport { z } from 'zod';\nimport { respond } from '../respond.js';\n\nexport function registerAudienceTools(server: McpServer, client: Eusend): void {\n server.registerTool(\n 'list_audiences',\n {\n title: 'List audiences',\n description: 'List contact audiences (lists) on the account, with per-audience contact counts.',\n inputSchema: {},\n },\n async () => respond(await client.audiences.list()),\n );\n\n server.registerTool(\n 'create_audience',\n {\n title: 'Create an audience',\n description: 'Create a new audience (contact list) to hold contacts for broadcasts.',\n inputSchema: {\n name: z.string().describe('Human-readable name for the audience.'),\n },\n },\n async ({ name }) => respond(await client.audiences.create(name)),\n );\n\n server.registerTool(\n 'list_contacts',\n {\n title: 'List contacts',\n description: 'List contacts in an audience. Supports search and subscription filtering.',\n inputSchema: {\n audienceId: z.string().describe('The audience to list contacts from.'),\n limit: z.number().int().min(1).max(100).optional(),\n cursor: z.string().optional().describe('Pagination cursor from a previous nextCursor.'),\n search: z.string().optional().describe('Filter by email or name substring.'),\n subscribed: z.boolean().optional().describe('true = only subscribed, false = only unsubscribed.'),\n },\n },\n async ({ audienceId, ...opts }) => respond(await client.audiences.listContacts(audienceId, opts)),\n );\n\n server.registerTool(\n 'create_contact',\n {\n title: 'Add a contact',\n description: 'Add a contact to an audience.',\n inputSchema: {\n audienceId: z.string().describe('The audience to add the contact to.'),\n email: z.string().describe('Contact email address.'),\n firstName: z.string().optional(),\n lastName: z.string().optional(),\n },\n },\n async ({ audienceId, email, firstName, lastName }) =>\n respond(await client.audiences.createContact(audienceId, { email, firstName, lastName })),\n );\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Eusend } from '@eusend_dev/sdk';\nimport { z } from 'zod';\nimport type { McpConfig } from '../config.js';\nimport { respond, failText } from '../respond.js';\n\nexport function registerBroadcastTools(server: McpServer, client: Eusend, config: McpConfig): void {\n server.registerTool(\n 'list_broadcasts',\n {\n title: 'List broadcasts',\n description: 'List broadcasts (bulk sends to an audience) with their status and send counts.',\n inputSchema: {},\n },\n async () => respond(await client.broadcasts.list()),\n );\n\n server.registerTool(\n 'get_broadcast',\n {\n title: 'Get a broadcast',\n description: 'Fetch a broadcast by id, including recipient/sent counts and per-event stats.',\n inputSchema: {\n id: z.string().describe('The broadcast id.'),\n },\n },\n async ({ id }) => respond(await client.broadcasts.get(id)),\n );\n\n server.registerTool(\n 'create_broadcast',\n {\n title: 'Create a broadcast (draft)',\n description:\n 'Create a broadcast in draft state, targeting an audience. Does NOT send it — use send_broadcast ' +\n 'to send or schedule after reviewing. If `from` is omitted the configured default sender is used.',\n inputSchema: {\n name: z.string().describe('Internal name for the broadcast.'),\n audienceId: z.string().describe('The audience to send to.'),\n subject: z.string().describe('Email subject line.'),\n from: z.string().optional().describe('Sender. Defaults to the configured sender.'),\n html: z.string().optional().describe('HTML body of the broadcast.'),\n },\n },\n async (args) => {\n const from = args.from ?? config.defaultFrom;\n if (!from) {\n return failText(\n 'No sender specified and no default sender configured. Pass `from`, or set EUSEND_FROM / --from on the server.',\n );\n }\n return respond(\n await client.broadcasts.create({\n name: args.name,\n audienceId: args.audienceId,\n subject: args.subject,\n from,\n html: args.html,\n }),\n );\n },\n );\n\n server.registerTool(\n 'send_broadcast',\n {\n title: 'Send or schedule a broadcast',\n description:\n 'Send a draft broadcast to its entire audience now, or schedule it for later. This is a bulk send — ' +\n 'confirm with the user before calling. Omit `scheduledAt` to send immediately.',\n inputSchema: {\n id: z.string().describe('The draft broadcast id.'),\n scheduledAt: z\n .string()\n .optional()\n .describe('Optional future send time (ISO 8601). Omit to send immediately.'),\n },\n },\n async ({ id, scheduledAt }) => respond(await client.broadcasts.send(id, { scheduledAt })),\n );\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { Eusend } from '@eusend_dev/sdk';\nimport type { McpConfig } from './config.js';\nimport { registerEmailTools } from './tools/emails.js';\nimport { registerDomainTools } from './tools/domains.js';\nimport { registerAudienceTools } from './tools/audiences.js';\nimport { registerBroadcastTools } from './tools/broadcasts.js';\n\nexport const SERVER_NAME = 'eusend';\nexport const SERVER_VERSION = '0.1.0';\n\n/**\n * Build a fully-wired MCP server for one Eusend account. This is\n * transport-agnostic: the stdio entry (index.ts) and the HTTP entry (http.ts)\n * both call this and then attach their own transport. All tool definitions live\n * in ./tools and are registered here exactly once.\n */\nexport function createServer(config: McpConfig): McpServer {\n const server = new McpServer({\n name: SERVER_NAME,\n version: SERVER_VERSION,\n });\n\n const client = new Eusend(config.apiKey, { baseUrl: config.baseUrl });\n\n registerEmailTools(server, client, config);\n registerDomainTools(server, client);\n registerAudienceTools(server, client);\n registerBroadcastTools(server, client, config);\n\n return server;\n}\n","import type { IncomingMessage } from 'node:http';\n\n/**\n * Minimal in-memory fixed-window IP rate limiter for the public /mcp endpoint.\n *\n * The endpoint is unauthenticated at the transport layer (initialize/tools/list\n * work before any API key is validated), so it needs its own basic DoS ceiling\n * independent of the API's rate limiting. In-memory on purpose: it keeps the box\n * free of any external dependency or credential. Per-container state is fine here\n * — the limit is a coarse abuse ceiling, not an accounting mechanism.\n */\nexport interface RateLimiter {\n /** Returns true if the request is allowed, false if it should be rejected (429). */\n check(req: IncomingMessage): boolean;\n}\n\n/** Best-effort client IP, honoring the proxy's X-Forwarded-For (Coolify/Traefik sits in front). */\nfunction clientIp(req: IncomingMessage): string {\n const xff = req.headers['x-forwarded-for'];\n if (typeof xff === 'string' && xff.length > 0) {\n return xff.split(',')[0].trim();\n }\n return req.socket.remoteAddress ?? 'unknown';\n}\n\nexport function createRateLimiter(limit: number, windowMs: number): RateLimiter {\n const buckets = new Map<string, { count: number; resetAt: number }>();\n\n // Evict stale buckets so the map can't grow unbounded under a churn of IPs.\n const sweep = setInterval(() => {\n const now = Date.now();\n for (const [key, b] of buckets) {\n if (now > b.resetAt) buckets.delete(key);\n }\n }, windowMs);\n sweep.unref();\n\n return {\n check(req) {\n const key = clientIp(req);\n const now = Date.now();\n const bucket = buckets.get(key);\n if (!bucket || now > bucket.resetAt) {\n buckets.set(key, { count: 1, resetAt: now + windowMs });\n return true;\n }\n bucket.count += 1;\n return bucket.count <= limit;\n },\n };\n}\n","import { createServer as createHttpServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport type { McpDefaults } from './config.js';\nimport { createServer } from './server.js';\nimport { createRateLimiter } from './ratelimit.js';\n\nexport interface RunHttpOptions {\n /** Account-agnostic defaults; the API key comes per-request from the caller. */\n defaults: McpDefaults;\n port: number;\n host: string;\n /** Hostnames allowed in the Host header (DNS-rebinding protection). */\n allowedHosts: string[];\n}\n\nconst MCP_PATH = '/mcp';\n\n// Coarse per-IP DoS ceiling for the public endpoint. Each JSON-RPC call over an\n// MCP session is one request, so this is generous for a real agent while still\n// capping an anonymous flood. Tune via RATE_LIMIT_PER_MIN if needed.\nconst RATE_LIMIT_PER_MIN = Number(process.env.RATE_LIMIT_PER_MIN ?? '120');\nconst rateLimiter = createRateLimiter(RATE_LIMIT_PER_MIN, 60_000);\n\nfunction json(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n}\n\n/** Pull the API key out of `Authorization: Bearer <key>`. */\nfunction bearerKey(req: IncomingMessage): string | undefined {\n const header = req.headers.authorization;\n if (!header) return undefined;\n const [scheme, value] = header.split(' ');\n return scheme?.toLowerCase() === 'bearer' && value ? value : undefined;\n}\n\nasync function readBody(req: IncomingMessage): Promise<unknown> {\n const chunks: Buffer[] = [];\n for await (const chunk of req) chunks.push(chunk as Buffer);\n if (chunks.length === 0) return undefined;\n return JSON.parse(Buffer.concat(chunks).toString('utf8'));\n}\n\n/**\n * Run the Eusend MCP server over Streamable HTTP — the hosted/remote transport.\n * Stateless: every request builds a fresh server bound to the caller's API key,\n * so one endpoint safely serves many accounts. Mount this behind TLS at, e.g.,\n * https://mcp.eusend.dev/mcp.\n */\nexport async function runHttp(options: RunHttpOptions): Promise<void> {\n const httpServer = createHttpServer((req, res) => {\n void handle(req, res, options);\n });\n\n await new Promise<void>((resolve) => {\n httpServer.listen(options.port, options.host, resolve);\n });\n console.error(`Eusend MCP server (HTTP) listening on http://${options.host}:${options.port}${MCP_PATH}`);\n}\n\nasync function handle(req: IncomingMessage, res: ServerResponse, options: RunHttpOptions): Promise<void> {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);\n\n if (url.pathname === '/health') {\n return json(res, 200, { status: 'ok' });\n }\n\n if (url.pathname !== MCP_PATH) {\n return json(res, 404, { error: 'Not found' });\n }\n\n // Stateless server: sessions aren't retained, so GET (server-push stream) and\n // DELETE (session teardown) have nothing to act on.\n if (req.method !== 'POST') {\n res.writeHead(405, { Allow: 'POST' });\n return void res.end();\n }\n\n if (!rateLimiter.check(req)) {\n return json(res, 429, {\n jsonrpc: '2.0',\n error: { code: -32029, message: 'Too many requests. Slow down.' },\n id: null,\n });\n }\n\n const apiKey = bearerKey(req);\n if (!apiKey) {\n return json(res, 401, {\n jsonrpc: '2.0',\n error: { code: -32001, message: 'Missing or malformed Authorization: Bearer <api-key> header.' },\n id: null,\n });\n }\n\n const server = createServer({ apiKey, ...options.defaults });\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // stateless\n enableDnsRebindingProtection: true,\n allowedHosts: options.allowedHosts,\n });\n\n res.on('close', () => {\n void transport.close();\n void server.close();\n });\n\n try {\n await server.connect(transport);\n const body = await readBody(req);\n await transport.handleRequest(req, res, body);\n } catch (err) {\n console.error('Error handling MCP request:', err);\n if (!res.headersSent) {\n json(res, 500, {\n jsonrpc: '2.0',\n error: { code: -32603, message: 'Internal server error' },\n id: null,\n });\n }\n }\n}\n"],"mappings":"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"} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
58761
2.69%454
2.48%2
100%