@fruition/fcp-mcp-server 1.23.0 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +625 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -169,6 +169,28 @@ const TOOL_PERMISSIONS = {
169
169
  unroo_get_my_tasks: 'viewer',
170
170
  unroo_get_parking_lot: 'viewer',
171
171
  unroo_get_backlog: 'viewer',
172
+ // --- Alerting ---
173
+ // admin: hard-delete of a rule and client-facing bulk notifications are
174
+ // higher-stakes than internal alert create/close, so gate them tighter.
175
+ fcp_delete_alert_rule: 'admin',
176
+ fcp_bulk_notify_clients: 'admin',
177
+ // operator: routine alert/outage writes (create, close, update).
178
+ fcp_create_alert: 'operator',
179
+ fcp_update_alert: 'operator',
180
+ fcp_create_alert_rule: 'operator',
181
+ fcp_update_alert_rule: 'operator',
182
+ fcp_create_outage: 'operator',
183
+ fcp_update_outage: 'operator',
184
+ fcp_resolve_outage: 'operator',
185
+ // viewer: reads.
186
+ fcp_list_alerts: 'viewer',
187
+ fcp_get_alert: 'viewer',
188
+ fcp_get_alert_stats: 'viewer',
189
+ fcp_list_alert_rules: 'viewer',
190
+ fcp_get_alert_rule: 'viewer',
191
+ fcp_list_outages: 'viewer',
192
+ fcp_get_outage: 'viewer',
193
+ fcp_get_bulk_notify_history: 'viewer',
172
194
  };
173
195
  // Resolved role for the current API key. Cached only on successful lookup so
174
196
  // transient FCP outages don't pin us to 'none' for the rest of the session.
@@ -368,6 +390,97 @@ class FCPClient {
368
390
  method: 'DELETE',
369
391
  });
370
392
  }
