import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import axios from "axios"; import { z } from "zod"; import * as dotenv from "dotenv"; dotenv.config(); const LISTMONK_URL = process.env.LISTMONK_URL || "https://listmonk.infra.mintel.me"; const LISTMONK_USERNAME = process.env.LISTMONK_USERNAME || "mmintel"; const LISTMONK_PASSWORD = process.env.LISTMONK_PASSWORD || "Tim300493."; const apiClient = axios.create({ baseURL: LISTMONK_URL, auth: { username: LISTMONK_USERNAME, password: LISTMONK_PASSWORD, }, headers: { "Content-Type": "application/json", }, }); const server = new McpServer({ name: "listmonk-mcp", version: "1.0.0", }); // GET /api/subscribers server.tool( "listmonk_list_subscribers", { query: z.string().optional().describe("Search query (email or name)"), page: z.number().optional().describe("Page number (default 1)"), per_page: z.number().optional().describe("Results per page (default 20)"), }, async ({ query, page, per_page }) => { try { const response = await apiClient.get("/api/subscribers", { params: { query, page, per_page }, }); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.response?.data?.message || error.message}` }], isError: true, }; } } ); // GET /api/subscribers/{id} server.tool( "listmonk_get_subscriber", { id: z.number().describe("Subscriber ID") }, async ({ id }) => { try { const response = await apiClient.get(`/api/subscribers/${id}`); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.response?.data?.message || error.message}` }], isError: true, }; } } ); // POST /api/subscribers server.tool( "listmonk_create_subscriber", { email: z.string().email().describe("Subscriber email address"), name: z.string().describe("Subscriber name"), status: z.enum(["enabled", "disabled", "blocklisted"]).describe("Subscriber status"), lists: z.array(z.number()).describe("Array of list IDs to subscribe to"), attribs: z.record(z.string(), z.any()).optional().describe("Optional JSON attributes"), preconfirm_subscriptions: z.boolean().optional().describe("Mark subscriptions as confirmed (opt-in bypassed)"), }, async ({ email, name, status, lists, attribs, preconfirm_subscriptions }) => { try { const response = await apiClient.post("/api/subscribers", { email, name, status, lists, attribs, preconfirm_subscriptions, }); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.response?.data?.message || error.message}` }], isError: true, }; } } ); // PUT /api/subscribers/{id} server.tool( "listmonk_update_subscriber", { id: z.number().describe("Subscriber ID"), email: z.string().email().optional().describe("Subscriber email address"), name: z.string().optional().describe("Subscriber name"), status: z.enum(["enabled", "disabled", "blocklisted"]).optional().describe("Subscriber status"), lists: z.array(z.number()).optional().describe("Array of list IDs to subscribe to"), attribs: z.record(z.string(), z.any()).optional().describe("Optional JSON attributes"), }, async ({ id, email, name, status, lists, attribs }) => { try { const data: any = {}; if (email) data.email = email; if (name) data.name = name; if (status) data.status = status; if (lists) data.lists = lists; if (attribs) data.attribs = attribs; const response = await apiClient.put(`/api/subscribers/${id}`, data); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.response?.data?.message || error.message}` }], isError: true, }; } } ); // GET /api/lists server.tool( "listmonk_list_lists", {}, async () => { try { const response = await apiClient.get("/api/lists"); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.response?.data?.message || error.message}` }], isError: true, }; } } ); // POST /api/lists server.tool( "listmonk_create_list", { name: z.string().describe("List name"), type: z.enum(["public", "private"]).describe("List type"), optin: z.enum(["single", "double"]).describe("Opt-in type"), description: z.string().optional().describe("Optional description"), tags: z.array(z.string()).optional().describe("Optional tags"), }, async ({ name, type, optin, description, tags }) => { try { const response = await apiClient.post("/api/lists", { name, type, optin, description, tags, }); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.response?.data?.message || error.message}` }], isError: true, }; } } ); // GET /api/campaigns server.tool( "listmonk_list_campaigns", { page: z.number().optional().describe("Page number"), per_page: z.number().optional().describe("Results per page"), }, async ({ page, per_page }) => { try { const response = await apiClient.get("/api/campaigns", { params: { page, per_page }, }); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.response?.data?.message || error.message}` }], isError: true, }; } } ); // POST /api/campaigns server.tool( "listmonk_create_campaign", { name: z.string().describe("Internal campaign name"), subject: z.string().describe("Email subject line"), lists: z.array(z.number()).describe("Array of list IDs to send to"), type: z.enum(["regular", "optin"]).describe("Campaign type"), content_type: z.enum(["richtext", "html", "markdown", "plain"]).describe("Content type"), body: z.string().describe("Email body content"), altbody: z.string().optional().describe("Optional plain text alternative body"), from_email: z.string().optional().describe("From email address"), messenger: z.string().describe("Messenger to use (e.g. 'email')"), template_id: z.number().optional().describe("Template ID to wrap the content"), tags: z.array(z.string()).optional().describe("Optional tags"), }, async (params) => { try { const response = await apiClient.post("/api/campaigns", params); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.response?.data?.message || error.message}` }], isError: true, }; } } ); // POST /api/tx server.tool( "listmonk_send_tx", { subscriber_email: z.string().email().describe("Subscriber email to send to"), subscriber_id: z.number().optional().describe("Subscriber ID (optional, alternative to email)"), template_id: z.number().describe("Template ID to use"), data: z.record(z.string(), z.any()).describe("JSON data for template variables"), messenger: z.string().optional().describe("Messenger type (default 'email')"), }, async ({ subscriber_email, subscriber_id, template_id, data, messenger }) => { try { const payload: any = { template_id, data, messenger: messenger || "email", }; if (subscriber_email) payload.subscriber_email = subscriber_email; if (subscriber_id) payload.subscriber_id = subscriber_id; const response = await apiClient.post("/api/tx", payload); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.response?.data?.message || error.message}` }], isError: true, }; } } ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Listmonk MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });