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