@fruition/fcp-mcp-server 1.22.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 +707 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -165,9 +165,32 @@ const TOOL_PERMISSIONS = {
165
165
  unroo_list_tasks: 'viewer',
166
166
  unroo_get_task: 'viewer',
167
167
  unroo_list_comments: 'viewer',
168
+ unroo_list_attachments: 'viewer',
168
169
  unroo_get_my_tasks: 'viewer',
169
170
  unroo_get_parking_lot: 'viewer',
170
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',
171
194
  };
172
195
  // Resolved role for the current API key. Cached only on successful lookup so
173
196
  // transient FCP outages don't pin us to 'none' for the rest of the session.
@@ -367,6 +390,97 @@ class FCPClient {
367
390
  method: 'DELETE',
368
391
  });
369
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
+ }
370
484
  async createLaunch(input) {
371
485
  return this.fetch('/api/launches', {
372
486
  method: 'POST',
@@ -842,6 +956,18 @@ class UnrooClient {
842
956
  return this.fetch(`/api/external/fcp/tasks/${encodeURIComponent(taskId)}/comments`);
843
957
  }
844
958
  // ============================================================================
959
+ // Attachment APIs
960
+ // ============================================================================
961
+ async listAttachments(taskId, opts = {}) {
962
+ const params = new URLSearchParams();
963
+ if (opts.includeContent)
964
+ params.set('include_content', 'true');
965
+ const query = params.toString();
966
+ // Inline image fetches can be slow if the bucket has cold objects; give it
967
+ // a longer timeout than the default 15s.
968
+ return this.fetch(`/api/external/fcp/tasks/${encodeURIComponent(taskId)}/attachments${query ? `?${query}` : ''}`, {}, 60_000);
969
+ }
970
+ // ============================================================================
845
971
  // Parking Lot / Backlog APIs
846
972
  // ============================================================================
847
973
  async logFutureWork(input) {
@@ -1825,6 +1951,24 @@ const TOOLS = [
1825
1951
  required: ['task_id'],
1826
1952
  },
1827
1953
  },
1954
+ {
1955
+ name: 'unroo_list_attachments',
1956
+ description: 'List file attachments on an Unroo task. Use this to see screenshots, logs, or other files attached to a bug report or ticket. By default, image and text attachments are embedded inline so you can view them directly; pass include_content=false to get metadata only (faster for large attachments).',
1957
+ inputSchema: {
1958
+ type: 'object',
1959
+ properties: {
1960
+ task_id: {
1961
+ type: 'string',
1962
+ description: 'Task ID or Jira key (e.g., task_abc123 or FLYFRU-189)',
1963
+ },
1964
+ include_content: {
1965
+ type: 'boolean',
1966
+ description: 'When true (default), embed image and text content inline. Set to false to return metadata only.',
1967
+ },
1968
+ },
1969
+ required: ['task_id'],
1970
+ },
1971
+ },
1828
1972
  {
1829
1973
  name: 'unroo_get_my_tasks',
1830
1974
  description: 'Get tasks assigned to the current user (based on API key). Useful for finding your work items.',
@@ -3052,6 +3196,344 @@ const TOOLS = [
3052
3196
  required: ['siteId'],
3053
3197
  },
3054
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
+ },
3055
3537
  ];
3056
3538
  // Register tool handlers
3057
3539
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -3643,6 +4125,57 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3643
4125
  ],
3644
4126
  };
3645
4127
  }
4128
+ case 'unroo_list_attachments': {
4129
+ const { task_id, include_content } = args;
4130
+ const includeContent = include_content !== false; // default true
4131
+ const result = await unrooClient.listAttachments(task_id, { includeContent });
4132
+ // Summary block: metadata only, no base64 bodies (those go into image/text blocks below).
4133
+ const summary = {
4134
+ total: result.total,
4135
+ attachments: result.attachments.map((a) => ({
4136
+ id: a.id,
4137
+ file_name: a.file_name,
4138
+ file_size: a.file_size,
4139
+ mime_type: a.mime_type,
4140
+ uploaded_by: a.uploaded_by,
4141
+ created_at: a.created_at,
4142
+ file_url: a.file_url,
4143
+ jira_attachment_id: a.jira_attachment_id,
4144
+ inlined: !!a.content_base64,
4145
+ content_omitted_reason: a.content_omitted_reason,
4146
+ })),
4147
+ };
4148
+ const content = [
4149
+ { type: 'text', text: JSON.stringify(summary, null, 2) },
4150
+ ];
4151
+ if (includeContent) {
4152
+ for (const a of result.attachments) {
4153
+ if (!a.content_base64)
4154
+ continue;
4155
+ if (a.content_kind === 'image') {
4156
+ content.push({
4157
+ type: 'image',
4158
+ data: a.content_base64,
4159
+ mimeType: a.mime_type || 'application/octet-stream',
4160
+ });
4161
+ }
4162
+ else if (a.content_kind === 'text') {
4163
+ let decoded;
4164
+ try {
4165
+ decoded = Buffer.from(a.content_base64, 'base64').toString('utf8');
4166
+ }
4167
+ catch {
4168
+ decoded = '[failed to decode text content]';
4169
+ }
4170
+ content.push({
4171
+ type: 'text',
4172
+ text: `--- ${a.file_name} (${a.mime_type || 'text'}) ---\n${decoded}`,
4173
+ });
4174
+ }
4175
+ }
4176
+ }
4177
+ return { content };
4178
+ }
3646
4179
  case 'unroo_get_my_tasks': {
3647
4180
  const { status, limit } = args;
3648
4181
  // Note: The API key determines the user, tasks are filtered server-side
@@ -4205,6 +4738,180 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4205
4738
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4206
4739
  };
4207
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
+ }
4208
4915
  default:
4209
4916
  throw new Error(`Unknown tool: ${name}`);
4210
4917
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fruition/fcp-mcp-server",
3
- "version": "1.22.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",