287 lines
8.9 KiB
TypeScript
287 lines
8.9 KiB
TypeScript
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);
|
|
});
|