Files
novu-mcp-server/src/tools/workflows.ts
T

315 lines
11 KiB
TypeScript

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<string, unknown> = { 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<string, unknown> = {};
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);
}
}
);
}