From c00c646d25f8825af95dc9b1443942cbf2c7e0ec Mon Sep 17 00:00:00 2001 From: Adhish-Krishna Date: Wed, 15 Jul 2026 22:49:21 +0530 Subject: [PATCH 1/9] feat: Refactor alert management tools and add new functionalities - Removed SIMPLENS_DASHBOARD_URL from server configuration. - Updated index.ts to remove references to SIMPLENS_DASHBOARD_URL. - Introduced new tools for managing admin alert channels, including listing, creating, updating, and deleting channels. - Added dashboard tools for fetching stats and trends. - Replaced resolve_alert tool with delete_alert for dismissing alerts. - Implemented notifications management tools for listing and deleting notifications. - Added template management tools for creating, updating, and deleting notification templates. - Enhanced tests to verify the registration of new tools and their functionalities. --- packages/mcp-server/.env.example | 1 - packages/mcp-server/package.json | 3 +- packages/mcp-server/src/api-client.ts | 248 ++++++++++++++++-- packages/mcp-server/src/config.ts | 2 - packages/mcp-server/src/index.ts | 1 - .../mcp-server/src/tools/admin-channels.ts | 200 ++++++++++++++ packages/mcp-server/src/tools/dashboard.ts | 51 ++++ packages/mcp-server/src/tools/delete-alert.ts | 35 +++ packages/mcp-server/src/tools/index.ts | 14 +- packages/mcp-server/src/tools/list-alerts.ts | 6 +- .../src/tools/notifications-management.ts | 101 +++++++ .../mcp-server/src/tools/resolve-alert.ts | 35 --- .../src/tools/resolve-alerts-retry.ts | 57 ++++ packages/mcp-server/src/tools/templates.ts | 128 +++++++++ packages/mcp-server/tests/verify-stdio.js | 16 +- packages/mcp-server/tests/verify-stdio.ts | 12 +- tests/integration/mcp_server.test.ts | 184 +++++++++++++ 17 files changed, 1002 insertions(+), 92 deletions(-) create mode 100644 packages/mcp-server/src/tools/admin-channels.ts create mode 100644 packages/mcp-server/src/tools/dashboard.ts create mode 100644 packages/mcp-server/src/tools/delete-alert.ts create mode 100644 packages/mcp-server/src/tools/notifications-management.ts delete mode 100644 packages/mcp-server/src/tools/resolve-alert.ts create mode 100644 packages/mcp-server/src/tools/resolve-alerts-retry.ts create mode 100644 packages/mcp-server/src/tools/templates.ts create mode 100644 tests/integration/mcp_server.test.ts diff --git a/packages/mcp-server/.env.example b/packages/mcp-server/.env.example index 5f43548..efa9ff6 100644 --- a/packages/mcp-server/.env.example +++ b/packages/mcp-server/.env.example @@ -7,4 +7,3 @@ ALLOWED_ORIGINS=* # In HTTP mode, these are passed per-request via headers NS_API_KEY=your-api-key-here SIMPLENS_CORE_URL=http://localhost:3000 -SIMPLENS_DASHBOARD_URL=http://localhost:3002 diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 363dd07..d3d86ff 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -19,7 +19,8 @@ "prepublishOnly": "npm run build", "start": "node dist/index.js", "stdio": "node dist/index.js --stdio", - "dev": "tsx watch src/index.ts" + "dev": "tsx watch src/index.ts", + "test": "npm run build && tsx tests/verify-stdio.ts" }, "keywords": [ "mcp", diff --git a/packages/mcp-server/src/api-client.ts b/packages/mcp-server/src/api-client.ts index 5070900..9b15488 100644 --- a/packages/mcp-server/src/api-client.ts +++ b/packages/mcp-server/src/api-client.ts @@ -31,12 +31,20 @@ async function request( params?: Record; } = {} ): Promise> { - const url = new URL(path, baseUrl); + let cleanBaseUrl = baseUrl; + if (!cleanBaseUrl.endsWith('/')) { + cleanBaseUrl = `${cleanBaseUrl}/`; + } + let cleanPath = path; + if (cleanPath.startsWith('/')) { + cleanPath = cleanPath.slice(1); + } + const url = new URL(cleanPath, cleanBaseUrl); if (options.params) { for (const [key, value] of Object.entries(options.params)) { - if (value !== undefined && value !== '') { - url.searchParams.set(key, value); + if (value !== undefined && value !== null && value !== '') { + url.searchParams.set(key, String(value)); } } } @@ -48,7 +56,7 @@ async function request( 'Accept': 'application/json', ...options.headers, }, - body: options.body ? JSON.stringify(options.body) : undefined, + body: options.body !== undefined ? JSON.stringify(options.body) : undefined, }); const text = await response.text(); @@ -73,10 +81,10 @@ async function request( } // ============================================================================ -// CORE API CLIENT +// UNIFIED API CLIENT // ============================================================================ -export class CoreApiClient { +export class ApiClient { private baseUrl: string; private authHeader: Record; @@ -85,9 +93,10 @@ export class CoreApiClient { this.authHeader = { Authorization: `Bearer ${credentials.apiKey}` }; } + // Existing notification methods /** POST /api/notification - Send a single notification */ async sendNotification(payload: unknown): Promise { - return request(this.baseUrl, '/api/notification', { + return request(this.baseUrl, 'api/notification', { method: 'POST', headers: this.authHeader, body: payload, @@ -96,7 +105,7 @@ export class CoreApiClient { /** POST /api/notification/batch - Send batch notifications */ async sendBatchNotification(payload: unknown): Promise { - return request(this.baseUrl, '/api/notification/batch', { + return request(this.baseUrl, 'api/notification/batch', { method: 'POST', headers: this.authHeader, body: payload, @@ -105,25 +114,77 @@ export class CoreApiClient { /** GET /api/plugins - List installed plugins */ async getPlugins(): Promise { - return request(this.baseUrl, '/api/plugins', { + return request(this.baseUrl, 'api/plugins', { headers: this.authHeader, }); } -} -// ============================================================================ -// DASHBOARD API CLIENT -// ============================================================================ + // New Template CRUD methods + /** POST /api/templates/create - Create a new notification template */ + async createTemplate(payload: unknown): Promise { + return request(this.baseUrl, 'api/templates/create', { + method: 'POST', + headers: this.authHeader, + body: payload, + }); + } -export class DashboardApiClient { - private baseUrl: string; - private authHeader: Record; + /** GET /api/templates - List notification templates */ + async listTemplates(params: { package_name?: string } = {}): Promise { + return request(this.baseUrl, 'api/templates', { + headers: this.authHeader, + params: params as Record, + }); + } - constructor(credentials: UserCredentials) { - this.baseUrl = credentials.coreUrl.endsWith('/') - ? credentials.coreUrl - : `${credentials.coreUrl}/`; - this.authHeader = { Authorization: `Bearer ${credentials.apiKey}` }; + /** GET /api/templates/:template_id - Retrieve a single notification template by template_id */ + async getTemplateById(templateId: string): Promise { + return request(this.baseUrl, `api/templates/${templateId}`, { + headers: this.authHeader, + }); + } + + /** PUT /api/templates/:template_id - Update a template by template_id */ + async updateTemplate(templateId: string, payload: unknown): Promise { + return request(this.baseUrl, `api/templates/${templateId}`, { + method: 'PUT', + headers: this.authHeader, + body: payload, + }); + } + + /** DELETE /api/templates/:template_id - Delete a template by template_id */ + async deleteTemplate(templateId: string): Promise { + return request(this.baseUrl, `api/templates/${templateId}`, { + method: 'DELETE', + headers: this.authHeader, + }); + } + + // New Notifications Management methods + /** GET /api/notifications - List notifications with filters */ + async listNotifications(params: { + page?: number; + limit?: number; + status?: string; + channel?: string; + search?: string; + from?: string; + to?: string; + } = {}): Promise { + const queryParams: Record = {}; + if (params.page !== undefined) queryParams.page = String(params.page); + if (params.limit !== undefined) queryParams.limit = String(params.limit); + if (params.status !== undefined) queryParams.status = params.status; + if (params.channel !== undefined) queryParams.channel = params.channel; + if (params.search !== undefined) queryParams.search = params.search; + if (params.from !== undefined) queryParams.from = params.from; + if (params.to !== undefined) queryParams.to = params.to; + + return request(this.baseUrl, 'api/notifications', { + headers: this.authHeader, + params: queryParams, + }); } /** GET /api/notifications?status=failed - Find failed notifications */ @@ -149,7 +210,29 @@ export class DashboardApiClient { }); } - /** POST /api/notifications/[id]/retry - Retry a failed notification */ + /** GET /api/notifications/recent - Get feed of recent notifications */ + async getRecentNotifications(): Promise { + return request(this.baseUrl, 'api/notifications/recent', { + headers: this.authHeader, + }); + } + + /** GET /api/notifications/:id - Retrieve a single notification by MongoDB ID */ + async getNotificationById(id: string): Promise { + return request(this.baseUrl, `api/notifications/${id}`, { + headers: this.authHeader, + }); + } + + /** DELETE /api/notifications/:id - Delete a notification log */ + async deleteNotification(id: string): Promise { + return request(this.baseUrl, `api/notifications/${id}`, { + method: 'DELETE', + headers: this.authHeader, + }); + } + + /** POST /api/notifications/:id/retry - Retry a failed notification */ async retryFailure(notificationId: string): Promise { return request(this.baseUrl, `api/notifications/${notificationId}/retry`, { method: 'POST', @@ -157,7 +240,8 @@ export class DashboardApiClient { }); } - /** GET /api/alerts - List unresolved alerts */ + // Alerts & Remediation methods + /** GET /api/alerts - List system alerts */ async listAlerts(params: { page?: string; limit?: string; @@ -173,11 +257,125 @@ export class DashboardApiClient { }); } - /** DELETE /api/alerts/[id] - Resolve/dismiss an alert */ - async resolveAlert(alertId: string): Promise { + /** DELETE /api/alerts/:id - Dismiss/delete an alert */ + async deleteAlert(alertId: string): Promise { return request(this.baseUrl, `api/alerts/${alertId}`, { method: 'DELETE', headers: this.authHeader, }); } + + /** Alias for deleteAlert */ + async resolveAlert(alertId: string): Promise { + return this.deleteAlert(alertId); + } + + /** POST /api/alerts/:id/resolve - Resolve alert with retry */ + async resolveAlertWithRetry(alertId: string, appendWarning?: boolean): Promise { + return request(this.baseUrl, `api/alerts/${alertId}/resolve`, { + method: 'POST', + headers: this.authHeader, + body: { appendWarning: !!appendWarning }, + }); + } + + /** POST /api/alerts/bulk-resolve - Bulk resolve alerts with retry */ + async bulkResolveAlerts(params: { appendWarning?: boolean; limit?: number } = {}): Promise { + return request(this.baseUrl, 'api/alerts/bulk-resolve', { + method: 'POST', + headers: this.authHeader, + body: { + appendWarning: !!params.appendWarning, + limit: params.limit || 50, + }, + }); + } + + // Dashboard & Analytics methods + /** GET /api/dashboard/stats - Fetch dashboard stats */ + async getDashboardStats(): Promise { + return request(this.baseUrl, 'api/dashboard/stats', { + headers: this.authHeader, + }); + } + + /** GET /api/dashboard/trends - Get historical trends */ + async getDashboardTrends(range?: string): Promise { + return request(this.baseUrl, 'api/dashboard/trends', { + headers: this.authHeader, + params: range ? { range } : undefined, + }); + } + + // Admin Alert Channels CRUD and utility methods + /** GET /api/admin-channels/providers - List alert providers */ + async listAdminChannelProviders(): Promise { + return request(this.baseUrl, 'api/admin-channels/providers', { + headers: this.authHeader, + }); + } + + /** POST /api/admin-channels/test - Test connection */ + async testAdminChannel(payload: { channel_type: string; config: Record }): Promise { + return request(this.baseUrl, 'api/admin-channels/test', { + method: 'POST', + headers: this.authHeader, + body: payload, + }); + } + + /** POST /api/admin-channels/validate - Validate configuration */ + async validateAdminChannelConfig(payload: { channel_type: string; config: Record }): Promise { + return request(this.baseUrl, 'api/admin-channels/validate', { + method: 'POST', + headers: this.authHeader, + body: payload, + }); + } + + /** GET /api/admin-channels - List admin channels */ + async listAdminChannels(): Promise { + return request(this.baseUrl, 'api/admin-channels', { + headers: this.authHeader, + }); + } + + /** POST /api/admin-channels - Create admin channel */ + async createAdminChannel(payload: unknown): Promise { + return request(this.baseUrl, 'api/admin-channels', { + method: 'POST', + headers: this.authHeader, + body: payload, + }); + } + + /** GET /api/admin-channels/:id - Get admin channel detail */ + async getAdminChannel(id: string): Promise { + return request(this.baseUrl, `api/admin-channels/${id}`, { + headers: this.authHeader, + }); + } + + /** PATCH /api/admin-channels/:id - Update admin channel */ + async updateAdminChannel(id: string, payload: unknown): Promise { + return request(this.baseUrl, `api/admin-channels/${id}`, { + method: 'PATCH', + headers: this.authHeader, + body: payload, + }); + } + + /** DELETE /api/admin-channels/:id - Delete admin channel */ + async deleteAdminChannel(id: string): Promise { + return request(this.baseUrl, `api/admin-channels/${id}`, { + method: 'DELETE', + headers: this.authHeader, + }); + } } + +// Keep backward compatibility exports +export const CoreApiClient = ApiClient; +export type CoreApiClient = ApiClient; +export const DashboardApiClient = ApiClient; +export type DashboardApiClient = ApiClient; diff --git a/packages/mcp-server/src/config.ts b/packages/mcp-server/src/config.ts index 7ac59d5..7fc8d28 100644 --- a/packages/mcp-server/src/config.ts +++ b/packages/mcp-server/src/config.ts @@ -15,8 +15,6 @@ export const serverConfig = { ? process.env.ALLOWED_ORIGINS.split(',').map((origin) => origin.trim()).filter(Boolean) : ['*'], - // Stdio-mode credentials (only used with --stdio flag) SIMPLENS_API_KEY: process.env.NS_API_KEY || '', SIMPLENS_CORE_URL: process.env.SIMPLENS_CORE_URL || 'http://localhost:3000', - SIMPLENS_DASHBOARD_URL: process.env.SIMPLENS_DASHBOARD_URL || 'http://localhost:3002', }; diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 3169e1e..2d380c9 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -98,7 +98,6 @@ async function main() { 'Last-Event-ID', 'X-SimpleNS-API-Key', 'X-SimpleNS-Core-URL', - 'X-SimpleNS-Dashboard-URL', ], })); diff --git a/packages/mcp-server/src/tools/admin-channels.ts b/packages/mcp-server/src/tools/admin-channels.ts new file mode 100644 index 0000000..f090da1 --- /dev/null +++ b/packages/mcp-server/src/tools/admin-channels.ts @@ -0,0 +1,200 @@ +/** + * Tools: Admin Alert Channel Configurations + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { UserCredentials } from '../auth.js'; +import { ApiClient } from '../api-client.js'; +import { formatApiResponse, formatToolError } from './response.js'; + +const objectId = z.string().regex(/^[a-fA-F0-9]{24}$/); +const providerType = z.enum(['discord', 'telegram', 'email', 'slack']); + +export function registerAdminChannelsTools(server: McpServer, getCredentials: () => UserCredentials) { + // 14. list_admin_channel_providers + server.registerTool( + 'list_admin_channel_providers', + { + description: 'List available system alert providers (Discord, Telegram, Slack) with their required configuration forms and schemas. AI agents should run this before creating or updating a channel.', + inputSchema: {}, + }, + async () => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.listAdminChannelProviders(); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to list admin channel providers', error); + } + } + ); + + // 15. test_admin_channel + server.registerTool( + 'test_admin_channel', + { + description: 'Test sending a test message to an admin channel without saving its configuration to verify credentials.', + inputSchema: { + channel_type: providerType.describe('Provider type'), + config: z.record(z.string(), z.string()).describe('Raw connection credentials matching the provider\'s schema (e.g. webhook_url)') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.testAdminChannel(params); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to test admin channel connection', error); + } + } + ); + + // 16. validate_admin_channel_config + server.registerTool( + 'validate_admin_channel_config', + { + description: 'Validate credential keys and formats against the provider\'s schema without triggering any HTTP requests.', + inputSchema: { + channel_type: providerType.describe('Provider type'), + config: z.record(z.string(), z.string()).describe('Credential object to validate') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.validateAdminChannelConfig(params); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to validate admin channel config', error); + } + } + ); + + // 17. list_admin_channels + server.registerTool( + 'list_admin_channels', + { + description: 'Retrieve all registered admin alert channels. (Encrypted credentials are excluded for security).', + inputSchema: {}, + }, + async () => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.listAdminChannels(); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to list admin channels', error); + } + } + ); + + // 18. create_admin_channel + server.registerTool( + 'create_admin_channel', + { + description: 'Configure and register a new admin alert channel. Validates credentials against provider schema automatically.', + inputSchema: { + channel_type: providerType.describe('Provider type'), + name: z.string().min(1).describe('User-friendly nickname for this channel (e.g., "Developer Discord Alert")'), + config: z.record(z.string(), z.string()).describe('Credentials config matching provider requirements'), + alert_filters: z.object({ + failed_notifications: z.boolean().default(true), + service_health: z.boolean().default(true), + stuck_processing: z.boolean().default(true), + orphaned_pending: z.boolean().default(true), + ghost_delivery: z.boolean().default(false) + }).optional().describe('Which types of system alert trigger this channel') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.createAdminChannel(params); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to create admin channel', error); + } + } + ); + + // 19. get_admin_channel + server.registerTool( + 'get_admin_channel', + { + description: 'Get a single admin alert channel configuration by MongoDB ID (excluding encrypted credentials).', + inputSchema: { + id: objectId.describe('The MongoDB ID of the channel') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.getAdminChannel(params.id); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to retrieve admin channel', error); + } + } + ); + + // 20. update_admin_channel + server.registerTool( + 'update_admin_channel', + { + description: 'Update an admin alert channel configuration.', + inputSchema: { + id: objectId.describe('The MongoDB ID of the channel to update'), + name: z.string().optional().describe('Updated channel label'), + enabled: z.boolean().optional().describe('Toggle channel enablement'), + config: z.record(z.string(), z.string()).optional().describe('New credentials (validates automatically)'), + alert_filters: z.object({ + failed_notifications: z.boolean(), + service_health: z.boolean(), + stuck_processing: z.boolean(), + orphaned_pending: z.boolean(), + ghost_delivery: z.boolean() + }).partial().optional().describe('Updated alert filters') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const { id, ...payload } = params; + const result = await client.updateAdminChannel(id, payload); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to update admin channel', error); + } + } + ); + + // 21. delete_admin_channel + server.registerTool( + 'delete_admin_channel', + { + description: 'Delete an admin alert channel.', + inputSchema: { + id: objectId.describe('The MongoDB ID of the channel to delete') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.deleteAdminChannel(params.id); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to delete admin channel', error); + } + } + ); +} diff --git a/packages/mcp-server/src/tools/dashboard.ts b/packages/mcp-server/src/tools/dashboard.ts new file mode 100644 index 0000000..d5f93d4 --- /dev/null +++ b/packages/mcp-server/src/tools/dashboard.ts @@ -0,0 +1,51 @@ +/** + * Tools: get_dashboard_stats, get_dashboard_trends + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { UserCredentials } from '../auth.js'; +import { ApiClient } from '../api-client.js'; +import { formatApiResponse, formatToolError } from './response.js'; + +export function registerDashboardTools(server: McpServer, getCredentials: () => UserCredentials) { + // 12. get_dashboard_stats + server.registerTool( + 'get_dashboard_stats', + { + description: 'Fetch status counts, channel breakdowns, and general notification performance statistics.', + inputSchema: {}, + }, + async () => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.getDashboardStats(); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to retrieve dashboard stats', error); + } + } + ); + + // 13. get_dashboard_trends + server.registerTool( + 'get_dashboard_trends', + { + description: 'Get historical trends and sending rate patterns over a given period (24h, 7d, 30d).', + inputSchema: { + range: z.enum(['24h', '7d', '30d']).optional().describe('Trends historical duration range (default: 24h)') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.getDashboardTrends(params.range); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to retrieve dashboard trends', error); + } + } + ); +} diff --git a/packages/mcp-server/src/tools/delete-alert.ts b/packages/mcp-server/src/tools/delete-alert.ts new file mode 100644 index 0000000..3cd2ef7 --- /dev/null +++ b/packages/mcp-server/src/tools/delete-alert.ts @@ -0,0 +1,35 @@ +/** + * Tool: delete_alert + * Dismiss/delete a specific alert by ID without retry + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { UserCredentials } from '../auth.js'; +import { ApiClient } from '../api-client.js'; +import { formatApiResponse, formatToolError } from './response.js'; + +const objectId = z.string().regex(/^[a-fA-F0-9]{24}$/); + +export function registerDeleteAlert(server: McpServer, getCredentials: () => UserCredentials) { + server.registerTool( + 'delete_alert', + { + description: + 'Delete (dismiss) a specific system alert by its ID without queueing a retry of the notification. Use list_alerts first to see unresolved alerts.', + inputSchema: { + alert_id: objectId.describe('The MongoDB ObjectId of the alert to delete (24-character hex string)'), + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.deleteAlert(params.alert_id); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to delete alert', error); + } + } + ); +} diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts index d64d8e2..12e6367 100644 --- a/packages/mcp-server/src/tools/index.ts +++ b/packages/mcp-server/src/tools/index.ts @@ -13,8 +13,13 @@ import { registerListPlugins } from './list-plugins.js'; import { registerFindFailures } from './find-failures.js'; import { registerRetryFailure } from './retry-failure.js'; import { registerListAlerts } from './list-alerts.js'; -import { registerResolveAlert } from './resolve-alert.js'; +import { registerDeleteAlert } from './delete-alert.js'; import { registerGetSendSchema } from './get-send-schema.js'; +import { registerTemplateTools } from './templates.js'; +import { registerNotificationsManagementTools } from './notifications-management.js'; +import { registerResolveAlertsRetryTools } from './resolve-alerts-retry.js'; +import { registerDashboardTools } from './dashboard.js'; +import { registerAdminChannelsTools } from './admin-channels.js'; export function registerAllTools(server: McpServer, getCredentials: () => UserCredentials) { registerSendNotification(server, getCredentials); @@ -23,6 +28,11 @@ export function registerAllTools(server: McpServer, getCredentials: () => UserCr registerFindFailures(server, getCredentials); registerRetryFailure(server, getCredentials); registerListAlerts(server, getCredentials); - registerResolveAlert(server, getCredentials); + registerDeleteAlert(server, getCredentials); registerGetSendSchema(server); + registerTemplateTools(server, getCredentials); + registerNotificationsManagementTools(server, getCredentials); + registerResolveAlertsRetryTools(server, getCredentials); + registerDashboardTools(server, getCredentials); + registerAdminChannelsTools(server, getCredentials); } diff --git a/packages/mcp-server/src/tools/list-alerts.ts b/packages/mcp-server/src/tools/list-alerts.ts index a2a079e..eb7238f 100644 --- a/packages/mcp-server/src/tools/list-alerts.ts +++ b/packages/mcp-server/src/tools/list-alerts.ts @@ -6,7 +6,7 @@ import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { UserCredentials } from '../auth.js'; -import { DashboardApiClient } from '../api-client.js'; +import { ApiClient } from '../api-client.js'; import { formatApiResponse, formatToolError } from './response.js'; export function registerListAlerts(server: McpServer, getCredentials: () => UserCredentials) { @@ -14,7 +14,7 @@ export function registerListAlerts(server: McpServer, getCredentials: () => User 'list_alerts', { description: - 'List unresolved system alerts. Alerts indicate problems like ghost deliveries (status mismatch between Redis and DB), stuck processing (notifications stuck in processing state), and orphaned pending (notifications stuck in pending state). Use resolve_alert to dismiss specific alerts.', + 'List unresolved system alerts. Alerts indicate problems like ghost deliveries (status mismatch between Redis and DB), stuck processing (notifications stuck in processing state), and orphaned pending (notifications stuck in pending state). Use delete_alert to dismiss specific alerts.', inputSchema: { page: z.number().int().min(1).optional().describe('Page number (default: 1)'), limit: z.number().int().min(1).max(100).optional().describe('Results per page (default: 50)'), @@ -27,7 +27,7 @@ export function registerListAlerts(server: McpServer, getCredentials: () => User async (params) => { try { const credentials = getCredentials(); - const client = new DashboardApiClient(credentials); + const client = new ApiClient(credentials); const result = await client.listAlerts({ page: params.page?.toString(), limit: params.limit?.toString(), diff --git a/packages/mcp-server/src/tools/notifications-management.ts b/packages/mcp-server/src/tools/notifications-management.ts new file mode 100644 index 0000000..35b764a --- /dev/null +++ b/packages/mcp-server/src/tools/notifications-management.ts @@ -0,0 +1,101 @@ +/** + * Tool: Notifications Management + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { UserCredentials } from '../auth.js'; +import { ApiClient } from '../api-client.js'; +import { formatApiResponse, formatToolError } from './response.js'; + +const objectId = z.string().regex(/^[a-fA-F0-9]{24}$/); + +export function registerNotificationsManagementTools(server: McpServer, getCredentials: () => UserCredentials) { + // 6. list_notifications + server.registerTool( + 'list_notifications', + { + description: 'List notifications with advanced filtering, sorting, and pagination. (Use find_failures for failed-only alerts).', + inputSchema: { + page: z.number().int().min(1).optional().describe('Page number (default: 1)'), + limit: z.number().int().min(1).max(100).optional().describe('Results per page (default: 20)'), + status: z.string().optional().describe('Filter by status (e.g. "pending", "processing", "sent", "failed")'), + channel: z.string().optional().describe('Filter by channel (e.g. "email", "sms")'), + search: z.string().optional().describe('Search by request ID, client ID, or client name'), + from: z.string().datetime().optional().describe('Filter from ISO date'), + to: z.string().datetime().optional().describe('Filter to ISO date') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.listNotifications(params); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to list notifications', error); + } + } + ); + + // 7. get_recent_notifications + server.registerTool( + 'get_recent_notifications', + { + description: 'Get a feed of recent notifications (activity feed/logs).', + inputSchema: {}, + }, + async () => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.getRecentNotifications(); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to retrieve recent notifications', error); + } + } + ); + + // 8. get_notification_by_id + server.registerTool( + 'get_notification_by_id', + { + description: 'Retrieve full status, configuration, and error logs for a single notification by its internal MongoDB ID.', + inputSchema: { + id: objectId.describe('The 24-character hexadecimal MongoDB ID of the notification') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.getNotificationById(params.id); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to retrieve notification', error); + } + } + ); + + // 9. delete_notification + server.registerTool( + 'delete_notification', + { + description: 'Delete a notification log from the database.', + inputSchema: { + id: objectId.describe('The 24-character hexadecimal MongoDB ID of the notification to delete') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.deleteNotification(params.id); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to delete notification', error); + } + } + ); +} diff --git a/packages/mcp-server/src/tools/resolve-alert.ts b/packages/mcp-server/src/tools/resolve-alert.ts deleted file mode 100644 index b6dc077..0000000 --- a/packages/mcp-server/src/tools/resolve-alert.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Tool: resolve_alert - * Resolve/dismiss a specific alert by ID - */ - -import { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { UserCredentials } from '../auth.js'; -import { DashboardApiClient } from '../api-client.js'; -import { formatApiResponse, formatToolError } from './response.js'; - -const objectId = z.string().regex(/^[a-fA-F0-9]{24}$/); - -export function registerResolveAlert(server: McpServer, getCredentials: () => UserCredentials) { - server.registerTool( - 'resolve_alert', - { - description: - 'Resolve (dismiss) a specific system alert by its ID. This marks the alert as resolved. Use list_alerts first to see unresolved alerts and get their IDs.', - inputSchema: { - alert_id: objectId.describe('The MongoDB ObjectId of the alert to resolve (24-character hex string)'), - }, - }, - async (params) => { - try { - const credentials = getCredentials(); - const client = new DashboardApiClient(credentials); - const result = await client.resolveAlert(params.alert_id); - return formatApiResponse(result); - } catch (error) { - return formatToolError('Failed to resolve alert', error); - } - } - ); -} diff --git a/packages/mcp-server/src/tools/resolve-alerts-retry.ts b/packages/mcp-server/src/tools/resolve-alerts-retry.ts new file mode 100644 index 0000000..995ed7a --- /dev/null +++ b/packages/mcp-server/src/tools/resolve-alerts-retry.ts @@ -0,0 +1,57 @@ +/** + * Tools: resolve_alert_with_retry, bulk_resolve_alerts + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { UserCredentials } from '../auth.js'; +import { ApiClient } from '../api-client.js'; +import { formatApiResponse, formatToolError } from './response.js'; + +const objectId = z.string().regex(/^[a-fA-F0-9]{24}$/); + +export function registerResolveAlertsRetryTools(server: McpServer, getCredentials: () => UserCredentials) { + // 10. resolve_alert_with_retry + server.registerTool( + 'resolve_alert_with_retry', + { + description: 'Resolve a system alert AND retry the failed notification job. Optionally appends a warning to the template content.', + inputSchema: { + alert_id: objectId.describe('The MongoDB ObjectId of the alert to resolve'), + appendWarning: z.boolean().optional().describe('Whether to append "Ignore if already received" warning to the message (default: false)') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.resolveAlertWithRetry(params.alert_id, params.appendWarning); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to resolve alert with retry', error); + } + } + ); + + // 11. bulk_resolve_alerts + server.registerTool( + 'bulk_resolve_alerts', + { + description: 'Resolve all unresolved system alerts in bulk and queue their notifications for retry.', + inputSchema: { + appendWarning: z.boolean().optional().describe('Append warning to all retried notifications'), + limit: z.number().int().min(1).max(200).optional().describe('Limit the number of alerts resolved in this run (default: 50)') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.bulkResolveAlerts(params); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to bulk resolve alerts', error); + } + } + ); +} diff --git a/packages/mcp-server/src/tools/templates.ts b/packages/mcp-server/src/tools/templates.ts new file mode 100644 index 0000000..610890d --- /dev/null +++ b/packages/mcp-server/src/tools/templates.ts @@ -0,0 +1,128 @@ +/** + * Tool: Notification Template Management + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { UserCredentials } from '../auth.js'; +import { ApiClient } from '../api-client.js'; +import { formatApiResponse, formatToolError } from './response.js'; + +export function registerTemplateTools(server: McpServer, getCredentials: () => UserCredentials) { + // 1. create_template + server.registerTool( + 'create_template', + { + description: 'Create a new notification template. Guides AI to define the content template for a specific plugin package.', + inputSchema: { + name: z.string().describe('Human-readable name of the template'), + template_id: z.string().describe('Unique string identifier (slug) for the template'), + description: z.string().optional().describe('Optional explanation of the template\'s purpose'), + content: z.record(z.string(), z.unknown()).describe('Template content structure matching the package schema (e.g. HTML/Subject). Double curly braces {{variable}} can be used for placeholders.'), + package: z.string().describe('Package name of the provider this template targets (e.g., "@simplens/smtp")') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.createTemplate(params); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to create template', error); + } + } + ); + + // 2. list_templates + server.registerTool( + 'list_templates', + { + description: 'List all available notification templates, optionally filtered by package.', + inputSchema: { + package_name: z.string().optional().describe('Filter templates by package name (e.g. "@simplens/smtp")') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.listTemplates(params.package_name ? { package_name: params.package_name } : {}); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to list templates', error); + } + } + ); + + // 3. get_template_by_id + server.registerTool( + 'get_template_by_id', + { + description: 'Retrieve a single notification template by its unique template ID, including its content layout.', + inputSchema: { + template_id: z.string().describe('Unique template ID (slug)') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.getTemplateById(params.template_id); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to retrieve template', error); + } + } + ); + + // 4. update_template + server.registerTool( + 'update_template', + { + description: 'Update an existing template by its template ID.', + inputSchema: { + template_id: z.string().describe('The template ID of the template to update'), + name: z.string().describe('Updated human-readable name'), + description: z.string().optional().describe('Updated description'), + content: z.record(z.string(), z.unknown()).describe('Updated template content structure'), + package: z.string().describe('Package name of the provider') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const { template_id, ...payload } = params; + const result = await client.updateTemplate(template_id, { + template_id, + ...payload + }); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to update template', error); + } + } + ); + + // 5. delete_template + server.registerTool( + 'delete_template', + { + description: 'Permanently delete a notification template by its template ID.', + inputSchema: { + template_id: z.string().describe('The template ID of the template to delete') + }, + }, + async (params) => { + try { + const credentials = getCredentials(); + const client = new ApiClient(credentials); + const result = await client.deleteTemplate(params.template_id); + return formatApiResponse(result); + } catch (error) { + return formatToolError('Failed to delete template', error); + } + } + ); +} diff --git a/packages/mcp-server/tests/verify-stdio.js b/packages/mcp-server/tests/verify-stdio.js index 9c91f93..1be13be 100644 --- a/packages/mcp-server/tests/verify-stdio.js +++ b/packages/mcp-server/tests/verify-stdio.js @@ -61,7 +61,7 @@ function main() { transport = new stdio_js_1.StdioClientTransport({ command: 'node', args: ['dist/index.js', '--stdio'], - env: __assign(__assign({}, process.env), { NS_API_KEY: process.env.NS_API_KEY || "", SIMPLENS_CORE_URL: process.env.SIMPLENS_CORE_URL || "", SIMPLENS_DASHBOARD_URL: process.env.SIMPLENS_DASHBOARD_URL || "" }) + env: __assign(__assign({}, process.env), { NS_API_KEY: process.env.NS_API_KEY || "", SIMPLENS_CORE_URL: process.env.SIMPLENS_CORE_URL || "" }) }); client = new index_js_1.Client({ name: 'test-client', @@ -78,20 +78,12 @@ function main() { tools = _a.sent(); console.log("Found ".concat(tools.tools.length, " tools:")); tools.tools.forEach(function (t) { return console.log("- ".concat(t.name)); }); - if (tools.tools.length !== 7) { - throw new Error("Expected 7 tools, found ".concat(tools.tools.length)); - } - return [4 /*yield*/, client.listResources()]; - case 3: - resources = _a.sent(); - console.log("Found ".concat(resources.resources.length, " resources:")); - resources.resources.forEach(function (r) { return console.log("- ".concat(r.uri)); }); - if (resources.resources.length !== 2) { - throw new Error("Expected 2 resources, found ".concat(resources.resources.length)); + if (tools.tools.length !== 29) { + throw new Error("Expected 29 tools, found ".concat(tools.tools.length)); } console.log('Verification successful!'); return [4 /*yield*/, client.close()]; - case 4: + case 3: _a.sent(); return [2 /*return*/]; } diff --git a/packages/mcp-server/tests/verify-stdio.ts b/packages/mcp-server/tests/verify-stdio.ts index 1f57b68..b13ded7 100644 --- a/packages/mcp-server/tests/verify-stdio.ts +++ b/packages/mcp-server/tests/verify-stdio.ts @@ -15,7 +15,6 @@ async function main() { ...process.env, NS_API_KEY: process.env.NS_API_KEY || "", SIMPLENS_CORE_URL: process.env.SIMPLENS_CORE_URL || "", - SIMPLENS_DASHBOARD_URL: process.env.SIMPLENS_DASHBOARD_URL || "", } }); @@ -34,18 +33,11 @@ async function main() { console.log(`Found ${tools.tools.length} tools:`); tools.tools.forEach(t => console.log(`- ${t.name}`)); - if (tools.tools.length !== 8) { - throw new Error(`Expected 8 tools, found ${tools.tools.length}`); + if (tools.tools.length !== 29) { + throw new Error(`Expected 29 tools, found ${tools.tools.length}`); } - // List Resources - const resources = await client.listResources(); - console.log(`Found ${resources.resources.length} resources:`); - resources.resources.forEach(r => console.log(`- ${r.uri}`)); - if (resources.resources.length !== 0) { - throw new Error(`Expected 0 resources, found ${resources.resources.length}`); - } console.log('Verification successful!'); await client.close(); diff --git a/tests/integration/mcp_server.test.ts b/tests/integration/mcp_server.test.ts new file mode 100644 index 0000000..5ba3852 --- /dev/null +++ b/tests/integration/mcp_server.test.ts @@ -0,0 +1,184 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { registerAllTools } from '../../packages/mcp-server/src/tools/index.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +import { JSONRPCRequest, JSONRPCResponse, JSONRPCNotification } from '@modelcontextprotocol/sdk/types.js'; +import { McpServer as SdkMcpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; + +// Loopback transport for in-memory client-server MCP communication +class LoopbackTransport implements Transport { + private other?: LoopbackTransport; + onclose?: () => void; + onerror?: (error: Error) => void; + onmessage?: (message: JSONRPCRequest | JSONRPCResponse | JSONRPCNotification) => void; + + static link(t1: LoopbackTransport, t2: LoopbackTransport) { + t1.other = t2; + t2.other = t1; + } + + async send(message: JSONRPCRequest | JSONRPCResponse | JSONRPCNotification): Promise { + // Run asynchronously to allow call stack execution to proceed + setTimeout(() => { + if (this.other?.onmessage) { + this.other.onmessage(message); + } + }, 0); + } + + async start(): Promise {} + + async close(): Promise { + this.onclose?.(); + if (this.other) { + this.other.onclose?.(); + } + } +} + +describe('MCP Server Integration Tests', () => { + let server: SdkMcpServer; + let client: Client; + let clientTransport: LoopbackTransport; + let serverTransport: LoopbackTransport; + + const credentials = { + coreUrl: 'http://localhost:3000', + apiKey: 'test-api-key' + }; + + let fetchMock = vi.fn(); + + beforeEach(async () => { + // Stub global fetch + fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + + server = new SdkMcpServer({ + name: 'test-mcp-server', + version: '1.0.0' + }); + + registerAllTools(server, () => credentials); + + client = new Client({ + name: 'test-mcp-client', + version: '1.0.0' + }, { + capabilities: {} + }); + + clientTransport = new LoopbackTransport(); + serverTransport = new LoopbackTransport(); + LoopbackTransport.link(clientTransport, serverTransport); + + await Promise.all([ + server.connect(serverTransport), + client.connect(clientTransport) + ]); + }); + + afterEach(async () => { + await client.close(); + await server.close(); + vi.unstubAllGlobals(); + }); + + it('should register exactly 29 tools', async () => { + const toolsResult = await client.listTools(); + expect(toolsResult.tools.length).toBe(29); + + const toolNames = toolsResult.tools.map(t => t.name); + expect(toolNames).toContain('send_notification'); + expect(toolNames).toContain('create_template'); + expect(toolNames).toContain('list_notifications'); + expect(toolNames).toContain('delete_alert'); // Renamed from resolve_alert + expect(toolNames).toContain('resolve_alert_with_retry'); + expect(toolNames).toContain('get_dashboard_stats'); + expect(toolNames).toContain('list_admin_channel_providers'); + }); + + it('should route list_plugins tool call to GET /api/plugins', async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + status: 200, + headers: new Headers({ 'content-type': 'application/json' }), + text: async () => JSON.stringify([{ name: 'smtp', channel: 'email' }]) + }); + + const response = await client.callTool({ + name: 'list_plugins', + arguments: {} + }); + + expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/plugins', expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ + 'Authorization': 'Bearer test-api-key' + }) + })); + + expect(response.isError).toBeUndefined(); + expect(response.content[0].type).toBe('text'); + expect(JSON.parse(response.content[0].text as string)).toEqual([{ name: 'smtp', channel: 'email' }]); + }); + + it('should route create_template tool call to POST /api/templates/create', async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + status: 201, + headers: new Headers({ 'content-type': 'application/json' }), + text: async () => JSON.stringify({ success: true, id: 'temp-123' }) + }); + + const templateArgs = { + name: 'Welcome Email', + template_id: 'welcome-email', + description: 'Send to new signups', + content: { subject: 'Welcome!', body: 'Hello {{name}}' }, + package: '@simplens/smtp' + }; + + const response = await client.callTool({ + name: 'create_template', + arguments: templateArgs + }); + + expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/templates/create', expect.objectContaining({ + method: 'POST', + body: JSON.stringify(templateArgs), + headers: expect.objectContaining({ + 'Authorization': 'Bearer test-api-key' + }) + })); + + expect(response.isError).toBeUndefined(); + expect(JSON.parse(response.content[0].text as string)).toEqual({ success: true, id: 'temp-123' }); + }); + + it('should route delete_alert tool call to DELETE /api/alerts/:id', async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + status: 200, + headers: new Headers({ 'content-type': 'application/json' }), + text: async () => JSON.stringify({ success: true }) + }); + + const response = await client.callTool({ + name: 'delete_alert', + arguments: { + alert_id: '60d5ec48f83c2c2e88a53820' + } + }); + + expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/alerts/60d5ec48f83c2c2e88a53820', expect.objectContaining({ + method: 'DELETE', + headers: expect.objectContaining({ + 'Authorization': 'Bearer test-api-key' + }) + })); + + expect(response.isError).toBeUndefined(); + expect(JSON.parse(response.content[0].text as string)).toEqual({ success: true }); + }); +}); From d0a8d73752aabd5f0ed0d392c7dab197dd2c89c6 Mon Sep 17 00:00:00 2001 From: Adhish-Krishna Date: Wed, 15 Jul 2026 23:01:02 +0530 Subject: [PATCH 2/9] feat: refactor alert management tools to use shared schemas for validation and improve code consistency --- packages/mcp-server/src/api-client.ts | 10 +--------- packages/mcp-server/src/tools/admin-channels.ts | 8 ++++---- packages/mcp-server/src/tools/delete-alert.ts | 6 ++---- packages/mcp-server/src/tools/find-failures.ts | 8 ++++---- packages/mcp-server/src/tools/list-plugins.ts | 4 ++-- .../src/tools/notifications-management.ts | 9 ++++----- .../mcp-server/src/tools/resolve-alerts-retry.ts | 5 ++--- packages/mcp-server/src/tools/retry-failure.ts | 9 ++++----- packages/mcp-server/src/tools/schemas.ts | 14 ++++++++++++++ .../src/tools/send-batch-notification.ts | 4 ++-- packages/mcp-server/src/tools/send-notification.ts | 4 ++-- tests/integration/mcp_server.test.ts | 11 +++++++---- 12 files changed, 48 insertions(+), 44 deletions(-) create mode 100644 packages/mcp-server/src/tools/schemas.ts diff --git a/packages/mcp-server/src/api-client.ts b/packages/mcp-server/src/api-client.ts index 9b15488..c043121 100644 --- a/packages/mcp-server/src/api-client.ts +++ b/packages/mcp-server/src/api-client.ts @@ -265,10 +265,6 @@ export class ApiClient { }); } - /** Alias for deleteAlert */ - async resolveAlert(alertId: string): Promise { - return this.deleteAlert(alertId); - } /** POST /api/alerts/:id/resolve - Resolve alert with retry */ async resolveAlertWithRetry(alertId: string, appendWarning?: boolean): Promise { @@ -374,8 +370,4 @@ export class ApiClient { } } -// Keep backward compatibility exports -export const CoreApiClient = ApiClient; -export type CoreApiClient = ApiClient; -export const DashboardApiClient = ApiClient; -export type DashboardApiClient = ApiClient; + diff --git a/packages/mcp-server/src/tools/admin-channels.ts b/packages/mcp-server/src/tools/admin-channels.ts index f090da1..fe98e45 100644 --- a/packages/mcp-server/src/tools/admin-channels.ts +++ b/packages/mcp-server/src/tools/admin-channels.ts @@ -8,7 +8,7 @@ import type { UserCredentials } from '../auth.js'; import { ApiClient } from '../api-client.js'; import { formatApiResponse, formatToolError } from './response.js'; -const objectId = z.string().regex(/^[a-fA-F0-9]{24}$/); +import { objectIdSchema } from './schemas.js'; const providerType = z.enum(['discord', 'telegram', 'email', 'slack']); export function registerAdminChannelsTools(server: McpServer, getCredentials: () => UserCredentials) { @@ -130,7 +130,7 @@ export function registerAdminChannelsTools(server: McpServer, getCredentials: () { description: 'Get a single admin alert channel configuration by MongoDB ID (excluding encrypted credentials).', inputSchema: { - id: objectId.describe('The MongoDB ID of the channel') + id: objectIdSchema.describe('The MongoDB ID of the channel') }, }, async (params) => { @@ -151,7 +151,7 @@ export function registerAdminChannelsTools(server: McpServer, getCredentials: () { description: 'Update an admin alert channel configuration.', inputSchema: { - id: objectId.describe('The MongoDB ID of the channel to update'), + id: objectIdSchema.describe('The MongoDB ID of the channel to update'), name: z.string().optional().describe('Updated channel label'), enabled: z.boolean().optional().describe('Toggle channel enablement'), config: z.record(z.string(), z.string()).optional().describe('New credentials (validates automatically)'), @@ -183,7 +183,7 @@ export function registerAdminChannelsTools(server: McpServer, getCredentials: () { description: 'Delete an admin alert channel.', inputSchema: { - id: objectId.describe('The MongoDB ID of the channel to delete') + id: objectIdSchema.describe('The MongoDB ID of the channel to delete') }, }, async (params) => { diff --git a/packages/mcp-server/src/tools/delete-alert.ts b/packages/mcp-server/src/tools/delete-alert.ts index 3cd2ef7..58c0142 100644 --- a/packages/mcp-server/src/tools/delete-alert.ts +++ b/packages/mcp-server/src/tools/delete-alert.ts @@ -3,13 +3,11 @@ * Dismiss/delete a specific alert by ID without retry */ -import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { UserCredentials } from '../auth.js'; import { ApiClient } from '../api-client.js'; import { formatApiResponse, formatToolError } from './response.js'; - -const objectId = z.string().regex(/^[a-fA-F0-9]{24}$/); +import { objectIdSchema } from './schemas.js'; export function registerDeleteAlert(server: McpServer, getCredentials: () => UserCredentials) { server.registerTool( @@ -18,7 +16,7 @@ export function registerDeleteAlert(server: McpServer, getCredentials: () => Use description: 'Delete (dismiss) a specific system alert by its ID without queueing a retry of the notification. Use list_alerts first to see unresolved alerts.', inputSchema: { - alert_id: objectId.describe('The MongoDB ObjectId of the alert to delete (24-character hex string)'), + alert_id: objectIdSchema.describe('The MongoDB ObjectId of the alert to delete (24-character hex string)'), }, }, async (params) => { diff --git a/packages/mcp-server/src/tools/find-failures.ts b/packages/mcp-server/src/tools/find-failures.ts index ccfa56b..84b45b2 100644 --- a/packages/mcp-server/src/tools/find-failures.ts +++ b/packages/mcp-server/src/tools/find-failures.ts @@ -6,8 +6,9 @@ import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { UserCredentials } from '../auth.js'; -import { DashboardApiClient } from '../api-client.js'; +import { ApiClient } from '../api-client.js'; import { formatApiResponse, formatToolError } from './response.js'; +import { paginationSchemas } from './schemas.js'; export function registerFindFailures(server: McpServer, getCredentials: () => UserCredentials) { server.registerTool( @@ -16,8 +17,7 @@ export function registerFindFailures(server: McpServer, getCredentials: () => Us description: 'Find failed notifications with optional filtering by channel, date range, or search term. Returns paginated results with notification details and error messages.', inputSchema: { - page: z.number().int().min(1).optional().describe('Page number (default: 1)'), - limit: z.number().int().min(1).max(100).optional().describe('Results per page (default: 20, max: 100)'), + ...paginationSchemas, channel: z.string().optional().describe('Filter by channel (e.g. "email", "sms")'), search: z.string().optional().describe('Search by request_id, client_id, or client_name'), from: z.string().datetime().optional().describe('Filter from date (ISO datetime)'), @@ -27,7 +27,7 @@ export function registerFindFailures(server: McpServer, getCredentials: () => Us async (params) => { try { const credentials = getCredentials(); - const client = new DashboardApiClient(credentials); + const client = new ApiClient(credentials); const result = await client.findFailures({ page: params.page?.toString(), limit: params.limit?.toString(), diff --git a/packages/mcp-server/src/tools/list-plugins.ts b/packages/mcp-server/src/tools/list-plugins.ts index 97b304b..b4f755e 100644 --- a/packages/mcp-server/src/tools/list-plugins.ts +++ b/packages/mcp-server/src/tools/list-plugins.ts @@ -6,7 +6,7 @@ import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { UserCredentials } from '../auth.js'; -import { CoreApiClient } from '../api-client.js'; +import { ApiClient } from '../api-client.js'; import { formatApiResponse, formatToolError } from './response.js'; export function registerListPlugins(server: McpServer, getCredentials: () => UserCredentials) { @@ -19,7 +19,7 @@ export function registerListPlugins(server: McpServer, getCredentials: () => Use async () => { try { const credentials = getCredentials(); - const client = new CoreApiClient(credentials); + const client = new ApiClient(credentials); const result = await client.getPlugins(); return formatApiResponse(result); } catch (error) { diff --git a/packages/mcp-server/src/tools/notifications-management.ts b/packages/mcp-server/src/tools/notifications-management.ts index 35b764a..3ba9e49 100644 --- a/packages/mcp-server/src/tools/notifications-management.ts +++ b/packages/mcp-server/src/tools/notifications-management.ts @@ -8,7 +8,7 @@ import type { UserCredentials } from '../auth.js'; import { ApiClient } from '../api-client.js'; import { formatApiResponse, formatToolError } from './response.js'; -const objectId = z.string().regex(/^[a-fA-F0-9]{24}$/); +import { objectIdSchema, paginationSchemas } from './schemas.js'; export function registerNotificationsManagementTools(server: McpServer, getCredentials: () => UserCredentials) { // 6. list_notifications @@ -17,8 +17,7 @@ export function registerNotificationsManagementTools(server: McpServer, getCrede { description: 'List notifications with advanced filtering, sorting, and pagination. (Use find_failures for failed-only alerts).', inputSchema: { - page: z.number().int().min(1).optional().describe('Page number (default: 1)'), - limit: z.number().int().min(1).max(100).optional().describe('Results per page (default: 20)'), + ...paginationSchemas, status: z.string().optional().describe('Filter by status (e.g. "pending", "processing", "sent", "failed")'), channel: z.string().optional().describe('Filter by channel (e.g. "email", "sms")'), search: z.string().optional().describe('Search by request ID, client ID, or client name'), @@ -63,7 +62,7 @@ export function registerNotificationsManagementTools(server: McpServer, getCrede { description: 'Retrieve full status, configuration, and error logs for a single notification by its internal MongoDB ID.', inputSchema: { - id: objectId.describe('The 24-character hexadecimal MongoDB ID of the notification') + id: objectIdSchema.describe('The 24-character hexadecimal MongoDB ID of the notification') }, }, async (params) => { @@ -84,7 +83,7 @@ export function registerNotificationsManagementTools(server: McpServer, getCrede { description: 'Delete a notification log from the database.', inputSchema: { - id: objectId.describe('The 24-character hexadecimal MongoDB ID of the notification to delete') + id: objectIdSchema.describe('The 24-character hexadecimal MongoDB ID of the notification to delete') }, }, async (params) => { diff --git a/packages/mcp-server/src/tools/resolve-alerts-retry.ts b/packages/mcp-server/src/tools/resolve-alerts-retry.ts index 995ed7a..94578ad 100644 --- a/packages/mcp-server/src/tools/resolve-alerts-retry.ts +++ b/packages/mcp-server/src/tools/resolve-alerts-retry.ts @@ -7,8 +7,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { UserCredentials } from '../auth.js'; import { ApiClient } from '../api-client.js'; import { formatApiResponse, formatToolError } from './response.js'; - -const objectId = z.string().regex(/^[a-fA-F0-9]{24}$/); +import { objectIdSchema } from './schemas.js'; export function registerResolveAlertsRetryTools(server: McpServer, getCredentials: () => UserCredentials) { // 10. resolve_alert_with_retry @@ -17,7 +16,7 @@ export function registerResolveAlertsRetryTools(server: McpServer, getCredential { description: 'Resolve a system alert AND retry the failed notification job. Optionally appends a warning to the template content.', inputSchema: { - alert_id: objectId.describe('The MongoDB ObjectId of the alert to resolve'), + alert_id: objectIdSchema.describe('The MongoDB ObjectId of the alert to resolve'), appendWarning: z.boolean().optional().describe('Whether to append "Ignore if already received" warning to the message (default: false)') }, }, diff --git a/packages/mcp-server/src/tools/retry-failure.ts b/packages/mcp-server/src/tools/retry-failure.ts index ccb4f28..2a9a257 100644 --- a/packages/mcp-server/src/tools/retry-failure.ts +++ b/packages/mcp-server/src/tools/retry-failure.ts @@ -6,10 +6,9 @@ import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { UserCredentials } from '../auth.js'; -import { DashboardApiClient } from '../api-client.js'; +import { ApiClient } from '../api-client.js'; import { formatApiResponse, formatToolError } from './response.js'; - -const objectId = z.string().regex(/^[a-fA-F0-9]{24}$/); +import { objectIdSchema } from './schemas.js'; export function registerRetryFailure(server: McpServer, getCredentials: () => UserCredentials) { server.registerTool( @@ -18,7 +17,7 @@ export function registerRetryFailure(server: McpServer, getCredentials: () => Us description: 'Retry a specific failed notification by its ID. Resets the notification to pending status and re-queues it for processing. Only works on notifications with "failed" status. Use find_failures first to get notification IDs.', inputSchema: { - notification_id: objectId.describe( + notification_id: objectIdSchema.describe( 'The MongoDB ObjectId of the failed notification to retry (24-character hex string)' ), }, @@ -26,7 +25,7 @@ export function registerRetryFailure(server: McpServer, getCredentials: () => Us async (params) => { try { const credentials = getCredentials(); - const client = new DashboardApiClient(credentials); + const client = new ApiClient(credentials); const result = await client.retryFailure(params.notification_id); return formatApiResponse(result); } catch (error) { diff --git a/packages/mcp-server/src/tools/schemas.ts b/packages/mcp-server/src/tools/schemas.ts new file mode 100644 index 0000000..50589a8 --- /dev/null +++ b/packages/mcp-server/src/tools/schemas.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +/** + * Shared Zod schemas for MCP tools + */ + +// MongoDB ObjectId validation schema (24-character hexadecimal string) +export const objectIdSchema = z.string().regex(/^[a-fA-F0-9]{24}$/); + +// Common pagination and filter schemas for list tools +export const paginationSchemas = { + page: z.number().int().min(1).optional().describe('Page number (default: 1)'), + limit: z.number().int().min(1).max(100).optional().describe('Results per page (default: 20)'), +}; diff --git a/packages/mcp-server/src/tools/send-batch-notification.ts b/packages/mcp-server/src/tools/send-batch-notification.ts index e471403..4a34605 100644 --- a/packages/mcp-server/src/tools/send-batch-notification.ts +++ b/packages/mcp-server/src/tools/send-batch-notification.ts @@ -6,7 +6,7 @@ import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { UserCredentials } from '../auth.js'; -import { CoreApiClient } from '../api-client.js'; +import { ApiClient } from '../api-client.js'; import { formatApiResponse, formatToolError } from './response.js'; export function registerSendBatchNotification(server: McpServer, getCredentials: () => UserCredentials) { @@ -77,7 +77,7 @@ export function registerSendBatchNotification(server: McpServer, getCredentials: async (params) => { try { const credentials = getCredentials(); - const client = new CoreApiClient(credentials); + const client = new ApiClient(credentials); const result = await client.sendBatchNotification(params); return formatApiResponse(result); } catch (error) { diff --git a/packages/mcp-server/src/tools/send-notification.ts b/packages/mcp-server/src/tools/send-notification.ts index 1a88b93..f6ad3ca 100644 --- a/packages/mcp-server/src/tools/send-notification.ts +++ b/packages/mcp-server/src/tools/send-notification.ts @@ -6,7 +6,7 @@ import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { UserCredentials } from '../auth.js'; -import { CoreApiClient } from '../api-client.js'; +import { ApiClient } from '../api-client.js'; import { formatApiResponse, formatToolError } from './response.js'; export function registerSendNotification(server: McpServer, getCredentials: () => UserCredentials) { @@ -90,7 +90,7 @@ export function registerSendNotification(server: McpServer, getCredentials: () = async (params) => { try { const credentials = getCredentials(); - const client = new CoreApiClient(credentials); + const client = new ApiClient(credentials); const result = await client.sendNotification(params); return formatApiResponse(result); } catch (error) { diff --git a/tests/integration/mcp_server.test.ts b/tests/integration/mcp_server.test.ts index 5ba3852..dd45c86 100644 --- a/tests/integration/mcp_server.test.ts +++ b/tests/integration/mcp_server.test.ts @@ -119,8 +119,9 @@ describe('MCP Server Integration Tests', () => { })); expect(response.isError).toBeUndefined(); - expect(response.content[0].type).toBe('text'); - expect(JSON.parse(response.content[0].text as string)).toEqual([{ name: 'smtp', channel: 'email' }]); + const content = response.content[0] as { type: string; text: string }; + expect(content.type).toBe('text'); + expect(JSON.parse(content.text)).toEqual([{ name: 'smtp', channel: 'email' }]); }); it('should route create_template tool call to POST /api/templates/create', async () => { @@ -153,7 +154,8 @@ describe('MCP Server Integration Tests', () => { })); expect(response.isError).toBeUndefined(); - expect(JSON.parse(response.content[0].text as string)).toEqual({ success: true, id: 'temp-123' }); + const content = response.content[0] as { text: string }; + expect(JSON.parse(content.text)).toEqual({ success: true, id: 'temp-123' }); }); it('should route delete_alert tool call to DELETE /api/alerts/:id', async () => { @@ -179,6 +181,7 @@ describe('MCP Server Integration Tests', () => { })); expect(response.isError).toBeUndefined(); - expect(JSON.parse(response.content[0].text as string)).toEqual({ success: true }); + const content = response.content[0] as { text: string }; + expect(JSON.parse(content.text)).toEqual({ success: true }); }); }); From ebc479e6c80435c394066436300965a07698741a Mon Sep 17 00:00:00 2001 From: Adhish-Krishna Date: Thu, 16 Jul 2026 08:03:13 +0530 Subject: [PATCH 3/9] feat(api-client): replace fetch with axios for improved HTTP requests and add parameter handling --- AGENTS.md | 2 +- packages/mcp-server/package-lock.json | 178 +++++++++++++++++++++++++- packages/mcp-server/package.json | 5 +- packages/mcp-server/src/api-client.ts | 37 ++---- 4 files changed, 185 insertions(+), 37 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 913eb7f..d688c65 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ Install dependencies with `npm install` and `npm install --prefix dashboard`. ## Coding Style & Naming Conventions Use TypeScript with strict typing and existing `@src/*` path aliases. Match current style: 2-space indentation, camelCase for variables and functions, PascalCase for types, classes, and React components, and filenames such as `notification.controller.test.ts`. -Plan changes before writing code. Follow low-level design principles: single responsibility, clear interfaces, useful dependency inversion, and explicit error handling. Do not duplicate logic; extract shared behavior into focused utilities, services, or test helpers. +Plan changes before writing code. Follow low-level design principles: single responsibility, clear interfaces, useful dependency inversion, and explicit error handling. Do not duplicate logic; extract shared behavior into focused utilities, services, or test helpers. Always use `axios` instead of the plain `fetch` API in this project. ## Testing Guidelines Vitest is the test runner. Integration tests also use `supertest`, `mongodb-memory-server`, and Redis mocks. Name test files with `.test.ts`, for example `tests/unit/plugins/loader.test.ts`. diff --git a/packages/mcp-server/package-lock.json b/packages/mcp-server/package-lock.json index b4ddaa8..b485fd3 100644 --- a/packages/mcp-server/package-lock.json +++ b/packages/mcp-server/package-lock.json @@ -1,15 +1,16 @@ { - "name": "@simplens/mcp-server", - "version": "1.0.3", + "name": "@simplens/mcp", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@simplens/mcp-server", - "version": "1.0.3", + "name": "@simplens/mcp", + "version": "1.1.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", + "axios": "^1.18.1", "cors": "^2.8.5", "dotenv": "^17.2.4", "express": "^5.2.1", @@ -27,6 +28,9 @@ "@types/node": "^24.10.1", "tsx": "^4.21.0", "typescript": "^5.9.3" + }, + "engines": { + "node": ">=22.0.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -654,6 +658,18 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -687,6 +703,24 @@ } } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -749,6 +783,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -837,6 +883,15 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -917,6 +972,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -1099,6 +1169,63 @@ "url": "https://opencollective.com/express" } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1215,10 +1342,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1265,6 +1407,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -1500,6 +1655,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index d3d86ff..4e38140 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -31,12 +31,13 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", + "axios": "^1.18.1", "cors": "^2.8.5", + "dotenv": "^17.2.4", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "helmet": "^8.1.0", - "zod": "^4.1.13", - "dotenv": "^17.2.4" + "zod": "^4.1.13" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/packages/mcp-server/src/api-client.ts b/packages/mcp-server/src/api-client.ts index c043121..828a5ad 100644 --- a/packages/mcp-server/src/api-client.ts +++ b/packages/mcp-server/src/api-client.ts @@ -5,6 +5,7 @@ * Instantiated with user credentials, discarded after the response. */ +import axios from 'axios'; import type { UserCredentials } from './auth.js'; export interface ApiResponse { @@ -13,14 +14,6 @@ export interface ApiResponse { data: T; } -function isLikelyJson(contentType: string | null, bodyText: string): boolean { - if (contentType && contentType.toLowerCase().includes('application/json')) { - return true; - } - const trimmed = bodyText.trim(); - return trimmed.startsWith('{') || trimmed.startsWith('['); -} - async function request( baseUrl: string, path: string, @@ -41,42 +34,32 @@ async function request( } const url = new URL(cleanPath, cleanBaseUrl); + const params: Record = {}; if (options.params) { for (const [key, value] of Object.entries(options.params)) { if (value !== undefined && value !== null && value !== '') { - url.searchParams.set(key, String(value)); + params[key] = String(value); } } } - const response = await fetch(url.toString(), { + const response = await axios({ + url: url.toString(), method: options.method || 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', ...options.headers, }, - body: options.body !== undefined ? JSON.stringify(options.body) : undefined, + data: options.body, + params, + validateStatus: () => true, }); - const text = await response.text(); - let data: T | string | null; - if (!text) { - data = null; - } else if (isLikelyJson(response.headers.get('content-type'), text)) { - try { - data = JSON.parse(text) as T; - } catch { - data = text; - } - } else { - data = text; - } - return { - ok: response.ok, + ok: response.status >= 200 && response.status < 300, status: response.status, - data: data as T, + data: (response.data === undefined ? null : response.data) as T, }; } From 544122a1337f765933297506c9b369a95ffecfee Mon Sep 17 00:00:00 2001 From: Adhish-Krishna Date: Thu, 16 Jul 2026 08:09:09 +0530 Subject: [PATCH 4/9] fix(api-client): update parameter name for getDashboardTrends method from 'range' to 'period' --- packages/mcp-server/src/api-client.ts | 2 +- tests/integration/mcp_server.test.ts | 56 ++++++++++++++++----------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/packages/mcp-server/src/api-client.ts b/packages/mcp-server/src/api-client.ts index 828a5ad..9b76265 100644 --- a/packages/mcp-server/src/api-client.ts +++ b/packages/mcp-server/src/api-client.ts @@ -282,7 +282,7 @@ export class ApiClient { async getDashboardTrends(range?: string): Promise { return request(this.baseUrl, 'api/dashboard/trends', { headers: this.authHeader, - params: range ? { range } : undefined, + params: range ? { period: range } : undefined, }); } diff --git a/tests/integration/mcp_server.test.ts b/tests/integration/mcp_server.test.ts index dd45c86..4b36db0 100644 --- a/tests/integration/mcp_server.test.ts +++ b/tests/integration/mcp_server.test.ts @@ -5,6 +5,24 @@ import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import { JSONRPCRequest, JSONRPCResponse, JSONRPCNotification } from '@modelcontextprotocol/sdk/types.js'; import { McpServer as SdkMcpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +const { mockAxios } = vi.hoisted(() => { + return { + mockAxios: vi.fn(), + }; +}); + +// Mock axios for both root and sub-package resolutions +vi.mock('axios', () => { + return { + default: mockAxios, + }; +}); +vi.mock('../../packages/mcp-server/node_modules/axios', () => { + return { + default: mockAxios, + }; +}); + // Loopback transport for in-memory client-server MCP communication class LoopbackTransport implements Transport { private other?: LoopbackTransport; @@ -47,19 +65,15 @@ describe('MCP Server Integration Tests', () => { apiKey: 'test-api-key' }; - let fetchMock = vi.fn(); - beforeEach(async () => { - // Stub global fetch - fetchMock = vi.fn(); - vi.stubGlobal('fetch', fetchMock); + mockAxios.mockReset(); server = new SdkMcpServer({ name: 'test-mcp-server', version: '1.0.0' }); - registerAllTools(server, () => credentials); + registerAllTools(server as any, () => credentials); client = new Client({ name: 'test-mcp-client', @@ -81,7 +95,6 @@ describe('MCP Server Integration Tests', () => { afterEach(async () => { await client.close(); await server.close(); - vi.unstubAllGlobals(); }); it('should register exactly 29 tools', async () => { @@ -99,11 +112,9 @@ describe('MCP Server Integration Tests', () => { }); it('should route list_plugins tool call to GET /api/plugins', async () => { - fetchMock.mockResolvedValueOnce({ - ok: true, + mockAxios.mockResolvedValueOnce({ status: 200, - headers: new Headers({ 'content-type': 'application/json' }), - text: async () => JSON.stringify([{ name: 'smtp', channel: 'email' }]) + data: [{ name: 'smtp', channel: 'email' }] }); const response = await client.callTool({ @@ -111,7 +122,8 @@ describe('MCP Server Integration Tests', () => { arguments: {} }); - expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/plugins', expect.objectContaining({ + expect(mockAxios).toHaveBeenCalledWith(expect.objectContaining({ + url: 'http://localhost:3000/api/plugins', method: 'GET', headers: expect.objectContaining({ 'Authorization': 'Bearer test-api-key' @@ -125,11 +137,9 @@ describe('MCP Server Integration Tests', () => { }); it('should route create_template tool call to POST /api/templates/create', async () => { - fetchMock.mockResolvedValueOnce({ - ok: true, + mockAxios.mockResolvedValueOnce({ status: 201, - headers: new Headers({ 'content-type': 'application/json' }), - text: async () => JSON.stringify({ success: true, id: 'temp-123' }) + data: { success: true, id: 'temp-123' } }); const templateArgs = { @@ -145,9 +155,10 @@ describe('MCP Server Integration Tests', () => { arguments: templateArgs }); - expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/templates/create', expect.objectContaining({ + expect(mockAxios).toHaveBeenCalledWith(expect.objectContaining({ + url: 'http://localhost:3000/api/templates/create', method: 'POST', - body: JSON.stringify(templateArgs), + data: templateArgs, headers: expect.objectContaining({ 'Authorization': 'Bearer test-api-key' }) @@ -159,11 +170,9 @@ describe('MCP Server Integration Tests', () => { }); it('should route delete_alert tool call to DELETE /api/alerts/:id', async () => { - fetchMock.mockResolvedValueOnce({ - ok: true, + mockAxios.mockResolvedValueOnce({ status: 200, - headers: new Headers({ 'content-type': 'application/json' }), - text: async () => JSON.stringify({ success: true }) + data: { success: true } }); const response = await client.callTool({ @@ -173,7 +182,8 @@ describe('MCP Server Integration Tests', () => { } }); - expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/alerts/60d5ec48f83c2c2e88a53820', expect.objectContaining({ + expect(mockAxios).toHaveBeenCalledWith(expect.objectContaining({ + url: 'http://localhost:3000/api/alerts/60d5ec48f83c2c2e88a53820', method: 'DELETE', headers: expect.objectContaining({ 'Authorization': 'Bearer test-api-key' From e31d42f5b622b4fb9992992dce4ba0ab8ddea43c Mon Sep 17 00:00:00 2001 From: Adhish-Krishna Date: Tue, 21 Jul 2026 11:22:16 +0530 Subject: [PATCH 5/9] feat: add build and publish gh action for npm packages --- .github/workflows/publish-packages.yml | 130 +++++++++++++++++++ packages/create-simplens-plugin/package.json | 2 +- 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish-packages.yml diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml new file mode 100644 index 0000000..b43c368 --- /dev/null +++ b/.github/workflows/publish-packages.yml @@ -0,0 +1,130 @@ +name: NPM Packages CI/CD + +on: + push: + branches: + - main + - master + - development + - develop + paths: + - 'packages/**' + pull_request: + branches: + - main + - master + - development + - develop + paths: + - 'packages/**' + workflow_dispatch: + +permissions: + contents: read + +jobs: + detect-packages: + runs-on: ubuntu-latest + outputs: + packages: ${{ steps.filter.outputs.changes }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Detect changed package paths + uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + config-gen: 'packages/config-gen/**' + create-simplens-plugin: 'packages/create-simplens-plugin/**' + mcp-server: 'packages/mcp-server/**' + onboard: 'packages/onboard/**' + sdk: 'packages/sdk/**' + + build-and-test: + needs: detect-packages + if: needs.detect-packages.outputs.packages != '[]' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + package: ${{ fromJSON(needs.detect-packages.outputs.packages) }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install package dependencies + working-directory: packages/${{ matrix.package }} + run: npm install + + - name: Build package + working-directory: packages/${{ matrix.package }} + run: npm run build --if-present + + - name: Run package tests (if present) + working-directory: packages/${{ matrix.package }} + run: npm test --if-present + env: + CI: true + + publish: + needs: [detect-packages, build-and-test] + if: | + needs.detect-packages.outputs.packages != '[]' && + github.event_name == 'push' && + (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + package: ${{ fromJSON(needs.detect-packages.outputs.packages) }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + + - name: Check package version on NPM + id: check + working-directory: packages/${{ matrix.package }} + run: | + PKG_NAME=$(node -p "require('./package.json').name") + PKG_VERSION=$(node -p "require('./package.json').version") + echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT + echo "pkg_version=$PKG_VERSION" >> $GITHUB_OUTPUT + + if npm view "$PKG_NAME@$PKG_VERSION" version > /dev/null 2>&1; then + echo "published=true" >> $GITHUB_OUTPUT + echo "â„šī¸ Version $PKG_VERSION of $PKG_NAME is ALREADY published on NPM. Skipping publish." + else + echo "published=false" >> $GITHUB_OUTPUT + echo "🚀 Version $PKG_VERSION of $PKG_NAME is NEW! Proceeding to publish." + fi + + - name: Install package dependencies + if: steps.check.outputs.published == 'false' + working-directory: packages/${{ matrix.package }} + run: npm install + + - name: Build package + if: steps.check.outputs.published == 'false' + working-directory: packages/${{ matrix.package }} + run: npm run build --if-present + + - name: Publish package to NPM + if: steps.check.outputs.published == 'false' + working-directory: packages/${{ matrix.package }} + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/packages/create-simplens-plugin/package.json b/packages/create-simplens-plugin/package.json index f276ff2..a1055f2 100644 --- a/packages/create-simplens-plugin/package.json +++ b/packages/create-simplens-plugin/package.json @@ -10,7 +10,7 @@ }, "scripts": { "build": "tsc && npm run build:copy", - "build:copy": "xcopy /E /I /Y src\\templates dist\\templates", + "build:copy": "node -e \"require('fs').cpSync('src/templates', 'dist/templates', { recursive: true })\"", "start": "node dist/index.js", "dev": "tsx src/index.ts", "test": "vitest run", From 6d3b3dadacebb982e8926a31c9f96f0db52eeb0d Mon Sep 17 00:00:00 2001 From: Adhish-Krishna Date: Tue, 21 Jul 2026 11:36:37 +0530 Subject: [PATCH 6/9] fix(create-simplens-plugin): add vitest.config.ts file --- packages/create-simplens-plugin/package.json | 4 ++-- packages/create-simplens-plugin/src/generator.test.ts | 9 +++------ packages/create-simplens-plugin/vitest.config.ts | 9 +++++++++ 3 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 packages/create-simplens-plugin/vitest.config.ts diff --git a/packages/create-simplens-plugin/package.json b/packages/create-simplens-plugin/package.json index a1055f2..7574356 100644 --- a/packages/create-simplens-plugin/package.json +++ b/packages/create-simplens-plugin/package.json @@ -13,8 +13,8 @@ "build:copy": "node -e \"require('fs').cpSync('src/templates', 'dist/templates', { recursive: true })\"", "start": "node dist/index.js", "dev": "tsx src/index.ts", - "test": "vitest run", - "test:watch": "vitest", + "test": "vitest run --config vitest.config.ts", + "test:watch": "vitest --config vitest.config.ts", "prepublishOnly": "npm run build", "prepare": "husky" }, diff --git a/packages/create-simplens-plugin/src/generator.test.ts b/packages/create-simplens-plugin/src/generator.test.ts index 700b6af..822bba2 100644 --- a/packages/create-simplens-plugin/src/generator.test.ts +++ b/packages/create-simplens-plugin/src/generator.test.ts @@ -22,12 +22,9 @@ vi.mock('ora', () => ({ })); vi.mock('chalk', () => ({ - default: { - green: vi.fn((s) => s), - cyan: vi.fn((s) => s), - yellow: vi.fn((s) => s), - bold: vi.fn((s) => s), - }, + default: new Proxy({}, { + get: () => (s: unknown) => s, + }), })); vi.mock('./utils/git.js', () => ({ diff --git a/packages/create-simplens-plugin/vitest.config.ts b/packages/create-simplens-plugin/vitest.config.ts new file mode 100644 index 0000000..0ea0d98 --- /dev/null +++ b/packages/create-simplens-plugin/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); From 9841fec5086a6367bcc5db9283f9c54f36817a22 Mon Sep 17 00:00:00 2001 From: Adhish-Krishna Date: Tue, 21 Jul 2026 11:42:45 +0530 Subject: [PATCH 7/9] feat(create-simplens-plugin): npm version patch --- packages/create-simplens-plugin/package-lock.json | 4 ++-- packages/create-simplens-plugin/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/create-simplens-plugin/package-lock.json b/packages/create-simplens-plugin/package-lock.json index 7b3376d..46c1112 100644 --- a/packages/create-simplens-plugin/package-lock.json +++ b/packages/create-simplens-plugin/package-lock.json @@ -1,12 +1,12 @@ { "name": "@simplens/create-simplens-plugin", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@simplens/create-simplens-plugin", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { "@clack/prompts": "^1.0.0", diff --git a/packages/create-simplens-plugin/package.json b/packages/create-simplens-plugin/package.json index 7574356..0cbc249 100644 --- a/packages/create-simplens-plugin/package.json +++ b/packages/create-simplens-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@simplens/create-simplens-plugin", - "version": "1.0.1", + "version": "1.0.2", "description": "CLI tool to scaffold new SimpleNS notification plugins", "main": "dist/index.js", "types": "dist/index.d.ts", From b67a6d20b434cb251ce5fda8729a7917726429c9 Mon Sep 17 00:00:00 2001 From: Adhish-Krishna Date: Tue, 21 Jul 2026 11:43:10 +0530 Subject: [PATCH 8/9] fix(mcp-server): add env fallkback for ci testing --- packages/mcp-server/tests/verify-stdio.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mcp-server/tests/verify-stdio.ts b/packages/mcp-server/tests/verify-stdio.ts index b13ded7..ea213f2 100644 --- a/packages/mcp-server/tests/verify-stdio.ts +++ b/packages/mcp-server/tests/verify-stdio.ts @@ -13,8 +13,8 @@ async function main() { args: ['dist/index.js', '--stdio'], env: { ...process.env, - NS_API_KEY: process.env.NS_API_KEY || "", - SIMPLENS_CORE_URL: process.env.SIMPLENS_CORE_URL || "", + NS_API_KEY: process.env.NS_API_KEY || "dummy_api_key_for_stdio_verification", + SIMPLENS_CORE_URL: process.env.SIMPLENS_CORE_URL || "http://localhost:3000", } }); From 81d98f2b19e7eb3eed693d8643fa1b4d86a4ff84 Mon Sep 17 00:00:00 2001 From: Adhish-Krishna Date: Tue, 21 Jul 2026 12:03:30 +0530 Subject: [PATCH 9/9] docs(mcp-server): update README --- packages/mcp-server/README.md | 99 +++++++++++++++++++-------- packages/mcp-server/package-lock.json | 4 +- packages/mcp-server/package.json | 2 +- 3 files changed, 75 insertions(+), 30 deletions(-) diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 60cf973..644f1b3 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -1,35 +1,32 @@ # SimpleNS MCP Server -A Model Context Protocol (MCP) server for the SimpleNS notification engine. Allows AI assistants like Claude Desktop and Cursor to interact with your SimpleNS instance to send notifications, check analytics, and manage alerts. +A Model Context Protocol (MCP) server for the SimpleNS notification orchestration engine (`@simplens/mcp`). Enables AI assistants like Claude Desktop, Cursor, and custom MCP clients to send notifications, manage templates, query delivery logs, resolve alerts, inspect analytics, and configure channel providers. -## Features +--- -- **Streamable HTTP Transport**: Local usage via npm package. -- **Stdio Transport**: Local usage via command line/npm package. +## Features -- **Tool Set**: - - `send_notification` / `send_batch_notification` (supports templates and inline content) - - `get_send_schema` — full schema docs with examples for AI agents - - `list_plugins` - - `find_failures` / `retry_failure` - - `list_alerts` / `resolve_alert` +- **Streamable HTTP Transport**: Connect remote or local MCP clients via HTTP (`/mcp`). +- **Stdio Transport**: Local execution via command line (`--stdio`). +- **Full Tool Suite (29 Tools)**: Comprehensive coverage for notifications, templates, alerts, logs, metrics, and channel configuration. -## Installation +--- -No installation is required when using `npx`. +## Installation & Usage -## Usage +No pre-installation is required when using `npx`. ### Streamable HTTP (Local via npm package) Run the server locally: + ```bash npx -y @simplens/mcp ``` -The server starts default at port: `3001` +The server starts by default on port `3001`. -Then point your MCP client at the local HTTP endpoint and pass headers on every request: +Configure your MCP client: ```json { @@ -69,9 +66,9 @@ Then point your MCP client at the local HTTP endpoint and pass headers on every ### Stdio (Local via npm package) -You can run the server locally if you have SimpleNS running locally. +Run the MCP server in stdio mode alongside a running SimpleNS backend: -Add to your MCP Client config: +Add to your MCP Client configuration: ```json { @@ -88,15 +85,63 @@ Add to your MCP Client config: } ``` -## Tools Reference +--- + +## Complete Tools Reference (29 Tools) + +### 1. Notification Dispatch +| Tool | Description | +| :--- | :--- | +| `send_notification` | Send a single notification via any installed channel (Email, Slack, SMS, Webhook, etc.) with inline content or template references. | +| `send_batch_notification` | Dispatch bulk notifications to multiple recipients or channels simultaneously. | + +### 2. Plugin & Schema Discovery +| Tool | Description | +| :--- | :--- | +| `list_plugins` | List all active notification channel plugins installed on the SimpleNS core instance. | +| `get_send_schema` | Retrieve payload JSON schemas, required fields, and credential specifications for any channel plugin. | + +### 3. Template Management +| Tool | Description | +| :--- | :--- | +| `create_template` | Create a new reusable notification template with subject and body placeholders. | +| `list_templates` | Query and list existing notification templates with pagination. | +| `get_template_by_id` | Fetch full details, metadata, and body content of a specific template. | +| `update_template` | Modify an existing notification template by ID. | +| `delete_template` | Delete a notification template from the system. | + +### 4. Notification History & Logs +| Tool | Description | +| :--- | :--- | +| `list_notifications` | Query historical notification delivery logs with filtering by channel, status, recipient, or date. | +| `get_recent_notifications` | Fetch a quick snapshot of the most recent notification dispatches. | +| `get_notification_by_id` | Inspect execution details, logs, and status of a specific notification. | +| `delete_notification` | Delete a notification record from history. | + +### 5. Alerts, Failures & DLQ Management +| Tool | Description | +| :--- | :--- | +| `list_alerts` | List active delivery failure alerts and dead-letter queue (DLQ) entries. | +| `delete_alert` | Dismiss or delete a specific delivery failure alert. | +| `find_failures` | Search delivery failure logs by channel, error code/message, or timeframe. | +| `retry_failure` | Manually re-queue and retry a failed notification dispatch by ID. | +| `resolve_alert_with_retry` | Resolve a failure alert and trigger an immediate notification retry. | +| `bulk_resolve_alerts` | Bulk resolve multiple delivery alerts with optional bulk retries. | + +### 6. Dashboard Analytics & Metrics +| Tool | Description | +| :--- | :--- | +| `get_dashboard_stats` | Get high-level system analytics (total sent, success rate, failure rate, active channels). | +| `get_dashboard_trends` | Retrieve notification volume, error rate, and delivery latency trends over time. | +### 7. Admin Channels & Configuration | Tool | Description | -|------|-------------| -| `send_notification` | Send a single notification via any channel (supports templates and inline content) | -| `send_batch_notification` | Send batch notifications to multiple recipients | -| `get_send_schema` | Get full request schema with examples — call before sending if unsure about format | -| `list_plugins` | List installed plugins, channels, and their schemas | -| `find_failures` | Find failed notifications with filters (channel, date, search) | -| `retry_failure` | Retry a specific failed notification by ID | -| `list_alerts` | List unresolved system alerts (ghost delivery, stuck processing) | -| `resolve_alert` | Dismiss a specific system alert | +| :--- | :--- | +| `list_admin_channel_providers` | List available channel providers (SMTP, SendGrid, Twilio, Slack Webhook, Telegram, etc.). | +| `list_admin_channels` | List all configured admin notification channels. | +| `get_admin_channel` | Retrieve configuration details and settings for a specific admin channel. | +| `create_admin_channel` | Configure and save a new admin notification channel. | +| `update_admin_channel` | Modify settings or credentials for an existing admin channel. | +| `delete_admin_channel` | Delete an admin channel configuration. | +| `test_admin_channel` | Execute a live connection and credential test for an admin channel. | +| `validate_admin_channel_config` | Validate configuration parameters against provider requirements before saving. | diff --git a/packages/mcp-server/package-lock.json b/packages/mcp-server/package-lock.json index b485fd3..e07e052 100644 --- a/packages/mcp-server/package-lock.json +++ b/packages/mcp-server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@simplens/mcp", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@simplens/mcp", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 4e38140..ca44c41 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@simplens/mcp", - "version": "1.1.0", + "version": "1.1.1", "description": "Remote MCP server for SimpleNS notification orchestration engine", "type": "module", "main": "dist/index.js",