653 lines
22 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
);
|
|
}
|