feat: add all tool domain files (events, subscribers, topics, workflows, notifications, messages, integrations, environments, translations, contexts, channels)

This commit is contained in:
2026-03-30 13:57:38 +02:00
parent 0fa7a41457
commit 1b9c5ac2b0
11 changed files with 3177 additions and 0 deletions
+417
View File
@@ -0,0 +1,417 @@
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";
// ---------------------------------------------------------------------------
// Channel Connections + Channel Endpoints tools
// ---------------------------------------------------------------------------
export function registerChannelsTools(server: McpServer): void {
// ==========================================================================
// Channel Connections
// ==========================================================================
// ---- List Channel Connections --------------------------------------------
server.registerTool(
"novu_list_channel_connections",
{
title: "List Channel Connections",
description:
"List all channel connections configured in the current environment. " +
"Supports offset-based pagination with limit and offset parameters.",
inputSchema: {
...PaginationV1Schema,
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
"/v1/channel-connections",
"GET",
undefined,
{
limit: params.limit,
offset: params.offset,
}
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Create Channel Connection -------------------------------------------
server.registerTool(
"novu_create_channel_connection",
{
title: "Create Channel Connection",
description:
"Create a new channel connection by specifying a provider and channel type. " +
"Optionally include credentials and a human-readable name.",
inputSchema: {
provider_id: z
.string()
.describe("The provider identifier for the connection (e.g. 'sendgrid', 'twilio')"),
channel: z
.string()
.describe("The channel type (e.g. 'email', 'sms', 'push', 'chat', 'in_app')"),
credentials: z
.record(z.unknown())
.optional()
.describe("Provider-specific credentials object"),
name: z
.string()
.optional()
.describe("Human-readable name for the connection"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
},
},
async (params) => {
try {
const result = await novuRequest(
"/v1/channel-connections",
"POST",
{
providerId: params.provider_id,
channel: params.channel,
credentials: params.credentials,
name: params.name,
}
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Get Channel Connection ----------------------------------------------
server.registerTool(
"novu_get_channel_connection",
{
title: "Get Channel Connection",
description:
"Retrieve a single channel connection by its ID, including provider, channel, credentials, and status.",
inputSchema: {
connection_id: z
.string()
.describe("The unique ID of the channel connection to retrieve"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/channel-connections/${encodeURIComponent(params.connection_id)}`,
"GET"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Update Channel Connection -------------------------------------------
server.registerTool(
"novu_update_channel_connection",
{
title: "Update Channel Connection",
description:
"Update an existing channel connection. Can modify credentials, name, or active status.",
inputSchema: {
connection_id: z
.string()
.describe("The unique ID of the channel connection to update"),
credentials: z
.record(z.unknown())
.optional()
.describe("Updated provider-specific credentials object"),
name: z
.string()
.optional()
.describe("Updated human-readable name"),
active: z
.boolean()
.optional()
.describe("Whether the connection is active"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const body: Record<string, unknown> = {};
if (params.credentials !== undefined) body.credentials = params.credentials;
if (params.name !== undefined) body.name = params.name;
if (params.active !== undefined) body.active = params.active;
const result = await novuRequest(
`/v1/channel-connections/${encodeURIComponent(params.connection_id)}`,
"PATCH",
body
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Delete Channel Connection -------------------------------------------
server.registerTool(
"novu_delete_channel_connection",
{
title: "Delete Channel Connection",
description:
"Permanently delete a channel connection by its ID. This cannot be undone.",
inputSchema: {
connection_id: z
.string()
.describe("The unique ID of the channel connection to delete"),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
},
},
async (params) => {
try {
await novuRequest<void>(
`/v1/channel-connections/${encodeURIComponent(params.connection_id)}`,
"DELETE"
);
return toolResult(
`Channel connection "${params.connection_id}" has been successfully deleted.`
);
} catch (error) {
return toolError(error);
}
}
);
// ==========================================================================
// Channel Endpoints
// ==========================================================================
// ---- List Channel Endpoints ----------------------------------------------
server.registerTool(
"novu_list_channel_endpoints",
{
title: "List Channel Endpoints",
description:
"List all channel endpoints configured in the current environment. " +
"Supports offset-based pagination with limit and offset parameters.",
inputSchema: {
...PaginationV1Schema,
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
"/v1/channel-endpoints",
"GET",
undefined,
{
limit: params.limit,
offset: params.offset,
}
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Create Channel Endpoint ---------------------------------------------
server.registerTool(
"novu_create_channel_endpoint",
{
title: "Create Channel Endpoint",
description:
"Create a new channel endpoint by specifying a channel type and URL. " +
"Optionally include a name and description.",
inputSchema: {
channel: z
.string()
.describe("The channel type (e.g. 'email', 'sms', 'push', 'chat', 'in_app')"),
url: z
.string()
.describe("The endpoint URL to receive channel callbacks"),
name: z
.string()
.optional()
.describe("Human-readable name for the endpoint"),
description: z
.string()
.optional()
.describe("Description of what this endpoint does"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
},
},
async (params) => {
try {
const result = await novuRequest(
"/v1/channel-endpoints",
"POST",
{
channel: params.channel,
url: params.url,
name: params.name,
description: params.description,
}
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Get Channel Endpoint ------------------------------------------------
server.registerTool(
"novu_get_channel_endpoint",
{
title: "Get Channel Endpoint",
description:
"Retrieve a single channel endpoint by its ID, including channel type, URL, and status.",
inputSchema: {
endpoint_id: z
.string()
.describe("The unique ID of the channel endpoint to retrieve"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/channel-endpoints/${encodeURIComponent(params.endpoint_id)}`,
"GET"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Update Channel Endpoint ---------------------------------------------
server.registerTool(
"novu_update_channel_endpoint",
{
title: "Update Channel Endpoint",
description:
"Update an existing channel endpoint. Can modify the URL, name, description, or active status.",
inputSchema: {
endpoint_id: z
.string()
.describe("The unique ID of the channel endpoint to update"),
url: z
.string()
.optional()
.describe("Updated endpoint URL"),
name: z
.string()
.optional()
.describe("Updated human-readable name"),
description: z
.string()
.optional()
.describe("Updated description"),
active: z
.boolean()
.optional()
.describe("Whether the endpoint is active"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const body: Record<string, unknown> = {};
if (params.url !== undefined) body.url = params.url;
if (params.name !== undefined) body.name = params.name;
if (params.description !== undefined) body.description = params.description;
if (params.active !== undefined) body.active = params.active;
const result = await novuRequest(
`/v1/channel-endpoints/${encodeURIComponent(params.endpoint_id)}`,
"PATCH",
body
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Delete Channel Endpoint ---------------------------------------------
server.registerTool(
"novu_delete_channel_endpoint",
{
title: "Delete Channel Endpoint",
description:
"Permanently delete a channel endpoint by its ID. This cannot be undone.",
inputSchema: {
endpoint_id: z
.string()
.describe("The unique ID of the channel endpoint to delete"),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
},
},
async (params) => {
try {
await novuRequest<void>(
`/v1/channel-endpoints/${encodeURIComponent(params.endpoint_id)}`,
"DELETE"
);
return toolResult(
`Channel endpoint "${params.endpoint_id}" has been successfully deleted.`
);
} catch (error) {
return toolError(error);
}
}
);
}
+204
View File
@@ -0,0 +1,204 @@
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";
import type { PaginatedListV1 } from "../types.js";
// ---------------------------------------------------------------------------
// Contexts tools
// ---------------------------------------------------------------------------
interface Context {
_id: string;
name: string;
description?: string;
data?: Record<string, unknown>;
createdAt?: string;
updatedAt?: string;
}
export function registerContextsTools(server: McpServer): void {
// ---- List Contexts ------------------------------------------------------
server.registerTool(
"novu_list_contexts",
{
title: "List Contexts",
description:
"List contexts in the current environment. " +
"Supports offset-based pagination with limit and offset parameters.",
inputSchema: {
...PaginationV1Schema,
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest<PaginatedListV1<Context>>(
"/v1/contexts",
"GET",
undefined,
{
limit: params.limit,
offset: params.offset,
}
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Create Context -----------------------------------------------------
server.registerTool(
"novu_create_context",
{
title: "Create Context",
description:
"Create a new context. A context holds arbitrary data that can be referenced by workflows and notifications.",
inputSchema: {
name: z.string().describe("Name of the context"),
description: z
.string()
.optional()
.describe("Human-readable description of the context"),
data: z
.record(z.unknown())
.optional()
.describe("Arbitrary key-value data to store in the context"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
},
},
async (params) => {
try {
const result = await novuRequest<Context>("/v1/contexts", "POST", {
name: params.name,
description: params.description,
data: params.data,
});
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Get Context --------------------------------------------------------
server.registerTool(
"novu_get_context",
{
title: "Get Context",
description:
"Retrieve a context by its ID, including its name, description, data, and timestamps.",
inputSchema: {
context_id: z
.string()
.describe("The unique ID of the context to retrieve"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest<Context>(
`/v1/contexts/${encodeURIComponent(params.context_id)}`,
"GET"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Update Context -----------------------------------------------------
server.registerTool(
"novu_update_context",
{
title: "Update Context",
description:
"Update an existing context. Only provided fields will be changed.",
inputSchema: {
context_id: z
.string()
.describe("The unique ID of the context to update"),
name: z.string().optional().describe("New name for the context"),
description: z
.string()
.optional()
.describe("New description for the context"),
data: z
.record(z.unknown())
.optional()
.describe("New key-value data for the context"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: 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.data !== undefined) body.data = params.data;
const result = await novuRequest<Context>(
`/v1/contexts/${encodeURIComponent(params.context_id)}`,
"PATCH",
body
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Delete Context -----------------------------------------------------
server.registerTool(
"novu_delete_context",
{
title: "Delete Context",
description:
"Permanently delete a context by its ID. This cannot be undone.",
inputSchema: {
context_id: z
.string()
.describe("The unique ID of the context to delete"),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
},
},
async (params) => {
try {
await novuRequest<void>(
`/v1/contexts/${encodeURIComponent(params.context_id)}`,
"DELETE"
);
return toolResult(
`Context "${params.context_id}" has been successfully deleted.`
);
} catch (error) {
return toolError(error);
}
}
);
}
+192
View File
@@ -0,0 +1,192 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { novuRequest, toolResult, toolError } from "../services/api-client.js";
// ---------------------------------------------------------------------------
// Environments tools
// ---------------------------------------------------------------------------
export function registerEnvironmentsTools(server: McpServer): void {
// ---- List Environments --------------------------------------------------
server.registerTool(
"novu_list_environments",
{
title: "List Environments",
description:
"List all environments for the current organization. " +
"Returns environment details including name, identifier, and configuration.",
inputSchema: {},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async () => {
try {
const result = await novuRequest<unknown>("/v1/environments", "GET");
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Create Environment -------------------------------------------------
server.registerTool(
"novu_create_environment",
{
title: "Create Environment",
description:
"Create a new environment within the current organization. " +
"Each environment has its own set of API keys, subscribers, and workflows.",
inputSchema: {
name: z.string().describe("Name for the new environment"),
parent_id: z
.string()
.optional()
.describe("Optional parent environment ID to inherit settings from"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
},
},
async (params) => {
try {
const result = await novuRequest<unknown>(
"/v1/environments",
"POST",
{
name: params.name,
parentId: params.parent_id,
}
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Update Environment -------------------------------------------------
server.registerTool(
"novu_update_environment",
{
title: "Update Environment",
description:
"Update an existing environment's name, identifier, or DNS configuration.",
inputSchema: {
environment_id: z
.string()
.describe("The ID of the environment to update"),
name: z
.string()
.optional()
.describe("New name for the environment"),
identifier: z
.string()
.optional()
.describe("New identifier for the environment"),
dns: z
.object({
mxRecordConfigured: z
.boolean()
.optional()
.describe("Whether MX record is configured"),
inboundParseDomain: z
.string()
.optional()
.describe("Inbound parse domain for email processing"),
})
.optional()
.describe("DNS configuration for the environment"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const body: Record<string, unknown> = {};
if (params.name !== undefined) body.name = params.name;
if (params.identifier !== undefined)
body.identifier = params.identifier;
if (params.dns !== undefined) body.dns = params.dns;
const result = await novuRequest<unknown>(
`/v1/environments/${encodeURIComponent(params.environment_id)}`,
"PUT",
body
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Delete Environment -------------------------------------------------
server.registerTool(
"novu_delete_environment",
{
title: "Delete Environment",
description:
"Permanently delete an environment. This cannot be undone and will remove all associated data including workflows, subscribers, and API keys.",
inputSchema: {
environment_id: z
.string()
.describe("The ID of the environment to delete"),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
},
},
async (params) => {
try {
await novuRequest<void>(
`/v1/environments/${encodeURIComponent(params.environment_id)}`,
"DELETE"
);
return toolResult(
`Environment "${params.environment_id}" has been successfully deleted.`
);
} catch (error) {
return toolError(error);
}
}
);
// ---- List Environment Tags ----------------------------------------------
server.registerTool(
"novu_list_environment_tags",
{
title: "List Environment Tags",
description:
"List all tags used across workflows in the current environment. " +
"Useful for discovering available tags for filtering workflows.",
inputSchema: {},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async () => {
try {
const result = await novuRequest<unknown>(
"/v1/environments/tags",
"GET"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
}
+271
View File
@@ -0,0 +1,271 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import {
novuRequest,
toolResult,
toolError,
} from "../services/api-client.js";
import type { TriggerResponse } from "../types.js";
// ── Shared sub-schemas ──────────────────────────────────────────────
const TriggerEventBodySchema = z
.object({
name: z.string().describe("Workflow trigger identifier (from the workflow settings)"),
to: z
.union([z.string(), z.array(z.string()).min(1)])
.describe("Subscriber ID or array of subscriber IDs to notify"),
payload: z
.record(z.unknown())
.optional()
.describe("Key-value data passed to the workflow template variables"),
overrides: z
.record(z.unknown())
.optional()
.describe("Provider-specific overrides (e.g. email subject, push title)"),
transactionId: z
.string()
.optional()
.describe("Unique ID for idempotent triggering and later cancellation"),
actor: z
.string()
.optional()
.describe("Subscriber ID of the user who performed the action"),
tenant: z
.string()
.optional()
.describe("Tenant identifier for multi-tenant setups"),
})
.strict();
const BulkEventItemSchema = z
.object({
name: z.string().describe("Workflow trigger identifier"),
to: z
.union([z.string(), z.array(z.string()).min(1)])
.describe("Subscriber ID or array of subscriber IDs"),
payload: z.record(z.unknown()).optional().describe("Template variable data"),
overrides: z.record(z.unknown()).optional().describe("Provider-specific overrides"),
transactionId: z
.string()
.optional()
.describe("Unique ID for idempotent triggering"),
})
.strict();
// ── Tool registration ───────────────────────────────────────────────
export function registerEventsTools(server: McpServer): void {
// ── 1. Trigger Event ────────────────────────────────────────────
server.registerTool(
"novu_trigger_event",
{
title: "Trigger Novu Event",
description: `Trigger a notification workflow for one or more subscribers.
Args:
name: Workflow trigger identifier (required)
to: Subscriber ID (string) or array of subscriber IDs (required)
payload: Key-value data for template variables (optional)
overrides: Provider-specific overrides for email, push, etc. (optional)
transactionId: Unique ID for deduplication / later cancellation (optional)
actor: Subscriber ID of the user who performed the action (optional)
tenant: Tenant identifier for multi-tenant setups (optional)
Returns:
JSON object with acknowledged, status, transactionId, and optional error array.
Examples:
- Simple trigger: { "name": "welcome-email", "to": "subscriber-123" }
- Multiple subscribers: { "name": "order-update", "to": ["sub-1", "sub-2"], "payload": { "orderId": "ORD-99" } }
- With overrides: { "name": "alert", "to": "sub-1", "overrides": { "email": { "from": "alerts@example.com" } } }`,
inputSchema: TriggerEventBodySchema,
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
},
async ({ name, to, payload, overrides, transactionId, actor, tenant }) => {
try {
const result = await novuRequest<TriggerResponse>(
"/v1/events/trigger",
"POST",
{ name, to, payload, overrides, transactionId, actor, tenant }
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ── 2. Bulk Trigger Events ──────────────────────────────────────
server.registerTool(
"novu_bulk_trigger_event",
{
title: "Bulk Trigger Novu Events",
description: `Trigger multiple notification workflows in a single API call (max 100).
Args:
events: Array of event objects, each containing:
- name: Workflow trigger identifier (required)
- to: Subscriber ID or array of subscriber IDs (required)
- payload: Template variable data (optional)
- overrides: Provider-specific overrides (optional)
- transactionId: Unique ID for deduplication (optional)
Returns:
JSON array of trigger responses, one per event.
Examples:
- Two events: { "events": [
{ "name": "welcome-email", "to": "sub-1" },
{ "name": "order-shipped", "to": "sub-2", "payload": { "trackingId": "TRK-42" } }
]}`,
inputSchema: z
.object({
events: z
.array(BulkEventItemSchema)
.min(1)
.max(100)
.describe("Array of trigger event objects (1-100)"),
})
.strict(),
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
},
async ({ events }) => {
try {
const result = await novuRequest<TriggerResponse[]>(
"/v1/events/trigger/bulk",
"POST",
{ events }
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ── 3. Broadcast Event ──────────────────────────────────────────
server.registerTool(
"novu_broadcast_event",
{
title: "Broadcast Novu Event",
description: `Broadcast a notification workflow to ALL subscribers in the environment.
Args:
name: Workflow trigger identifier (required)
payload: Key-value data for template variables (required)
overrides: Provider-specific overrides (optional)
transactionId: Unique ID for deduplication / later cancellation (optional)
actor: Subscriber ID of the user who performed the action (optional)
tenant: Tenant identifier for multi-tenant setups (optional)
Returns:
JSON object with acknowledged, status, transactionId, and optional error array.
Examples:
- Broadcast maintenance alert: { "name": "maintenance-notice", "payload": { "downtime": "2h", "startTime": "2025-04-01T02:00:00Z" } }
- With transaction ID: { "name": "product-launch", "payload": { "productName": "Widget Pro" }, "transactionId": "launch-2025-04" }`,
inputSchema: z
.object({
name: z.string().describe("Workflow trigger identifier"),
payload: z
.record(z.unknown())
.describe("Key-value data passed to the workflow template variables"),
overrides: z
.record(z.unknown())
.optional()
.describe("Provider-specific overrides"),
transactionId: z
.string()
.optional()
.describe("Unique ID for deduplication / later cancellation"),
actor: z
.string()
.optional()
.describe("Subscriber ID of the user who performed the action"),
tenant: z
.string()
.optional()
.describe("Tenant identifier for multi-tenant setups"),
})
.strict(),
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
},
async ({ name, payload, overrides, transactionId, actor, tenant }) => {
try {
const result = await novuRequest<TriggerResponse>(
"/v1/events/trigger/broadcast",
"POST",
{ name, payload, overrides, transactionId, actor, tenant }
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ── 4. Cancel Event ─────────────────────────────────────────────
server.registerTool(
"novu_cancel_event",
{
title: "Cancel Novu Event",
description: `Cancel a previously triggered event using its transaction ID.
Only pending (not yet delivered) notifications will be cancelled.
Args:
transaction_id: The transactionId that was returned or provided when the event was triggered (required)
Returns:
JSON confirmation of the cancellation.
Examples:
- Cancel by transaction ID: { "transaction_id": "order-123-notif" }`,
inputSchema: z
.object({
transaction_id: z
.string()
.describe(
"The transactionId returned when the event was triggered"
),
})
.strict(),
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: true,
},
},
async ({ transaction_id }) => {
try {
const result = await novuRequest<unknown>(
`/v1/events/trigger/${encodeURIComponent(transaction_id)}`,
"DELETE"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
}
+256
View File
@@ -0,0 +1,256 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { novuRequest, toolResult, toolError } from "../services/api-client.js";
// Reusable schema fragments for integration optional fields (shared by create & update)
const IntegrationOptionalFields = {
credentials: z
.record(z.unknown())
.optional()
.describe("Provider-specific credentials object (e.g., apiKey, secretKey, domain)"),
active: z.boolean().optional().describe("Whether the integration is active"),
name: z.string().optional().describe("Display name for the integration"),
identifier: z
.string()
.optional()
.describe("Unique identifier for the integration instance"),
environment_id: z
.string()
.optional()
.describe("Environment ID to associate the integration with"),
};
/** Map snake_case optional integration params to camelCase API body. */
function mapIntegrationOptionalFields(params: {
credentials?: Record<string, unknown>;
active?: boolean;
name?: string;
identifier?: string;
environment_id?: string;
}): Record<string, unknown> {
const body: Record<string, unknown> = {};
if (params.credentials !== undefined) body.credentials = params.credentials;
if (params.active !== undefined) body.active = params.active;
if (params.name !== undefined) body.name = params.name;
if (params.identifier !== undefined) body.identifier = params.identifier;
if (params.environment_id !== undefined)
body._environmentId = params.environment_id;
return body;
}
export function registerIntegrationsTools(server: McpServer): void {
// ── 1. List Integrations ──────────────────────────────────────────────
server.registerTool(
"novu_list_integrations",
{
title: "List Novu Integrations",
description:
"List all configured integrations for the current environment.",
inputSchema: {},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async () => {
try {
const result = await novuRequest("/v1/integrations", "GET");
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ── 2. List Active Integrations ───────────────────────────────────────
server.registerTool(
"novu_list_active_integrations",
{
title: "List Active Novu Integrations",
description:
"List only the active integrations for the current environment.",
inputSchema: {},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async () => {
try {
const result = await novuRequest("/v1/integrations/active", "GET");
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ── 3. Create Integration ─────────────────────────────────────────────
server.registerTool(
"novu_create_integration",
{
title: "Create Novu Integration",
description:
"Create a new integration for a notification provider (e.g., email, SMS, push, chat).",
inputSchema: {
provider_id: z
.string()
.describe("Provider identifier (e.g., 'sendgrid', 'twilio', 'fcm')"),
channel: z
.string()
.describe(
"Notification channel type (e.g., 'email', 'sms', 'push', 'chat', 'in_app')"
),
...IntegrationOptionalFields,
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
},
async (params) => {
try {
const result = await novuRequest("/v1/integrations", "POST", {
providerId: params.provider_id,
channel: params.channel,
...mapIntegrationOptionalFields(params),
});
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ── 4. Update Integration ─────────────────────────────────────────────
server.registerTool(
"novu_update_integration",
{
title: "Update Novu Integration",
description:
"Update an existing integration's configuration. Only provided fields are changed.",
inputSchema: {
integration_id: z.string().describe("Integration ID to update"),
...IntegrationOptionalFields,
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/integrations/${encodeURIComponent(params.integration_id)}`,
"PUT",
mapIntegrationOptionalFields(params)
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ── 5. Delete Integration ─────────────────────────────────────────────
server.registerTool(
"novu_delete_integration",
{
title: "Delete Novu Integration",
description:
"Permanently delete an integration. This action cannot be undone.",
inputSchema: {
integration_id: z.string().describe("Integration ID to delete"),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/integrations/${encodeURIComponent(params.integration_id)}`,
"DELETE"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ── 6. Set Primary Integration ────────────────────────────────────────
server.registerTool(
"novu_set_primary_integration",
{
title: "Set Primary Novu Integration",
description:
"Mark an integration as the primary provider for its channel.",
inputSchema: {
integration_id: z
.string()
.describe("Integration ID to set as primary"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/integrations/${encodeURIComponent(params.integration_id)}/set-primary`,
"POST"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ── 7. Generate Chat OAuth URL ────────────────────────────────────────
server.registerTool(
"novu_generate_chat_oauth_url",
{
title: "Generate Chat OAuth URL",
description:
"Generate an OAuth URL for a chat integration (e.g., Slack, Discord).",
inputSchema: {
integration_id: z
.string()
.describe("Chat integration ID to generate an OAuth URL for"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/integrations/${encodeURIComponent(params.integration_id)}/oauth/url`,
"POST"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
}
+124
View File
@@ -0,0 +1,124 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { novuRequest, toolResult, toolError } from "../services/api-client.js";
export function registerMessagesTools(server: McpServer): void {
// ── 1. List Messages ────────────────────────────────────────────────────
server.registerTool(
"novu_list_messages",
{
title: "List Novu Messages",
description:
"Retrieve a paginated list of messages, optionally filtered by channel, subscriber, or transaction.",
inputSchema: {
channel: z
.string()
.optional()
.describe("Filter by channel type (e.g., 'in_app', 'email', 'sms', 'push', 'chat')"),
subscriber_id: z
.string()
.optional()
.describe("Filter by subscriber identifier"),
transaction_id: z
.string()
.optional()
.describe("Filter by transaction identifier"),
limit: z
.number()
.int()
.min(1)
.max(100)
.default(20)
.describe("Maximum number of results to return (1-100)"),
offset: z
.number()
.int()
.min(0)
.default(0)
.describe("Number of results to skip for pagination"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async (params) => {
try {
const result = await novuRequest("/v1/messages", "GET", undefined, {
channel: params.channel,
subscriberId: params.subscriber_id,
transactionId: params.transaction_id,
limit: params.limit,
offset: params.offset,
});
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ── 2. Delete Message ───────────────────────────────────────────────────
server.registerTool(
"novu_delete_message",
{
title: "Delete Novu Message",
description:
"Permanently delete a message by its ID. This action cannot be undone.",
inputSchema: {
message_id: z.string().describe("The ID of the message to delete"),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/messages/${encodeURIComponent(params.message_id)}`,
"DELETE"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ── 3. Delete Messages by Transaction ───────────────────────────────────
server.registerTool(
"novu_delete_messages_by_transaction",
{
title: "Delete Novu Messages by Transaction",
description:
"Permanently delete all messages associated with a transaction ID. This action cannot be undone.",
inputSchema: {
transaction_id: z
.string()
.describe("The transaction ID whose messages should be deleted"),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/messages/transaction/${encodeURIComponent(params.transaction_id)}`,
"DELETE"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
}
+102
View File
@@ -0,0 +1,102 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { novuRequest, toolResult, toolError } from "../services/api-client.js";
export function registerNotificationsTools(server: McpServer): void {
// ── 1. List Notification Events ─────────────────────────────────────────
server.registerTool(
"novu_list_notification_events",
{
title: "List Novu Notification Events",
description:
"List notification events with optional filtering by channels, templates, emails, subscriber IDs, or transaction ID. Returns a paginated list.",
inputSchema: {
channels: z
.array(z.string())
.optional()
.describe("Filter by channel types (e.g., 'email', 'sms', 'in_app')"),
templates: z
.array(z.string())
.optional()
.describe("Filter by workflow/template IDs"),
emails: z
.array(z.string())
.optional()
.describe("Filter by recipient email addresses"),
subscriber_ids: z
.array(z.string())
.optional()
.describe("Filter by subscriber identifiers"),
transaction_id: z
.string()
.optional()
.describe("Filter by transaction identifier"),
limit: z
.number()
.int()
.min(1)
.max(100)
.default(20)
.describe("Maximum number of results to return (1-100)"),
offset: z
.number()
.int()
.min(0)
.default(0)
.describe("Number of results to skip for pagination"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async (params) => {
try {
const result = await novuRequest("/v1/notifications", "GET", undefined, {
channels: params.channels,
templates: params.templates,
emails: params.emails,
subscriberIds: params.subscriber_ids,
transactionId: params.transaction_id,
limit: params.limit,
offset: params.offset,
});
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ── 2. Get Notification Event ───────────────────────────────────────────
server.registerTool(
"novu_get_notification_event",
{
title: "Get Novu Notification Event",
description:
"Retrieve details of a specific notification event by its ID.",
inputSchema: {
notification_id: z.string().describe("Unique notification event identifier"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/notifications/${encodeURIComponent(params.notification_id)}`,
"GET"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
}
+652
View File
@@ -0,0 +1,652 @@
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<string, unknown>;
}): Record<string, unknown> {
const body: Record<string, unknown> = {};
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<string, unknown> = {};
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);
}
}
);
}
+391
View File
@@ -0,0 +1,391 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { novuRequest, toolResult, toolError } from "../services/api-client.js";
import { PaginationV1Schema, PaginationV2Schema } from "../schemas/common.js";
import type { Topic, PaginatedListV1, PaginatedListV2 } from "../types.js";
// ---------------------------------------------------------------------------
// Topics tools
// ---------------------------------------------------------------------------
export function registerTopicsTools(server: McpServer): void {
// ---- Create Topic -------------------------------------------------------
server.registerTool(
"novu_create_topic",
{
title: "Create Topic",
description:
"Create a new topic that can be used to group subscribers for bulk notifications. " +
"The topic key must be unique within the environment.",
inputSchema: {
key: z.string().describe("Unique key identifier for the topic"),
name: z
.string()
.optional()
.describe("Human-readable name for the topic"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
},
},
async (params) => {
try {
const result = await novuRequest<Topic>("/v1/topics", "POST", {
key: params.key,
name: params.name,
});
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Get Topic ----------------------------------------------------------
server.registerTool(
"novu_get_topic",
{
title: "Get Topic",
description:
"Retrieve a topic by its unique key, including metadata such as name and timestamps.",
inputSchema: {
topic_key: z.string().describe("The unique key of the topic to retrieve"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest<Topic>(
`/v1/topics/${encodeURIComponent(params.topic_key)}`,
"GET"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Update Topic -------------------------------------------------------
server.registerTool(
"novu_update_topic",
{
title: "Update Topic",
description: "Rename an existing topic by its key.",
inputSchema: {
topic_key: z.string().describe("The unique key of the topic to update"),
name: z.string().describe("New name for the topic"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest<Topic>(
`/v1/topics/${encodeURIComponent(params.topic_key)}`,
"PATCH",
{ name: params.name }
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Delete Topic -------------------------------------------------------
server.registerTool(
"novu_delete_topic",
{
title: "Delete Topic",
description:
"Permanently delete a topic by its key. This cannot be undone and will remove all subscriber associations.",
inputSchema: {
topic_key: z.string().describe("The unique key of the topic to delete"),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
},
},
async (params) => {
try {
await novuRequest<void>(
`/v1/topics/${encodeURIComponent(params.topic_key)}`,
"DELETE"
);
return toolResult(
`Topic "${params.topic_key}" has been successfully deleted.`
);
} catch (error) {
return toolError(error);
}
}
);
// ---- List Topics (v2 cursor-based) --------------------------------------
server.registerTool(
"novu_list_topics",
{
title: "List Topics",
description:
"List topics in the current environment with optional filtering by key or name. " +
"Uses cursor-based pagination; use the 'after' or 'before' cursors from a previous response to page through results.",
inputSchema: {
key: z
.string()
.optional()
.describe("Filter topics by key (exact or partial match)"),
name: z
.string()
.optional()
.describe("Filter topics by name (partial match)"),
...PaginationV2Schema,
orderBy: z
.string()
.optional()
.describe("Field to order results by (e.g. 'createdAt')"),
orderDirection: z
.enum(["ASC", "DESC"])
.optional()
.describe("Sort direction: ASC or DESC"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest<PaginatedListV2<Topic>>(
"/v2/topics",
"GET",
undefined,
{
key: params.key,
name: params.name,
limit: params.limit,
after: params.after,
before: params.before,
orderBy: params.orderBy,
orderDirection: params.orderDirection,
}
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Create Topic Subscriptions -----------------------------------------
server.registerTool(
"novu_create_topic_subscriptions",
{
title: "Add Subscribers to Topic",
description:
"Add one or more subscribers to a topic. Subscribers will then receive notifications sent to this topic.",
inputSchema: {
topic_key: z.string().describe("The unique key of the topic"),
subscriber_ids: z
.array(z.string())
.describe("Array of subscriber IDs to add to the topic"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/topics/${encodeURIComponent(params.topic_key)}/subscriptions`,
"POST",
{ subscriberIds: params.subscriber_ids }
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Delete Topic Subscriptions -----------------------------------------
server.registerTool(
"novu_delete_topic_subscriptions",
{
title: "Remove Subscribers from Topic",
description:
"Remove one or more subscribers from a topic. Those subscribers will no longer receive topic-targeted notifications.",
inputSchema: {
topic_key: z.string().describe("The unique key of the topic"),
subscriber_ids: z
.array(z.string())
.describe("Array of subscriber IDs to remove from the topic"),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/topics/${encodeURIComponent(params.topic_key)}/subscriptions`,
"DELETE",
{ subscriberIds: params.subscriber_ids }
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Update Topic Subscription ------------------------------------------
server.registerTool(
"novu_update_topic_subscription",
{
title: "Update Topic Subscription",
description:
"Update an existing subscription for a specific subscriber within a topic.",
inputSchema: {
topic_key: z.string().describe("The unique key of the topic"),
subscriber_id: z
.string()
.describe("The subscriber ID to update within the topic"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/topics/${encodeURIComponent(params.topic_key)}/subscriptions/${encodeURIComponent(params.subscriber_id)}`,
"PATCH"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- List Topic Subscriptions -------------------------------------------
server.registerTool(
"novu_list_topic_subscriptions",
{
title: "List Topic Subscriptions",
description:
"List all subscribers that belong to a given topic. " +
"Supports offset-based pagination with limit and offset parameters.",
inputSchema: {
topic_key: z.string().describe("The unique key of the topic"),
...PaginationV1Schema,
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest<PaginatedListV1<unknown>>(
`/v1/topics/${encodeURIComponent(params.topic_key)}/subscriptions`,
"GET",
undefined,
{
limit: params.limit,
offset: params.offset,
}
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Check Topic Subscriber ---------------------------------------------
server.registerTool(
"novu_check_topic_subscriber",
{
title: "Check Topic Subscriber",
description:
"Check whether a specific subscriber belongs to a topic. " +
"Returns the subscription details if the subscriber is a member.",
inputSchema: {
topic_key: z.string().describe("The unique key of the topic"),
subscriber_id: z
.string()
.describe("The subscriber ID to check membership for"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/topics/${encodeURIComponent(params.topic_key)}/subscriptions/${encodeURIComponent(params.subscriber_id)}`,
"GET"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Get Topic Subscription ---------------------------------------------
server.registerTool(
"novu_get_topic_subscription",
{
title: "Get Topic Subscription",
description:
"Retrieve the full subscription record for a specific subscriber within a topic, " +
"including subscription metadata and timestamps.",
inputSchema: {
topic_key: z.string().describe("The unique key of the topic"),
subscriber_id: z
.string()
.describe("The subscriber ID whose subscription to retrieve"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest(
`/v1/topics/${encodeURIComponent(params.topic_key)}/subscriptions/${encodeURIComponent(params.subscriber_id)}`,
"GET"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
}
+254
View File
@@ -0,0 +1,254 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { novuRequest, toolResult, toolError } from "../services/api-client.js";
// ---------------------------------------------------------------------------
// Translations tools
// ---------------------------------------------------------------------------
export function registerTranslationsTools(server: McpServer): void {
// ---- Create Translation -------------------------------------------------
server.registerTool(
"novu_create_translation",
{
title: "Create Translation",
description:
"Create a new translation for a specific locale within a translation group. " +
"Provide key-value pairs that map translation keys to their localized strings.",
inputSchema: {
group_id: z.string().describe("The ID of the translation group"),
locale: z
.string()
.describe("Locale code for the translation (e.g. 'en', 'fr', 'de')"),
translations: z
.record(z.string())
.describe(
"Key-value pairs mapping translation keys to localized strings"
),
name: z
.string()
.optional()
.describe("Human-readable name for this translation"),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
},
},
async (params) => {
try {
const result = await novuRequest<unknown>(
"/v1/translations",
"POST",
{
groupId: params.group_id,
locale: params.locale,
translations: params.translations,
name: params.name,
}
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Get Translation ----------------------------------------------------
server.registerTool(
"novu_get_translation",
{
title: "Get Translation",
description:
"Retrieve a single translation by its ID, including locale, keys, and group information.",
inputSchema: {
translation_id: z
.string()
.describe("The unique ID of the translation to retrieve"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest<unknown>(
`/v1/translations/${encodeURIComponent(params.translation_id)}`,
"GET"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Delete Translation -------------------------------------------------
server.registerTool(
"novu_delete_translation",
{
title: "Delete Translation",
description:
"Permanently delete a translation by its ID. This cannot be undone.",
inputSchema: {
translation_id: z
.string()
.describe("The unique ID of the translation to delete"),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
},
},
async (params) => {
try {
await novuRequest<void>(
`/v1/translations/${encodeURIComponent(params.translation_id)}`,
"DELETE"
);
return toolResult(
`Translation "${params.translation_id}" has been successfully deleted.`
);
} catch (error) {
return toolError(error);
}
}
);
// ---- Get Translation Group ----------------------------------------------
server.registerTool(
"novu_get_translation_group",
{
title: "Get Translation Group",
description:
"Retrieve a translation group by its ID, including its name, locales, and associated translations.",
inputSchema: {
group_id: z
.string()
.describe("The unique ID of the translation group to retrieve"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest<unknown>(
`/v1/translations/groups/${encodeURIComponent(params.group_id)}`,
"GET"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Delete Translation Group -------------------------------------------
server.registerTool(
"novu_delete_translation_group",
{
title: "Delete Translation Group",
description:
"Permanently delete a translation group and all its associated translations. This cannot be undone.",
inputSchema: {
group_id: z
.string()
.describe("The unique ID of the translation group to delete"),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
},
},
async (params) => {
try {
await novuRequest<void>(
`/v1/translations/groups/${encodeURIComponent(params.group_id)}`,
"DELETE"
);
return toolResult(
`Translation group "${params.group_id}" has been successfully deleted.`
);
} catch (error) {
return toolError(error);
}
}
);
// ---- Import Master Translations -----------------------------------------
server.registerTool(
"novu_import_master_translations",
{
title: "Import Master Translations",
description:
"Import or update master translation key-value pairs for a translation group. " +
"Master translations serve as the source of truth that other locale translations are derived from.",
inputSchema: {
group_id: z
.string()
.describe("The unique ID of the translation group"),
translations: z
.record(z.unknown())
.describe(
"JSON object of master translation key-value pairs to import"
),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest<unknown>(
`/v1/translations/groups/${encodeURIComponent(params.group_id)}/master`,
"POST",
{ translations: params.translations }
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
// ---- Get Master Translations --------------------------------------------
server.registerTool(
"novu_get_master_translations",
{
title: "Get Master Translations",
description:
"Retrieve the master translation key-value pairs for a translation group.",
inputSchema: {
group_id: z
.string()
.describe("The unique ID of the translation group"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (params) => {
try {
const result = await novuRequest<unknown>(
`/v1/translations/groups/${encodeURIComponent(params.group_id)}/master`,
"GET"
);
return toolResult(JSON.stringify(result, null, 2));
} catch (error) {
return toolError(error);
}
}
);
}
+314
View File
@@ -0,0 +1,314 @@
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);
}
}
);
}