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"; export function registerWorkflowsTools(server: McpServer): void { // ── List Workflows ────────────────────────────────────────────────── server.registerTool( "novu_list_workflows", { title: "List Novu Workflows", description: "List all notification workflows in the current Novu environment. " + "Returns workflow names, IDs, statuses, and step summaries. " + "Supports offset-based pagination.", inputSchema: { ...PaginationV1Schema, }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest("/v1/workflows", "GET", undefined, { limit: params.limit, offset: params.offset, }); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── Get Workflow ──────────────────────────────────────────────────── server.registerTool( "novu_get_workflow", { title: "Get Novu Workflow", description: "Retrieve a single workflow by its ID. " + "Returns the full workflow definition including steps, triggers, tags, and status.", inputSchema: { workflow_id: z.string().describe("The unique ID of the workflow to retrieve"), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/workflows/${params.workflow_id}`, "GET" ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── Create Workflow ───────────────────────────────────────────────── server.registerTool( "novu_create_workflow", { title: "Create Novu Workflow", description: "Create a new notification workflow. " + "At minimum, provide a name. Optionally configure description, tags, " + "active status, steps, notification group, and critical flag. " + "Steps and triggers are complex nested objects; pass them as arrays of objects.", inputSchema: { name: z.string().describe("Name of the workflow"), description: z .string() .optional() .describe("Description of the workflow"), tags: z .array(z.string()) .optional() .describe("Tags to categorize the workflow"), active: z .boolean() .optional() .describe("Whether the workflow is active (default: true)"), steps: z .array(z.record(z.unknown())) .optional() .describe( "Array of step definition objects. Each step typically includes " + "a template (with type, content, subject, etc.) and optional filters." ), notificationGroupId: z .string() .optional() .describe("ID of the notification group this workflow belongs to"), critical: z .boolean() .optional() .describe( "Whether this workflow is critical. Critical workflows are always sent, " + "even if the subscriber has disabled notifications." ), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, }, }, async (params) => { try { const body: Record = { name: params.name }; if (params.description !== undefined) body.description = params.description; if (params.tags !== undefined) body.tags = params.tags; if (params.active !== undefined) body.active = params.active; if (params.steps !== undefined) body.steps = params.steps; if (params.notificationGroupId !== undefined) body.notificationGroupId = params.notificationGroupId; if (params.critical !== undefined) body.critical = params.critical; const result = await novuRequest("/v1/workflows", "POST", body); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── Update Workflow ───────────────────────────────────────────────── server.registerTool( "novu_update_workflow", { title: "Update Novu Workflow", description: "Update an existing workflow by its ID. " + "Provide only the fields you want to change. " + "Steps and triggers are complex nested objects; pass them as arrays of objects.", inputSchema: { workflow_id: z.string().describe("The unique ID of the workflow to update"), name: z.string().optional().describe("Updated name of the workflow"), description: z .string() .optional() .describe("Updated description of the workflow"), tags: z .array(z.string()) .optional() .describe("Updated tags to categorize the workflow"), active: z .boolean() .optional() .describe("Whether the workflow is active"), steps: z .array(z.record(z.unknown())) .optional() .describe( "Updated array of step definition objects. Each step typically includes " + "a template (with type, content, subject, etc.) and optional filters." ), notificationGroupId: z .string() .optional() .describe("ID of the notification group this workflow belongs to"), critical: z .boolean() .optional() .describe( "Whether this workflow is critical. Critical workflows are always sent, " + "even if the subscriber has disabled notifications." ), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const body: Record = {}; if (params.name !== undefined) body.name = params.name; if (params.description !== undefined) body.description = params.description; if (params.tags !== undefined) body.tags = params.tags; if (params.active !== undefined) body.active = params.active; if (params.steps !== undefined) body.steps = params.steps; if (params.notificationGroupId !== undefined) body.notificationGroupId = params.notificationGroupId; if (params.critical !== undefined) body.critical = params.critical; const result = await novuRequest( `/v1/workflows/${params.workflow_id}`, "PUT", body ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── Delete Workflow ───────────────────────────────────────────────── server.registerTool( "novu_delete_workflow", { title: "Delete Novu Workflow", description: "Permanently delete a workflow by its ID. " + "This action cannot be undone. All associated triggers will stop working.", inputSchema: { workflow_id: z.string().describe("The unique ID of the workflow to delete"), }, annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/workflows/${params.workflow_id}`, "DELETE" ); return toolResult( JSON.stringify( result ?? { success: true, message: "Workflow deleted successfully" }, null, 2 ) ); } catch (error) { return toolError(error); } } ); // ── Sync Workflow ─────────────────────────────────────────────────── server.registerTool( "novu_sync_workflow", { title: "Sync Novu Workflow", description: "Sync a workflow to the target environment. " + "Use this to promote workflow changes between environments (e.g., dev to production).", inputSchema: { workflow_id: z.string().describe("The unique ID of the workflow to sync"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/workflows/${params.workflow_id}/sync`, "PUT" ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); // ── Get Workflow Step ─────────────────────────────────────────────── server.registerTool( "novu_get_workflow_step", { title: "Get Novu Workflow Step", description: "Retrieve a specific step from a workflow. " + "Returns the full step definition including its template, filters, and metadata.", inputSchema: { workflow_id: z.string().describe("The unique ID of the workflow"), step_id: z.string().describe("The unique ID of the step within the workflow"), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const result = await novuRequest( `/v1/workflows/${params.workflow_id}/steps/${params.step_id}`, "GET" ); return toolResult(JSON.stringify(result, null, 2)); } catch (error) { return toolError(error); } } ); }