393
+ // --- Alerting: incidents (alert_events), rules, outages, bulk notifications ---
394
+ // Thin wrappers over the FCP alerting HTTP API. Request bodies are camelCase
395
+ // (the API maps to snake_case DB columns internally).
396
+ // Build a query string from defined params (skips undefined/empty values).
397
+ buildQuery(q) {
398
+ const entries = Object.entries(q)
399
+ .filter(([, v]) => v !== undefined && v !== null && v !== '')
400
+ .map(([k, v]) => [k, String(v)]);
401
+ const qs = new URLSearchParams(entries).toString();
402
+ return qs ? `?${qs}` : '';
403
+ }
404
+ // Incidents (individual alerts).
405
+ async listAlerts(q = {}) {
406
+ return this.fetch(`/api/incidents${this.buildQuery(q)}`);
407
+ }
408
+ async getAlert(id) {
409
+ return this.fetch(`/api/incidents/${id}`);
410
+ }
411
+ async createAlert(input) {
412
+ return this.fetch('/api/incidents', {
413
+ method: 'POST',
414
+ body: JSON.stringify(input),
415
+ });
416
+ }
417
+ async updateAlert(id, updates) {
418
+ return this.fetch(`/api/incidents/${id}`, {
419
+ method: 'PATCH',
420
+ body: JSON.stringify(updates),
421
+ });
422
+ }
423
+ async getAlertStats() {
424
+ return this.fetch('/api/incidents/stats');
425
+ }
426
+ // Alert rules (notification configuration).
427
+ async listAlertRules(accountId) {
428
+ return this.fetch(`/api/alerts/rules${this.buildQuery({ accountId })}`);
429
+ }
430
+ async getAlertRule(ruleId) {
431
+ return this.fetch(`/api/alerts/rules/${ruleId}`);
432
+ }
433
+ async createAlertRule(input) {
434
+ return this.fetch('/api/alerts/rules', {
435
+ method: 'POST',
436
+ body: JSON.stringify(input),
437
+ });
438
+ }
439
+ async updateAlertRule(ruleId, updates) {
440
+ return this.fetch(`/api/alerts/rules/${ruleId}`, {
441
+ method: 'PATCH',
442
+ body: JSON.stringify(updates),
443
+ });
444
+ }
445
+ async deleteAlertRule(ruleId) {
446
+ return this.fetch(`/api/alerts/rules/${ruleId}`, {
447
+ method: 'DELETE',
448
+ });
449
+ }
450
+ // Outages (admin outage tracking).
451
+ async listOutages(q = {}) {
452
+ return this.fetch(`/api/alerts/outages${this.buildQuery(q)}`);
453
+ }
454
+ async getOutage(outageId) {
455
+ return this.fetch(`/api/alerts/outages/${outageId}`);
456
+ }
457
+ async createOutage(input) {
458
+ return this.fetch('/api/alerts/outages', {
459
+ method: 'POST',
460
+ body: JSON.stringify(input),
461
+ });
462
+ }
463
+ async updateOutage(outageId, updates) {
464
+ return this.fetch(`/api/alerts/outages/${outageId}`, {
465
+ method: 'PATCH',
466
+ body: JSON.stringify(updates),
467
+ });
468
+ }
469
+ async resolveOutage(outageId) {
470
+ return this.fetch(`/api/alerts/outages/${outageId}`, {
471
+ method: 'DELETE',
472
+ });
473
+ }
474
+ // Bulk client notifications. Sends real emails/SMS to clients — handle with care.
475
+ async bulkNotifyClients(input) {
476
+ return this.fetch('/api/alerts/bulk-notify', {
477
+ method: 'POST',
478
+ body: JSON.stringify(input),
479
+ });
480
+ }
481
+ async getBulkNotifyHistory(limit) {
482
+ return this.fetch(`/api/alerts/bulk-notify${this.buildQuery({ limit })}`);
483
+ }
371
484
  async createLaunch(input) {
372
485
  return this.fetch('/api/launches', {
373
486
  method: 'POST',
@@ -3083,6 +3196,344 @@ const TOOLS = [
3083
3196
  required: ['siteId'],
3084
3197
  },
3085
3198
  },
3199
+ // ===================== Alerting: incidents (alerts) =====================
3200
+ {
3201
+ name: 'fcp_list_alerts',
3202
+ description: 'List alerts (incidents). Defaults to ACTIVE alerts (status firing,acknowledged). Pass status="resolved" for past alerts, or status="all" for everything. Alerts come from alertmanager, GCP monitoring, uptimerobot, cluster_health, or manual creation. Supports filtering by source, severity, cluster, namespace, domain, website, and time range.',
3203
+ inputSchema: {
3204
+ type: 'object',
3205
+ properties: {
3206
+ status: {
3207
+ type: 'string',
3208
+ description: 'Comma-separated statuses. Default "firing,acknowledged" (active). Use "resolved" for past, "silenced", or "all" for everything.',
3209
+ },
3210
+ source: {
3211
+ type: 'string',
3212
+ enum: ['alertmanager', 'gcp_monitoring', 'uptimerobot', 'cluster_health', 'manual'],
3213
+ description: 'Filter by alert source',
3214
+ },
3215
+ severity: {
3216
+ type: 'string',
3217
+ description: 'Comma-separated severities: critical, high, medium, low, info',
3218
+ },
3219
+ cluster: { type: 'string', description: 'Filter by cluster name' },
3220
+ namespace: { type: 'string', description: 'Filter by namespace' },
3221
+ domain: { type: 'string', description: 'Filter by domain (partial match)' },
3222
+ websiteId: { type: 'number', description: 'Filter by FCP website ID' },
3223
+ environment: { type: 'string', description: 'Filter by site environment (production, staging)' },
3224
+ from: { type: 'string', description: 'Start of time range (ISO timestamp), filters started_at' },
3225
+ to: { type: 'string', description: 'End of time range (ISO timestamp), filters started_at' },
3226
+ page: { type: 'number', description: 'Page number (default 1)' },
3227
+ limit: { type: 'number', description: 'Results per page (default 50, max 200)' },
3228
+ },
3229
+ required: [],
3230
+ },
3231
+ },
3232
+ {
3233
+ name: 'fcp_get_alert',
3234
+ description: 'Get full detail for a single alert (incident) by ID, including linked site/account info.',
3235
+ inputSchema: {
3236
+ type: 'object',
3237
+ properties: {
3238
+ id: { type: 'number', description: 'The alert (incident) ID' },
3239
+ },
3240
+ required: ['id'],
3241
+ },
3242
+ },
3243
+ {
3244
+ name: 'fcp_create_alert',
3245
+ description: 'Create a MANUAL alert (incident). Source is always "manual". Use this to manually raise an alert that did not come from automated monitoring. Returns the created alert with status "firing".',
3246
+ inputSchema: {
3247
+ type: 'object',
3248
+ properties: {
3249
+ alertName: { type: 'string', description: 'Short alert name/title (required)' },
3250
+ severity: {
3251
+ type: 'string',
3252
+ enum: ['critical', 'high', 'medium', 'low', 'info'],
3253
+ description: 'Severity (default: medium)',
3254
+ },
3255
+ summary: { type: 'string', description: 'Human-readable summary of the alert' },
3256
+ cluster: { type: 'string', description: 'Affected cluster, if any' },
3257
+ namespace: { type: 'string', description: 'Affected namespace, if any' },
3258
+ domain: { type: 'string', description: 'Affected domain, if any' },
3259
+ websiteId: { type: 'number', description: 'Link to an FCP website ID, if applicable' },
3260
+ },
3261
+ required: ['alertName'],
3262
+ },
3263
+ },
3264
+ {
3265
+ name: 'fcp_update_alert',
3266
+ description: 'Update an alert (incident): acknowledge, silence, resolve (CLOSE), add admin notes, link a site, or link a Unroo task. To CLOSE an alert, set status="resolved". Silencing requires silencedUntil.',
3267
+ inputSchema: {
3268
+ type: 'object',
3269
+ properties: {
3270
+ id: { type: 'number', description: 'The alert (incident) ID' },
3271
+ status: {
3272
+ type: 'string',
3273
+ enum: ['acknowledged', 'resolved', 'silenced'],
3274
+ description: 'New status. "resolved" closes the alert; "silenced" requires silencedUntil.',
3275
+ },
3276
+ acknowledgedBy: { type: 'string', description: 'Who acknowledged (when status=acknowledged)' },
3277
+ silencedUntil: { type: 'string', description: 'ISO timestamp to silence until (required when status=silenced)' },
3278
+ silencedBy: { type: 'string', description: 'Who silenced (when status=silenced)' },
3279
+ adminNotes: { type: 'string', description: 'Internal admin notes' },
3280
+ websiteId: { type: 'number', description: 'Link/relink to an FCP website ID' },
3281
+ unrooTaskId: { type: 'number', description: 'Link to a Unroo task ID' },
3282
+ },
3283
+ required: ['id'],
3284
+ },
3285
+ },
3286
+ {
3287
+ name: 'fcp_get_alert_stats',
3288
+ description: 'Get alert summary statistics for the last 7 days: counts by status, by source, critical-active count, and per-cluster breakdown.',
3289
+ inputSchema: {
3290
+ type: 'object',
3291
+ properties: {},
3292
+ required: [],
3293
+ },
3294
+ },
3295
+ // ===================== Alerting: rules (notification config) =====================
3296
+ {
3297
+ name: 'fcp_list_alert_rules',
3298
+ description: 'List client alert rules (notification configuration). Optionally filter by account.',
3299
+ inputSchema: {
3300
+ type: 'object',
3301
+ properties: {
3302
+ accountId: { type: 'number', description: 'Filter rules by account ID' },
3303
+ },
3304
+ required: [],
3305
+ },
3306
+ },
3307
+ {
3308
+ name: 'fcp_get_alert_rule',
3309
+ description: 'Get a single alert rule by ID, with account and website context.',
3310
+ inputSchema: {
3311
+ type: 'object',
3312
+ properties: {
3313
+ ruleId: { type: 'number', description: 'The alert rule ID' },
3314
+ },
3315
+ required: ['ruleId'],
3316
+ },
3317
+ },
3318
+ {
3319
+ name: 'fcp_create_alert_rule',
3320
+ description: 'Create a client alert rule defining which events trigger notifications, on which channels, with flapping/business-hours controls.',
3321
+ inputSchema: {
3322
+ type: 'object',
3323
+ properties: {
3324
+ accountId: { type: 'number', description: 'Account this rule belongs to (required)' },
3325
+ websiteId: { type: 'number', description: 'Specific website, or omit/null for all sites in the account' },
3326
+ name: { type: 'string', description: 'Rule name (required)' },
3327
+ description: { type: 'string', description: 'Optional description' },
3328
+ eventType: {
3329
+ type: 'string',
3330
+ enum: ['uptime_down', 'uptime_up', 'maintenance', 'cve', 'ssl_expiry', 'domain_expiry'],
3331
+ description: 'Event type that triggers this rule (required)',
3332
+ },
3333
+ severityThreshold: {
3334
+ type: 'string',
3335
+ enum: ['critical', 'high', 'medium', 'low', 'info'],
3336
+ description: 'Minimum severity to trigger (default: info)',
3337
+ },
3338
+ notifyChannels: {
3339
+ type: 'array',
3340
+ items: { type: 'string', enum: ['email', 'teams', 'slack', 'sms'] },
3341
+ description: 'Channels to notify on',
3342
+ },
3343
+ delaySeconds: { type: 'number', description: 'Delay before notifying (flap prevention)' },
3344
+ cooldownSeconds: { type: 'number', description: 'Cooldown between repeat notifications' },
3345
+ businessHoursOnly: { type: 'boolean', description: 'Only notify during business hours' },
3346
+ businessHoursStart: { type: 'string', description: 'Business hours start (e.g. "09:00")' },
3347
+ businessHoursEnd: { type: 'string', description: 'Business hours end (e.g. "17:00")' },
3348
+ businessHoursTimezone: { type: 'string', description: 'IANA timezone for business hours' },
3349
+ createdBy: { type: 'string', description: 'Who created the rule' },
3350
+ },
3351
+ required: ['accountId', 'name', 'eventType'],
3352
+ },
3353
+ },
3354
+ {
3355
+ name: 'fcp_update_alert_rule',
3356
+ description: 'Update an existing alert rule. Only provided fields are changed.',
3357
+ inputSchema: {
3358
+ type: 'object',
3359
+ properties: {
3360
+ ruleId: { type: 'number', description: 'The alert rule ID (required)' },
3361
+ name: { type: 'string', description: 'Rule name' },
3362
+ description: { type: 'string', description: 'Description' },
3363
+ severityThreshold: {
3364
+ type: 'string',
3365
+ enum: ['critical', 'high', 'medium', 'low', 'info'],
3366
+ description: 'Minimum severity to trigger',
3367
+ },
3368
+ notifyChannels: {
3369
+ type: 'array',
3370
+ items: { type: 'string', enum: ['email', 'teams', 'slack', 'sms'] },
3371
+ description: 'Channels to notify on',
3372
+ },
3373
+ delaySeconds: { type: 'number', description: 'Delay before notifying' },
3374
+ cooldownSeconds: { type: 'number', description: 'Cooldown between repeat notifications' },
3375
+ businessHoursOnly: { type: 'boolean', description: 'Only notify during business hours' },
3376
+ businessHoursStart: { type: 'string', description: 'Business hours start' },
3377
+ businessHoursEnd: { type: 'string', description: 'Business hours end' },
3378
+ businessHoursTimezone: { type: 'string', description: 'IANA timezone' },
3379
+ isActive: { type: 'boolean', description: 'Enable/disable the rule' },
3380
+ updatedBy: { type: 'string', description: 'Who updated the rule' },
3381
+ },
3382
+ required: ['ruleId'],
3383
+ },
3384
+ },
3385
+ {
3386
+ name: 'fcp_delete_alert_rule',
3387
+ description: 'Permanently DELETE an alert rule (hard delete). Requires admin. This cannot be undone.',
3388
+ inputSchema: {
3389
+ type: 'object',
3390
+ properties: {
3391
+ ruleId: { type: 'number', description: 'The alert rule ID to delete' },
3392
+ },
3393
+ required: ['ruleId'],
3394
+ },
3395
+ },
3396
+ // ===================== Alerting: outages =====================
3397
+ {
3398
+ name: 'fcp_list_outages',
3399
+ description: 'List active outages (admin outage tracking). Filter by status (active/resolved/all), account, and limit.',
3400
+ inputSchema: {
3401
+ type: 'object',
3402
+ properties: {
3403
+ status: { type: 'string', description: 'Filter: active, resolved, or all (default: active)' },
3404
+ accountId: { type: 'number', description: 'Filter by account ID' },
3405
+ limit: { type: 'number', description: 'Max results' },
3406
+ },
3407
+ required: [],
3408
+ },
3409
+ },
3410
+ {
3411
+ name: 'fcp_get_outage',
3412
+ description: 'Get a single outage by ID, with its timeline updates, notification history, and affected client contacts.',
3413
+ inputSchema: {
3414
+ type: 'object',
3415
+ properties: {
3416
+ outageId: { type: 'number', description: 'The outage ID' },
3417
+ },
3418
+ required: ['outageId'],
3419
+ },
3420
+ },
3421
+ {
3422
+ name: 'fcp_create_outage',
3423
+ description: 'Create an outage record for centralized outage management, optionally linked to an incident.',
3424
+ inputSchema: {
3425
+ type: 'object',
3426
+ properties: {
3427
+ incidentId: { type: 'number', description: 'Linked incident ID (from downtime_incidents)' },
3428
+ websiteId: { type: 'number', description: 'Affected website ID' },
3429
+ accountId: { type: 'number', description: 'Affected account ID' },
3430
+ startedAt: { type: 'string', description: 'Outage start (ISO timestamp)' },
3431
+ domain: { type: 'string', description: 'Affected domain' },
3432
+ environment: { type: 'string', description: 'Environment (production, staging)' },
3433
+ status: {
3434
+ type: 'string',
3435
+ enum: ['active', 'investigating', 'identified', 'monitoring', 'resolved'],
3436
+ description: 'Outage status (default: active)',
3437
+ },
3438
+ severity: {
3439
+ type: 'string',
3440
+ enum: ['critical', 'high', 'medium', 'low', 'info'],
3441
+ description: 'Severity',
3442
+ },
3443
+ likelyCause: { type: 'string', description: 'Likely cause description' },
3444
+ },
3445
+ required: [],
3446
+ },
3447
+ },
3448
+ {
3449
+ name: 'fcp_update_outage',
3450
+ description: 'Update an outage: status, severity, likely cause, admin notes, assignee. Optionally append a timeline updateMessage. ⚠️ Setting notifyClients=true SENDS REAL NOTIFICATIONS to affected clients.',
3451
+ inputSchema: {
3452
+ type: 'object',
3453
+ properties: {
3454
+ outageId: { type: 'number', description: 'The outage ID (required)' },
3455
+ status: {
3456
+ type: 'string',
3457
+ enum: ['active', 'investigating', 'identified', 'monitoring', 'resolved'],
3458
+ description: 'New outage status',
3459
+ },
3460
+ severity: {
3461
+ type: 'string',
3462
+ enum: ['critical', 'high', 'medium', 'low', 'info'],
3463
+ description: 'New severity',
3464
+ },
3465
+ likelyCause: { type: 'string', description: 'Likely cause' },
3466
+ adminNotes: { type: 'string', description: 'Internal admin notes' },
3467
+ assignedTo: { type: 'string', description: 'Assignee' },
3468
+ updateMessage: { type: 'string', description: 'Timeline update message to append' },
3469
+ notifyClients: {
3470
+ type: 'boolean',
3471
+ description: '⚠️ If true, sends real notifications to affected clients. Default false.',
3472
+ },
3473
+ updatedBy: { type: 'string', description: 'Who made the update' },
3474
+ confirm: {
3475
+ type: 'boolean',
3476
+ description: 'Safety gate for client notifications. Only consulted when notifyClients=true: must be explicitly true to actually send. notifyClients=true without confirm=true makes the tool refuse (the rest of the update is NOT applied). No effect when notifyClients is absent/false.',
3477
+ },
3478
+ },
3479
+ required: ['outageId'],
3480
+ },
3481
+ },
3482
+ {
3483
+ name: 'fcp_resolve_outage',
3484
+ description: 'Resolve and close an outage (soft resolve — sets status to resolved and stamps resolved_at). Adds a resolution timeline entry.',
3485
+ inputSchema: {
3486
+ type: 'object',
3487
+ properties: {
3488
+ outageId: { type: 'number', description: 'The outage ID to resolve' },
3489
+ },
3490
+ required: ['outageId'],
3491
+ },
3492
+ },
3493
+ // ===================== Alerting: bulk client notifications =====================
3494
+ {
3495
+ name: 'fcp_bulk_notify_clients',
3496
+ description: '⚠️ SENDS REAL NOTIFICATIONS (email/Teams/Slack/SMS) to all clients affected by the given outages. Requires admin. Use deliberately — this contacts real people.',
3497
+ inputSchema: {
3498
+ type: 'object',
3499
+ properties: {
3500
+ outageIds: {
3501
+ type: 'array',
3502
+ items: { type: 'number' },
3503
+ description: 'Non-empty array of outage IDs whose affected clients should be notified',
3504
+ },
3505
+ notificationType: {
3506
+ type: 'string',
3507
+ enum: ['outage_initial', 'outage_update', 'recovery'],
3508
+ description: 'Type of notification',
3509
+ },
3510
+ subject: { type: 'string', description: 'Notification subject (required)' },
3511
+ message: { type: 'string', description: 'Notification body (required)' },
3512
+ channels: {
3513
+ type: 'array',
3514
+ items: { type: 'string', enum: ['email', 'teams', 'slack', 'sms'] },
3515
+ description: 'Channels to send on (default: ["email"])',
3516
+ },
3517
+ sentBy: { type: 'string', description: 'Who sent the notification' },
3518
+ confirm: {
3519
+ type: 'boolean',
3520
+ description: 'REQUIRED safety gate. Must be explicitly true to actually dispatch. Omitting or false makes the tool refuse and report who would be contacted — call again with confirm=true only after a human has approved sending.',
3521
+ },
3522
+ },
3523
+ required: ['outageIds', 'notificationType', 'subject', 'message'],
3524
+ },
3525
+ },
3526
+ {
3527
+ name: 'fcp_get_bulk_notify_history',
3528
+ description: 'Get history of bulk client notifications (batches sent), most recent first.',
3529
+ inputSchema: {
3530
+ type: 'object',
3531
+ properties: {
3532
+ limit: { type: 'number', description: 'Max results (default 20)' },
3533
+ },
3534
+ required: [],
3535
+ },
3536
+ },
3086
3537
  ];
3087
3538
  // Register tool handlers
3088
3539
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -4287,6 +4738,180 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4287
4738
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4288
4739
  };
