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;
|
totalVotes: number;
|
||||||
}>('/api/public/roadmap/stats', { method: 'GET' }, opts);
|
}>('/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
|
* nomgap.* — fasting sessions, push triggers
|
||||||
* peakpulse.* — adventure sessions, GPS routes, stats
|
* peakpulse.* — adventure sessions, GPS routes, stats
|
||||||
* tracker.* — items, votes, comments, public roadmap
|
* 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).
|
* Auth: JWT Bearer tokens issued by platform-service (same JWT_SECRET).
|
||||||
* Role gating: viewer / admin / super_admin per tool.
|
* 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/nomgap/nomgap-tools.js';
|
||||||
import './modules/peakpulse/peakpulse-tools.js';
|
import './modules/peakpulse/peakpulse-tools.js';
|
||||||
import './modules/tracker/tracker-tools.js';
|
import './modules/tracker/tracker-tools.js';
|
||||||
|
import './modules/platform/ops-tools.js';
|
||||||
|
import './modules/platform/webhooks-tools.js';
|
||||||
|
|
||||||
const app = await createServiceApp({
|
const app = await createServiceApp({
|
||||||
name: 'mcp-server',
|
name: 'mcp-server',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
description:
|
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,
|
corsOrigin: config.CORS_ORIGIN,
|
||||||
logLevel: config.LOG_LEVEL,
|
logLevel: config.LOG_LEVEL,
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user