feat(mcp-server): add 19 platform ops tools — flags.*, jobs.*, maintenance.*, settings.*, webhooks.*
flags (5): list, get, upsert (create-or-update), delete, killSwitch jobs (4): list, get, trigger, listRuns maintenance (3): getCurrent (admin full), set, scheduleWindow settings (3): get, update, checkKillSwitch webhooks (7): listSubscriptions, create, get, update, delete, listDeliveries, test, rotateSecret All backed by existing platform-service endpoints — no new backend changes. MCP server now has 84 tools across 16 namespaces.
This commit is contained in:
parent
33dd530d5f
commit
0f57a48168
@ -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<FeatureFlagDoc> {
|
||||
return platformFetch<FeatureFlagDoc>(
|
||||
`/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<FeatureFlagDoc> {
|
||||
// Try update first; fall back to create if not found
|
||||
try {
|
||||
return await platformFetch<FeatureFlagDoc>(
|
||||
`/api/flags/${encodeURIComponent(key)}`,
|
||||
{ method: 'PUT', body: JSON.stringify(input) },
|
||||
opts
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes('404')) {
|
||||
return platformFetch<FeatureFlagDoc>(
|
||||
'/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<unknown[]> {
|
||||
return platformFetch<unknown[]>('/api/jobs', { method: 'GET' }, opts);
|
||||
}
|
||||
|
||||
export function jobsGet(id: string, opts: PlatformClientOptions): Promise<unknown> {
|
||||
return platformFetch<unknown>(`/api/jobs/${encodeURIComponent(id)}`, { method: 'GET' }, opts);
|
||||
}
|
||||
|
||||
export function jobsTrigger(jobName: string, opts: PlatformClientOptions): Promise<unknown> {
|
||||
return platformFetch<unknown>(
|
||||
'/api/jobs/trigger',
|
||||
{ method: 'POST', body: JSON.stringify({ jobName }) },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
export function jobsListRuns(
|
||||
name: string,
|
||||
limit: number,
|
||||
opts: PlatformClientOptions
|
||||
): Promise<unknown> {
|
||||
return platformFetch<unknown>(
|
||||
`/api/jobs/${encodeURIComponent(name)}/runs?limit=${limit}`,
|
||||
{ method: 'GET' },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
// ── Maintenance ───────────────────────────────────────────────────────────────
|
||||
|
||||
export function maintenanceGetCurrent(opts: PlatformClientOptions): Promise<unknown> {
|
||||
return platformFetch<unknown>('/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<unknown> {
|
||||
return platformFetch<unknown>(
|
||||
'/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<unknown> {
|
||||
return platformFetch<unknown>(
|
||||
'/api/settings/maintenance/schedule',
|
||||
{ method: 'POST', body: JSON.stringify(input) },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
// ── Settings ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export function settingsGet(opts: PlatformClientOptions): Promise<unknown> {
|
||||
return platformFetch<unknown>('/api/settings', { method: 'GET' }, opts);
|
||||
}
|
||||
|
||||
export function settingsUpdate(
|
||||
settings: Record<string, unknown>,
|
||||
opts: PlatformClientOptions
|
||||
): Promise<unknown> {
|
||||
return platformFetch<unknown>(
|
||||
'/api/settings',
|
||||
{ method: 'PUT', body: JSON.stringify({ settings }) },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
export function settingsCheckKillSwitch(
|
||||
productId: string,
|
||||
opts: Pick<PlatformClientOptions, 'requestId'>
|
||||
): 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<unknown[]> {
|
||||
return platformFetch<unknown[]>(
|
||||
`/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<unknown> {
|
||||
return platformFetch<unknown>(
|
||||
'/api/webhooks/subscriptions',
|
||||
{ method: 'POST', body: JSON.stringify(input) },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
export function webhooksGet(
|
||||
id: string,
|
||||
productId: string,
|
||||
opts: PlatformClientOptions
|
||||
): Promise<unknown> {
|
||||
return platformFetch<unknown>(
|
||||
`/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<unknown> {
|
||||
return platformFetch<unknown>(
|
||||
`/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<unknown> {
|
||||
return platformFetch<unknown>(
|
||||
`/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
|
||||
);
|
||||
}
|
||||
|
||||
313
services/mcp-server/src/modules/platform/ops-tools.ts
Normal file
313
services/mcp-server/src/modules/platform/ops-tools.ts
Normal file
@ -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 });
|
||||
},
|
||||
});
|
||||
178
services/mcp-server/src/modules/platform/webhooks-tools.ts
Normal file
178
services/mcp-server/src/modules/platform/webhooks-tools.ts
Normal file
@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user