4289
4740
  }
4741
+ // ===================== Alerting: incidents (alerts) =====================
4742
+ case 'fcp_list_alerts': {
4743
+ const result = await client.listAlerts(args);
4744
+ return {
4745
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4746
+ };
4747
+ }
4748
+ case 'fcp_get_alert': {
4749
+ const { id } = args;
4750
+ const result = await client.getAlert(id);
4751
+ return {
4752
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4753
+ };
4754
+ }
4755
+ case 'fcp_create_alert': {
4756
+ const { alertName, severity, summary, cluster, namespace, domain, websiteId } = args;
4757
+ const result = await client.createAlert({ alertName, severity, summary, cluster, namespace, domain, websiteId });
4758
+ return {
4759
+ content: [
4760
+ {
4761
+ type: 'text',
4762
+ text: JSON.stringify({ success: true, message: `Alert created: "${alertName}"`, alert: result?.incident ?? result }, null, 2),
4763
+ },
4764
+ ],
4765
+ };
4766
+ }
4767
+ case 'fcp_update_alert': {
4768
+ const { id, ...updates } = args;
4769
+ const result = await client.updateAlert(id, updates);
4770
+ const verb = updates.status === 'resolved' ? 'closed (resolved)' : `updated${updates.status ? ` to ${updates.status}` : ''}`;
4771
+ return {
4772
+ content: [
4773
+ {
4774
+ type: 'text',
4775
+ text: JSON.stringify({ success: true, message: `Alert ${id} ${verb}`, alert: result?.incident ?? result }, null, 2),
4776
+ },
4777
+ ],
4778
+ };
4779
+ }
4780
+ case 'fcp_get_alert_stats': {
4781
+ const result = await client.getAlertStats();
4782
+ return {
4783
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4784
+ };
4785
+ }
4786
+ // ===================== Alerting: rules =====================
4787
+ case 'fcp_list_alert_rules': {
4788
+ const { accountId } = args;
4789
+ const result = await client.listAlertRules(accountId);
4790
+ return {
4791
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4792
+ };
4793
+ }
4794
+ case 'fcp_get_alert_rule': {
4795
+ const { ruleId } = args;
4796
+ const result = await client.getAlertRule(ruleId);
4797
+ return {
4798
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4799
+ };
4800
+ }
4801
+ case 'fcp_create_alert_rule': {
4802
+ const result = await client.createAlertRule(args);
4803
+ return {
4804
+ content: [
4805
+ {
4806
+ type: 'text',
4807
+ text: JSON.stringify({ success: true, message: 'Alert rule created', rule: result?.rule ?? result }, null, 2),
4808
+ },
4809
+ ],
4810
+ };
4811
+ }
4812
+ case 'fcp_update_alert_rule': {
4813
+ const { ruleId, ...updates } = args;
4814
+ const result = await client.updateAlertRule(ruleId, updates);
4815
+ return {
4816
+ content: [
4817
+ {
4818
+ type: 'text',
4819
+ text: JSON.stringify({ success: true, message: `Alert rule ${ruleId} updated`, rule: result?.rule ?? result }, null, 2),
4820
+ },
4821
+ ],
4822
+ };
4823
+ }
4824
+ case 'fcp_delete_alert_rule': {
4825
+ const { ruleId } = args;
4826
+ const result = await client.deleteAlertRule(ruleId);
4827
+ return {
4828
+ content: [
4829
+ {
4830
+ type: 'text',
4831
+ text: JSON.stringify({ success: true, message: `Alert rule ${ruleId} deleted`, result }, null, 2),
4832
+ },
4833
+ ],
4834
+ };
4835
+ }
4836
+ // ===================== Alerting: outages =====================
4837
+ case 'fcp_list_outages': {
4838
+ const result = await client.listOutages(args);
4839
+ return {
4840
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4841
+ };
4842
+ }
4843
+ case 'fcp_get_outage': {
4844
+ const { outageId } = args;
4845
+ const result = await client.getOutage(outageId);
4846
+ return {
4847
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4848
+ };
4849
+ }
4850
+ case 'fcp_create_outage': {
4851
+ const result = await client.createOutage(args);
4852
+ return {
4853
+ content: [
4854
+ {
4855
+ type: 'text',
4856
+ text: JSON.stringify({ success: true, message: 'Outage created', outage: result?.outage ?? result }, null, 2),
4857
+ },
4858
+ ],
4859
+ };
4860
+ }
4861
+ case 'fcp_update_outage': {
4862
+ const { outageId, confirm, ...updates } = args;
4863
+ // Safety gate: a notifying update must be explicitly confirmed. We refuse the
4864
+ // WHOLE update (not just the notify) so a half-applied state can't occur.
4865
+ if (updates.notifyClients === true && confirm !== true) {
4866
+ throw new Error(`Refusing to update: outage ${outageId} update has notifyClients=true, which SENDS REAL notifications ` +
4867
+ `to affected clients. Re-call with confirm=true to apply this update and notify, or drop notifyClients ` +
4868
+ `to update silently.`);
4869
+ }
4870
+ const result = await client.updateOutage(outageId, updates);
4871
+ return {
4872
+ content: [
4873
+ {
4874
+ type: 'text',
4875
+ text: JSON.stringify({ success: true, message: `Outage ${outageId} updated`, outage: result?.outage ?? result }, null, 2),
4876
+ },
4877
+ ],
4878
+ };
4879
+ }
4880
+ case 'fcp_resolve_outage': {
4881
+ const { outageId } = args;
4882
+ const result = await client.resolveOutage(outageId);
4883
+ return {
4884
+ content: [
4885
+ {
4886
+ type: 'text',
4887
+ text: JSON.stringify({ success: true, message: `Outage ${outageId} resolved`, result }, null, 2),
4888
+ },
4889
+ ],
4890
+ };
4891
+ }
4892
+ // ===================== Alerting: bulk client notifications =====================
4893
+ case 'fcp_bulk_notify_clients': {
4894
+ const { outageIds, notificationType, subject, message, channels, sentBy, confirm } = args;
4895
+ // Safety gate: refuse to email/SMS real clients unless explicitly confirmed.
4896
+ if (confirm !== true) {
4897
+ const ch = (channels && channels.length ? channels : ['email']).join(', ');
4898
+ const n = Array.isArray(outageIds) ? outageIds.length : 0;
4899
+ throw new Error(`Refusing to send: fcp_bulk_notify_clients dispatches REAL ${ch} notifications to clients ` +
4900
+ `affected by ${n} outage(s) [${Array.isArray(outageIds) ? outageIds.join(', ') : ''}]. ` +
4901
+ `Confirm with a human, then re-call with confirm=true to proceed.`);
4902
+ }
4903
+ const result = await client.bulkNotifyClients({ outageIds, notificationType, subject, message, channels, sentBy });
4904
+ return {
4905
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4906
+ };
4907
+ }
4908
+ case 'fcp_get_bulk_notify_history': {
4909
+ const { limit } = args;
4910
+ const result = await client.getBulkNotifyHistory(limit);
4911
+ return {
4912
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4913
+ };
4914
+ }
4290
4915
  default:
4291
4916
  throw new Error(`Unknown tool: ${name}`);
4292
4917
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fruition/fcp-mcp-server",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "MCP Server for FCP Launch Coordination System - enables Claude Code to interact with FCP launches and track development time",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",