feat: add all tool domain files (events, subscribers, topics, workflows, notifications, messages, integrations, environments, translations, contexts, channels)
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user