@fruition/fcp-mcp-server 1.24.0 → 1.24.1

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.
package/dist/index.js CHANGED
@@ -169,28 +169,6 @@ 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',
194
172
  };
195
173
  // Resolved role for the current API key. Cached only on successful lookup so
196
174
  // transient FCP outages don't pin us to 'none' for the rest of the session.
@@ -390,97 +368,6 @@ class FCPClient {
390
368
  method: 'DELETE',
391
369
  });
392
370
  }
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
- }
484
371
  async createLaunch(input) {
485
372
  return this.fetch('/api/launches', {
486
373
  method: 'POST',
@@ -3196,344 +3083,6 @@ const TOOLS = [
3196
3083
  required: ['siteId'],
3197
3084
  },
3198
3085
  },
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
- },
3537
3086
  ];
3538
3087
  // Register tool handlers
3539
3088
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -4738,180 +4287,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4738
4287
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4739
4288
  };
4740
4289
  }
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
- }
4915
4290
  default:
4916
4291
  throw new Error(`Unknown tool: ${name}`);
4917
4292
  }
@@ -422,9 +422,18 @@ async function pullOnce(ctx, state, result) {
422
422
  if (!slug)
423
423
  continue;
424
424
  const props = node.properties ?? {};
425
- const bodyMarkdown = typeof props.bodyMarkdown === 'string' ? props.bodyMarkdown : '';
425
+ // The capture endpoint receives `bodyMarkdown` from the wire but persists
426
+ // it to kg_nodes.properties as `body_md` (see knowledge-graph.ts upsertSkill).
427
+ // The search endpoint returns properties verbatim, so the pull side reads
428
+ // `body_md`. Accept either for forward-compat in case the storage shape
429
+ // is normalized later.
430
+ const bodyMarkdown = typeof props.body_md === 'string'
431
+ ? props.body_md
432
+ : typeof props.bodyMarkdown === 'string'
433
+ ? props.bodyMarkdown
434
+ : '';
426
435
  if (!bodyMarkdown) {
427
- result.skipped.push({ slug, why: 'remote has no bodyMarkdown' });
436
+ result.skipped.push({ slug, why: 'remote has no body_md' });
428
437
  continue;
429
438
  }
430
439
  const localPath = join(ctx.skillsDir, slug, 'SKILL.md');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fruition/fcp-mcp-server",
3
- "version": "1.24.0",
3
+ "version": "1.24.1",
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",