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

653 lines
22 KiB
TypeScript

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { novuRequest, toolResult, toolError } from "../services/api-client.js";
import { PaginationV1Schema } from "../schemas/common.js";
// 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);
}
}
);
}