diff --git a/services/mcp-server/src/lib/platform-client.ts b/services/mcp-server/src/lib/platform-client.ts index f3cb252c..298c2e6a 100644 --- a/services/mcp-server/src/lib/platform-client.ts +++ b/services/mcp-server/src/lib/platform-client.ts @@ -528,3 +528,284 @@ export function trackerPublicStats(opts: PlatformClientOptions): Promise<{ totalVotes: number; }>('/api/public/roadmap/stats', { method: 'GET' }, opts); } + +// ── Flags ───────────────────────────────────────────────────────────────────── + +export interface FeatureFlagDoc { + id: string; + productId: string; + key: string; + enabled: boolean; + percentage: number; + description?: string; + platforms: string[]; + regions?: string[]; + createdAt: string; + updatedAt: string; +} + +export function flagsList(opts: PlatformClientOptions): Promise<{ flags: FeatureFlagDoc[] }> { + return platformFetch<{ flags: FeatureFlagDoc[] }>('/api/flags', { method: 'GET' }, opts); +} + +export function flagsGet(key: string, opts: PlatformClientOptions): Promise { + return platformFetch( + `/api/flags/${encodeURIComponent(key)}`, + { method: 'GET' }, + opts + ); +} + +export async function flagsUpsert( + key: string, + input: { + enabled: boolean; + percentage?: number; + description?: string; + platforms?: string[]; + regions?: string[]; + }, + opts: PlatformClientOptions +): Promise { + // Try update first; fall back to create if not found + try { + return await platformFetch( + `/api/flags/${encodeURIComponent(key)}`, + { method: 'PUT', body: JSON.stringify(input) }, + opts + ); + } catch (err) { + if (err instanceof Error && err.message.includes('404')) { + return platformFetch( + '/api/flags', + { method: 'POST', body: JSON.stringify({ key, ...input }) }, + opts + ); + } + throw err; + } +} + +export function flagsDelete( + key: string, + opts: PlatformClientOptions +): Promise<{ success: boolean }> { + return platformFetch<{ success: boolean }>( + `/api/flags/${encodeURIComponent(key)}`, + { method: 'DELETE' }, + opts + ); +} + +export function flagsKillSwitch( + input: { platform?: string; keys?: string[] }, + opts: PlatformClientOptions +): Promise<{ disabled: string[]; count: number }> { + return platformFetch<{ disabled: string[]; count: number }>( + '/api/flags/kill', + { method: 'POST', body: JSON.stringify(input) }, + opts + ); +} + +// ── Jobs ────────────────────────────────────────────────────────────────────── + +export function jobsList(opts: PlatformClientOptions): Promise { + return platformFetch('/api/jobs', { method: 'GET' }, opts); +} + +export function jobsGet(id: string, opts: PlatformClientOptions): Promise { + return platformFetch(`/api/jobs/${encodeURIComponent(id)}`, { method: 'GET' }, opts); +} + +export function jobsTrigger(jobName: string, opts: PlatformClientOptions): Promise { + return platformFetch( + '/api/jobs/trigger', + { method: 'POST', body: JSON.stringify({ jobName }) }, + opts + ); +} + +export function jobsListRuns( + name: string, + limit: number, + opts: PlatformClientOptions +): Promise { + return platformFetch( + `/api/jobs/${encodeURIComponent(name)}/runs?limit=${limit}`, + { method: 'GET' }, + opts + ); +} + +// ── Maintenance ─────────────────────────────────────────────────────────────── + +export function maintenanceGetCurrent(opts: PlatformClientOptions): Promise { + return platformFetch('/api/settings/maintenance/full', { method: 'GET' }, opts); +} + +export function maintenanceSet( + input: { + mode: 'none' | 'scheduled' | 'active' | 'emergency'; + message?: string; + affectedServices?: string[]; + scheduledStart?: string; + scheduledEnd?: string; + }, + opts: PlatformClientOptions +): Promise { + return platformFetch( + '/api/settings/maintenance', + { method: 'PUT', body: JSON.stringify(input) }, + opts + ); +} + +export function maintenanceScheduleWindow( + input: { + title: string; + message: string; + mode: string; + scheduledStart: string; + scheduledEnd: string; + affectedServices?: string[]; + }, + opts: PlatformClientOptions +): Promise { + return platformFetch( + '/api/settings/maintenance/schedule', + { method: 'POST', body: JSON.stringify(input) }, + opts + ); +} + +// ── Settings ────────────────────────────────────────────────────────────────── + +export function settingsGet(opts: PlatformClientOptions): Promise { + return platformFetch('/api/settings', { method: 'GET' }, opts); +} + +export function settingsUpdate( + settings: Record, + opts: PlatformClientOptions +): Promise { + return platformFetch( + '/api/settings', + { method: 'PUT', body: JSON.stringify({ settings }) }, + opts + ); +} + +export function settingsCheckKillSwitch( + productId: string, + opts: Pick +): Promise<{ enabled: boolean; disabled: boolean; message: string }> { + return platformFetch<{ enabled: boolean; disabled: boolean; message: string }>( + `/api/settings/kill-switch?productId=${encodeURIComponent(productId)}`, + { method: 'GET' }, + opts + ); +} + +// ── Webhooks ────────────────────────────────────────────────────────────────── + +export function webhooksListSubscriptions( + productId: string, + opts: PlatformClientOptions +): Promise { + return platformFetch( + `/api/webhooks/subscriptions?productId=${encodeURIComponent(productId)}`, + { method: 'GET' }, + opts + ); +} + +export function webhooksCreate( + input: { + productId: string; + url: string; + events: string[]; + description?: string; + enabled?: boolean; + }, + opts: PlatformClientOptions +): Promise { + return platformFetch( + '/api/webhooks/subscriptions', + { method: 'POST', body: JSON.stringify(input) }, + opts + ); +} + +export function webhooksGet( + id: string, + productId: string, + opts: PlatformClientOptions +): Promise { + return platformFetch( + `/api/webhooks/subscriptions/${encodeURIComponent(id)}?productId=${encodeURIComponent(productId)}`, + { method: 'GET' }, + opts + ); +} + +export function webhooksUpdate( + id: string, + productId: string, + input: { url?: string; events?: string[]; enabled?: boolean; description?: string }, + opts: PlatformClientOptions +): Promise { + return platformFetch( + `/api/webhooks/subscriptions/${encodeURIComponent(id)}?productId=${encodeURIComponent(productId)}`, + { method: 'PATCH', body: JSON.stringify(input) }, + opts + ); +} + +export function webhooksDelete( + id: string, + productId: string, + opts: PlatformClientOptions +): Promise<{ success: boolean }> { + return platformFetch<{ success: boolean }>( + `/api/webhooks/subscriptions/${encodeURIComponent(id)}?productId=${encodeURIComponent(productId)}`, + { method: 'DELETE' }, + opts + ); +} + +export function webhooksListDeliveries( + id: string, + limit: number, + opts: PlatformClientOptions +): Promise { + return platformFetch( + `/api/webhooks/subscriptions/${encodeURIComponent(id)}/deliveries?limit=${limit}`, + { method: 'GET' }, + opts + ); +} + +export function webhooksTest( + id: string, + productId: string, + opts: PlatformClientOptions +): Promise<{ success: boolean; message: string }> { + return platformFetch<{ success: boolean; message: string }>( + `/api/webhooks/subscriptions/${encodeURIComponent(id)}/test?productId=${encodeURIComponent(productId)}`, + { method: 'POST', body: '{}' }, + opts + ); +} + +export function webhooksRotateSecret( + id: string, + productId: string, + opts: PlatformClientOptions +): Promise<{ secret: string; message: string }> { + return platformFetch<{ secret: string; message: string }>( + `/api/webhooks/subscriptions/${encodeURIComponent(id)}/rotate-secret?productId=${encodeURIComponent(productId)}`, + { method: 'POST', body: '{}' }, + opts + ); +} diff --git a/services/mcp-server/src/modules/platform/ops-tools.ts b/services/mcp-server/src/modules/platform/ops-tools.ts new file mode 100644 index 00000000..a80aaa67 --- /dev/null +++ b/services/mcp-server/src/modules/platform/ops-tools.ts @@ -0,0 +1,313 @@ +/** + * Platform ops MCP tools — flags.*, jobs.*, maintenance.*, settings.* + * + * Backed by: platform-service (port 4003). + * All tools require admin role. + */ + +import { z } from 'zod'; +import { registerTool } from '../tools/registry.js'; +import { + flagsList, + flagsGet, + flagsUpsert, + flagsDelete, + flagsKillSwitch, + jobsList, + jobsGet, + jobsTrigger, + jobsListRuns, + maintenanceGetCurrent, + maintenanceSet, + maintenanceScheduleWindow, + settingsGet, + settingsUpdate, + settingsCheckKillSwitch, +} from '../../lib/platform-client.js'; +import type { McpToolRequest } from '../tools/types.js'; + +const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7); + +// ── flags.list ──────────────────────────────────────────────────────────────── + +registerTool({ + name: 'flags.list', + description: + 'List all feature flags for a product (key, enabled, percentage, platforms, regions). Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + productId: z.string().min(1).describe('Product ID to scope the flags'), + }), + async execute(args, req) { + return flagsList({ token: tokenOf(req), requestId: req.id, productId: args.productId }); + }, +}); + +// ── flags.get ───────────────────────────────────────────────────────────────── + +registerTool({ + name: 'flags.get', + description: + 'Get a single feature flag by key including percentage rollout, platform targeting, and region targeting. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + key: z.string().min(1).describe('Flag key (e.g. "new_dashboard", "kill_switch")'), + productId: z.string().min(1).describe('Product ID'), + }), + async execute(args, req) { + return flagsGet(args.key, { + token: tokenOf(req), + requestId: req.id, + productId: args.productId, + }); + }, +}); + +// ── flags.upsert ────────────────────────────────────────────────────────────── + +registerTool({ + name: 'flags.upsert', + description: + 'Create or update a feature flag. If the key already exists it is updated; otherwise created. Percentage is 0-100 rollout. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + key: z.string().min(1).describe('Flag key (snake_case, e.g. "new_onboarding_flow")'), + productId: z.string().min(1).describe('Product ID'), + enabled: z.boolean().describe('Whether the flag is active'), + percentage: z.coerce + .number() + .min(0) + .max(100) + .optional() + .default(100) + .describe('Rollout % (0-100)'), + description: z.string().optional().describe('Human-readable description'), + platforms: z + .array(z.string()) + .optional() + .default([]) + .describe('Platform allowlist (ios, android, web, desktop) — empty = all platforms'), + regions: z + .array(z.string()) + .optional() + .describe('Region allowlist (us, eu, apac) — empty = all regions'), + }), + async execute(args, req) { + const { key, productId, ...input } = args; + return flagsUpsert(key, input, { token: tokenOf(req), requestId: req.id, productId }); + }, +}); + +// ── flags.delete ────────────────────────────────────────────────────────────── + +registerTool({ + name: 'flags.delete', + description: 'Permanently delete a feature flag by key. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + key: z.string().min(1).describe('Flag key to delete'), + productId: z.string().min(1).describe('Product ID'), + }), + async execute(args, req) { + return flagsDelete(args.key, { + token: tokenOf(req), + requestId: req.id, + productId: args.productId, + }); + }, +}); + +// ── flags.killSwitch ────────────────────────────────────────────────────────── + +registerTool({ + name: 'flags.killSwitch', + description: + 'Emergency: disable all flags (or specific keys/platform) at once. Returns list of disabled flag keys and count. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + productId: z.string().min(1).describe('Product ID'), + platform: z + .string() + .optional() + .describe('Only disable flags for this platform (ios/android/web/desktop)'), + keys: z + .array(z.string()) + .optional() + .describe('Specific flag keys to disable (empty = disable all matching flags)'), + }), + async execute(args, req) { + const { productId, ...input } = args; + return flagsKillSwitch(input, { token: tokenOf(req), requestId: req.id, productId }); + }, +}); + +// ── jobs.list ───────────────────────────────────────────────────────────────── + +registerTool({ + name: 'jobs.list', + description: + 'List all registered background job definitions (name, schedule, enabled, lastRunAt). Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({}), + async execute(_args, req) { + return jobsList({ token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── jobs.get ────────────────────────────────────────────────────────────────── + +registerTool({ + name: 'jobs.get', + description: + 'Get a specific job definition by its ID (e.g. "job_cleanup_expired_sessions"). Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + jobId: z.string().min(1).describe('Job definition ID (e.g. "job_cleanup_expired_sessions")'), + }), + async execute(args, req) { + return jobsGet(args.jobId, { token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── jobs.trigger ────────────────────────────────────────────────────────────── + +registerTool({ + name: 'jobs.trigger', + description: + 'Manually trigger a named background job outside its normal schedule. Returns the run record with status and timing. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + jobName: z + .string() + .min(1) + .describe('Registered job name (e.g. "cleanup_expired_sessions", "send_daily_brief")'), + }), + async execute(args, req) { + return jobsTrigger(args.jobName, { token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── jobs.listRuns ───────────────────────────────────────────────────────────── + +registerTool({ + name: 'jobs.listRuns', + description: + 'List recent run records for a job (status, startedAt, finishedAt, error if any). Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + jobName: z.string().min(1).describe('Job name to get runs for'), + limit: z.coerce.number().min(1).max(100).default(20), + }), + async execute(args, req) { + return jobsListRuns(args.jobName, args.limit, { token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── maintenance.getCurrent ──────────────────────────────────────────────────── + +registerTool({ + name: 'maintenance.getCurrent', + description: + 'Get the current maintenance configuration including mode, bypass rules, message, and upcoming scheduled windows. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({}), + async execute(_args, req) { + return maintenanceGetCurrent({ token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── maintenance.set ─────────────────────────────────────────────────────────── + +registerTool({ + name: 'maintenance.set', + description: + 'Set the maintenance mode (none | scheduled | active | emergency). Use "active" for immediate maintenance, "emergency" for critical incidents, "none" to restore service. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + mode: z + .enum(['none', 'scheduled', 'active', 'emergency']) + .describe('"none" = service restored, "active" = maintenance now, "emergency" = critical'), + message: z.string().optional().describe('User-facing maintenance message'), + affectedServices: z.array(z.string()).optional().describe('List of affected service names'), + scheduledStart: z + .string() + .optional() + .describe('ISO 8601 scheduled start (for "scheduled" mode)'), + scheduledEnd: z.string().optional().describe('ISO 8601 scheduled end'), + }), + async execute(args, req) { + return maintenanceSet(args, { token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── maintenance.scheduleWindow ──────────────────────────────────────────────── + +registerTool({ + name: 'maintenance.scheduleWindow', + description: + 'Schedule a future maintenance window with a title, message, start/end times, and list of affected services. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + title: z.string().min(1).describe('Short title (e.g. "Database migration v2.4")'), + message: z.string().min(1).describe('User-facing message during the window'), + mode: z.enum(['scheduled', 'active']).default('scheduled'), + scheduledStart: z.string().min(1).describe('ISO 8601 start time'), + scheduledEnd: z.string().min(1).describe('ISO 8601 end time (must be after start)'), + affectedServices: z.array(z.string()).optional().describe('Affected service names'), + }), + async execute(args, req) { + return maintenanceScheduleWindow(args, { token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── settings.get ────────────────────────────────────────────────────────────── + +registerTool({ + name: 'settings.get', + description: + 'Get user settings for the authenticated user (global settings + device overrides map). Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + productId: z.string().min(1).describe('Product ID to scope the settings'), + }), + async execute(args, req) { + return settingsGet({ token: tokenOf(req), requestId: req.id, productId: args.productId }); + }, +}); + +// ── settings.update ─────────────────────────────────────────────────────────── + +registerTool({ + name: 'settings.update', + description: + 'Merge-update user settings for the authenticated user. Existing keys are preserved; only provided keys are overwritten. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + productId: z.string().min(1).describe('Product ID'), + settings: z + .record(z.unknown()) + .describe('Key-value pairs to merge into the user settings object'), + }), + async execute(args, req) { + return settingsUpdate(args.settings, { + token: tokenOf(req), + requestId: req.id, + productId: args.productId, + }); + }, +}); + +// ── settings.checkKillSwitch ────────────────────────────────────────────────── + +registerTool({ + name: 'settings.checkKillSwitch', + description: + 'Check if the kill_switch feature flag is active for a product (same endpoint that mobile clients poll at launch). Returns { enabled, disabled, message }. No auth required on backend — useful for ops monitoring.', + requiredRole: 'admin', + inputSchema: z.object({ + productId: z.string().min(1).describe('Product ID to check'), + }), + async execute(args, req) { + return settingsCheckKillSwitch(args.productId, { requestId: req.id }); + }, +}); diff --git a/services/mcp-server/src/modules/platform/webhooks-tools.ts b/services/mcp-server/src/modules/platform/webhooks-tools.ts new file mode 100644 index 00000000..465b413d --- /dev/null +++ b/services/mcp-server/src/modules/platform/webhooks-tools.ts @@ -0,0 +1,178 @@ +/** + * Webhooks MCP tools — webhooks.* + * + * Backed by: platform-service (port 4003) — webhooks module. + * All tools require admin role. + */ + +import { z } from 'zod'; +import { registerTool } from '../tools/registry.js'; +import { config } from '../../lib/config.js'; +import { + webhooksListSubscriptions, + webhooksCreate, + webhooksGet, + webhooksUpdate, + webhooksDelete, + webhooksListDeliveries, + webhooksTest, + webhooksRotateSecret, +} from '../../lib/platform-client.js'; +import type { McpToolRequest } from '../tools/types.js'; + +const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7); + +// ── webhooks.listSubscriptions ──────────────────────────────────────────────── + +registerTool({ + name: 'webhooks.listSubscriptions', + description: + 'List all webhook subscriptions for a product (url, events, enabled, createdAt). Secrets are redacted. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + productId: z.string().min(1).describe('Product ID'), + }), + async execute(args, req) { + return webhooksListSubscriptions(args.productId, { token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── webhooks.create ─────────────────────────────────────────────────────────── + +registerTool({ + name: 'webhooks.create', + description: + 'Create a new webhook subscription. The signing secret is returned ONCE at creation — store it securely. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + productId: z.string().min(1).describe('Product ID'), + url: z.string().url().describe('HTTPS endpoint URL that will receive events'), + events: z + .array(z.string()) + .min(1) + .describe('Event types to subscribe to (e.g. ["user.created", "session.completed"])'), + description: z.string().optional().describe('Human-readable description of this subscription'), + enabled: z.boolean().optional().default(true), + }), + async execute(args, req) { + return webhooksCreate(args, { token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── webhooks.get ────────────────────────────────────────────────────────────── + +registerTool({ + name: 'webhooks.get', + description: + 'Get a single webhook subscription by ID. Secret is partially redacted (first 8 chars shown). Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + subscriptionId: z.string().min(1).describe('Webhook subscription ID'), + productId: z.string().min(1).describe('Product ID'), + }), + async execute(args, req) { + return webhooksGet(args.subscriptionId, args.productId, { + token: tokenOf(req), + requestId: req.id, + }); + }, +}); + +// ── webhooks.update ─────────────────────────────────────────────────────────── + +registerTool({ + name: 'webhooks.update', + description: + 'Update a webhook subscription (url, events list, enabled state, or description). Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + subscriptionId: z.string().min(1).describe('Webhook subscription ID'), + productId: z.string().min(1).describe('Product ID'), + url: z.string().url().optional().describe('New endpoint URL'), + events: z.array(z.string()).optional().describe('Replacement event types list'), + enabled: z.boolean().optional().describe('Enable or disable this subscription'), + description: z.string().optional().describe('Updated description'), + }), + async execute(args, req) { + const { subscriptionId, productId, ...updates } = args; + return webhooksUpdate(subscriptionId, productId, updates, { + token: tokenOf(req), + requestId: req.id, + }); + }, +}); + +// ── webhooks.delete ─────────────────────────────────────────────────────────── + +registerTool({ + name: 'webhooks.delete', + description: 'Permanently delete a webhook subscription. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + subscriptionId: z.string().min(1).describe('Webhook subscription ID to delete'), + productId: z.string().min(1).describe('Product ID'), + }), + async execute(args, req) { + return webhooksDelete(args.subscriptionId, args.productId, { + token: tokenOf(req), + requestId: req.id, + }); + }, +}); + +// ── webhooks.listDeliveries ─────────────────────────────────────────────────── + +registerTool({ + name: 'webhooks.listDeliveries', + description: + 'List recent delivery attempts for a webhook subscription (status, event, attempts, responseCode). Useful for debugging failed deliveries. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + subscriptionId: z.string().min(1).describe('Webhook subscription ID'), + limit: z.coerce.number().min(1).max(config.QUERY_MAX_LIMIT).default(config.QUERY_DEFAULT_LIMIT), + }), + async execute(args, req) { + return webhooksListDeliveries(args.subscriptionId, args.limit, { + token: tokenOf(req), + requestId: req.id, + }); + }, +}); + +// ── webhooks.test ───────────────────────────────────────────────────────────── + +registerTool({ + name: 'webhooks.test', + description: + 'Send a test event payload to a webhook subscription to verify the endpoint is reachable and responding. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + subscriptionId: z.string().min(1).describe('Webhook subscription ID to test'), + productId: z.string().min(1).describe('Product ID'), + }), + async execute(args, req) { + return webhooksTest(args.subscriptionId, args.productId, { + token: tokenOf(req), + requestId: req.id, + }); + }, +}); + +// ── webhooks.rotateSecret ───────────────────────────────────────────────────── + +registerTool({ + name: 'webhooks.rotateSecret', + description: + 'Rotate the HMAC signing secret for a webhook subscription. The new secret is returned ONCE — update your consumer immediately. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + subscriptionId: z.string().min(1).describe('Webhook subscription ID'), + productId: z.string().min(1).describe('Product ID'), + }), + async execute(args, req) { + return webhooksRotateSecret(args.subscriptionId, args.productId, { + token: tokenOf(req), + requestId: req.id, + }); + }, +}); diff --git a/services/mcp-server/src/server.ts b/services/mcp-server/src/server.ts index e5933118..aec4b777 100644 --- a/services/mcp-server/src/server.ts +++ b/services/mcp-server/src/server.ts @@ -13,6 +13,11 @@ * nomgap.* — fasting sessions, push triggers * peakpulse.* — adventure sessions, GPS routes, stats * tracker.* — items, votes, comments, public roadmap + * flags.* — feature flag CRUD + kill switch + * jobs.* — background job list, trigger, run history + * maintenance.* — maintenance mode + scheduled windows + * settings.* — user settings + kill switch check + * webhooks.* — subscription CRUD, deliveries, test, rotate secret * * Auth: JWT Bearer tokens issued by platform-service (same JWT_SECRET). * Role gating: viewer / admin / super_admin per tool. @@ -37,12 +42,14 @@ import './modules/chronomind/chronomind-tools.js'; import './modules/nomgap/nomgap-tools.js'; import './modules/peakpulse/peakpulse-tools.js'; import './modules/tracker/tracker-tools.js'; +import './modules/platform/ops-tools.js'; +import './modules/platform/webhooks-tools.js'; const app = await createServiceApp({ name: 'mcp-server', version: '0.1.0', description: - 'ByteLyst MCP Server — platform.*, extraction.*, support.*, mindlyst.*, lysnrai.*, jarvis.*, chronomind.*, nomgap.*, peakpulse.*, tracker.*', + 'ByteLyst MCP Server — platform.*, extraction.*, support.*, mindlyst.*, lysnrai.*, jarvis.*, chronomind.*, nomgap.*, peakpulse.*, tracker.*, flags.*, jobs.*, maintenance.*, settings.*, webhooks.*', corsOrigin: config.CORS_ORIGIN, logLevel: config.LOG_LEVEL, });