import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { novuRequest, toolResult, toolError } from "../services/api-client.js"; import { PaginationV1Schema } from "../schemas/common.js"; // Reusable schema fragments for subscriber fields const SubscriberOptionalFields = { first_name: z.string().optional().describe("Subscriber's first name"), last_name: z.string().optional().describe("Subscriber's last name"), email: z.string().optional().describe("Subscriber's email address"), phone: z.string().optional().describe("Subscriber's phone number"), avatar: z.string().optional().describe("URL to subscriber's avatar image"), locale: z.string().optional().describe("Subscriber's locale (e.g., 'en-US')"), timezone: z.string().optional().describe("Subscriber's timezone (e.g., 'America/New_York')"), data: z .record(z.unknown()) .optional() .describe("Custom data object for the subscriber"), }; /** Map snake_case optional subscriber params to camelCase API body. */ function mapSubscriberFields(params: { first_name?: string; last_name?: string; email?: string; phone?: string; avatar?: string; locale?: string; timezone?: string; data?: Record; }): Record { const body: Record = {}; if (params.first_name !== undefined) body.firstName = params.first_name; if (params.last_name !== undefined) body.lastName = params.last_name; if (params.email !== undefined) body.email = params.email; if (params.phone !== undefined) body.phone = params.phone; if (params.avatar !== undefined) body.avatar = params.avatar; if (params.locale !== undefined) body.locale = params.locale; if (params.timezone !== undefined) body.timezone = params.timezone; if (params.data !== undefined) body.data = params.data; return body; } export function registerSubscribersTools(server: McpServer): void { // ── 1. Create Subscriber ────────────────────────────────────────────── server.registerTool( "novu_create_subscriber", { title: "Create Novu Subscriber", description: "Create a new subscriber in Novu with an identifier and optional profile fields.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), ...SubscriberOptionalFields, }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest("/v1/subscribers", "POST", { subscriberId: params.subscriber_id, ...mapSubscriberFields(params), }); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 2. Get Subscriber ───────────────────────────────────────────────── server.registerTool( "novu_get_subscriber", { title: "Get Novu Subscriber", description: "Retrieve a subscriber by their unique identifier.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}`, "GET" ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 3. Update Subscriber ────────────────────────────────────────────── server.registerTool( "novu_update_subscriber", { title: "Update Novu Subscriber", description: "Update an existing subscriber's profile fields. Only provided fields are changed.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), ...SubscriberOptionalFields, }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}`, "PATCH", mapSubscriberFields(params) ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 4. Delete Subscriber ────────────────────────────────────────────── server.registerTool( "novu_delete_subscriber", { title: "Delete Novu Subscriber", description: "Permanently delete a subscriber and all associated data. This action cannot be undone.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), }, annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}`, "DELETE" ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 5. Search Subscribers ───────────────────────────────────────────── server.registerTool( "novu_search_subscribers", { title: "Search Novu Subscribers", description: "List subscribers with optional pagination. Returns a paginated list.", inputSchema: { ...PaginationV1Schema, }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( "/v1/subscribers", "GET", undefined, { limit: params.limit, offset: params.offset } ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 6. Bulk Create Subscribers ──────────────────────────────────────── server.registerTool( "novu_bulk_create_subscribers", { title: "Bulk Create Novu Subscribers", description: "Create multiple subscribers in a single request. Each subscriber object must include a subscriberId.", inputSchema: { subscribers: z .array( z.object({ subscriberId: z.string().describe("Unique subscriber identifier"), firstName: z.string().optional().describe("First name"), lastName: z.string().optional().describe("Last name"), email: z.string().optional().describe("Email address"), phone: z.string().optional().describe("Phone number"), avatar: z.string().optional().describe("Avatar URL"), locale: z.string().optional().describe("Locale"), timezone: z.string().optional().describe("Timezone"), data: z .record(z.unknown()) .optional() .describe("Custom data"), }) ) .describe("Array of subscriber objects to create"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( "/v1/subscribers/bulk", "POST", { subscribers: params.subscribers } ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 7. Get Subscriber Preferences ───────────────────────────────────── server.registerTool( "novu_get_subscriber_preferences", { title: "Get Subscriber Preferences", description: "Retrieve notification preferences for a subscriber.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}/preferences`, "GET" ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 8. Update Subscriber Preferences ────────────────────────────────── server.registerTool( "novu_update_subscriber_preferences", { title: "Update Subscriber Preferences", description: "Update notification preferences for a subscriber.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), channel: z .object({ type: z .enum(["in_app", "email", "sms", "chat", "push"]) .describe("Channel type"), enabled: z.boolean().describe("Whether the channel is enabled"), }) .optional() .describe("Channel preference to update"), enabled: z .boolean() .optional() .describe("Global preference enabled state"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const body: Record = {}; if (params.channel !== undefined) body.channel = params.channel; if (params.enabled !== undefined) body.enabled = params.enabled; const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}/preferences`, "PATCH", body ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 9. Bulk Update Subscriber Preferences ───────────────────────────── server.registerTool( "novu_bulk_update_subscriber_preferences", { title: "Bulk Update Subscriber Preferences", description: "Update multiple notification preferences for a subscriber at once.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), preferences: z .array( z.object({ workflowId: z .string() .optional() .describe("Workflow identifier to update preferences for"), channel: z .object({ type: z .enum(["in_app", "email", "sms", "chat", "push"]) .describe("Channel type"), enabled: z .boolean() .describe("Whether the channel is enabled"), }) .optional() .describe("Channel preference"), enabled: z .boolean() .optional() .describe("Global preference enabled state"), }) ) .describe("Array of preference objects to update"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}/preferences/bulk`, "PATCH", { preferences: params.preferences } ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 10. Get Subscriber Subscriptions ────────────────────────────────── server.registerTool( "novu_get_subscriber_subscriptions", { title: "Get Subscriber Subscriptions", description: "Retrieve topic subscriptions for a subscriber.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}/subscriptions`, "GET" ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 11. Get Subscriber Notifications ────────────────────────────────── server.registerTool( "novu_get_subscriber_notifications", { title: "Get Subscriber Notifications", description: "Retrieve notifications for a subscriber with pagination.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), ...PaginationV1Schema, }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}/notifications`, "GET", undefined, { limit: params.limit, offset: params.offset } ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 12. Update Notification State ───────────────────────────────────── server.registerTool( "novu_update_notification_state", { title: "Update Notification State", description: "Update the state (read/seen/unseen/unread) of specific notifications for a subscriber.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), message_ids: z .array(z.string()) .describe("Array of notification message IDs to update"), state: z .enum(["read", "seen", "unseen", "unread"]) .describe("Target notification state"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}/notifications/state`, "POST", { messageIds: params.message_ids, state: params.state } ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 13. Update All Notification States ──────────────────────────────── server.registerTool( "novu_update_all_notification_states", { title: "Update All Notification States", description: "Update the state of all notifications for a subscriber at once.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), state: z .enum(["read", "seen", "unseen", "unread"]) .describe("Target notification state to apply to all notifications"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}/notifications/state/all`, "POST", { state: params.state } ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 14. Update Subscriber Online Status ─────────────────────────────── server.registerTool( "novu_update_subscriber_online_status", { title: "Update Subscriber Online Status", description: "Set a subscriber's online or offline status.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), is_online: z.boolean().describe("Whether the subscriber is online"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}/online-status`, "PATCH", { isOnline: params.is_online } ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 15. Update Provider Credentials ─────────────────────────────────── server.registerTool( "novu_update_provider_credentials", { title: "Update Provider Credentials", description: "Set (replace) integration provider credentials for a subscriber. Use PUT semantics — all credential fields must be provided.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), provider_id: z.string().describe("Integration provider identifier"), credentials: z .record(z.unknown()) .describe("Provider credentials object (e.g., deviceTokens, webhookUrl)"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}/credentials`, "PUT", { providerId: params.provider_id, credentials: params.credentials, } ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 16. Upsert Provider Credentials ─────────────────────────────────── server.registerTool( "novu_upsert_provider_credentials", { title: "Upsert Provider Credentials", description: "Create or partially update integration provider credentials for a subscriber. Uses PATCH semantics — only provided fields are changed.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), provider_id: z.string().describe("Integration provider identifier"), credentials: z .record(z.unknown()) .describe("Provider credentials object to upsert"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}/credentials`, "PATCH", { providerId: params.provider_id, credentials: params.credentials, } ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── 17. Delete Provider Credentials ─────────────────────────────────── server.registerTool( "novu_delete_provider_credentials", { title: "Delete Provider Credentials", description: "Remove integration provider credentials from a subscriber. This action cannot be undone.", inputSchema: { subscriber_id: z.string().describe("Unique subscriber identifier"), provider_id: z.string().describe("Integration provider identifier to remove"), }, annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/subscribers/${encodeURIComponent(params.subscriber_id)}/credentials/${encodeURIComponent(params.provider_id)}`, "DELETE" ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); }