@fruition/fcp-mcp-server 1.19.0 → 1.21.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.
package/dist/index.js DELETED
@@ -1,4262 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * FCP MCP Server
4
- *
5
- * Provides Claude Code with direct access to FCP Launch Coordination System:
6
- * - Query launches, checklists, and legacy access info
7
- * - Update checklist item status
8
- * - Get project context for migrations
9
- */
10
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
- import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
13
- import { readFileSync } from 'fs';
14
- import { fileURLToPath } from 'url';
15
- import { dirname, join } from 'path';
16
- // Get version from package.json
17
- const __filename = fileURLToPath(import.meta.url);
18
- const __dirname = dirname(__filename);
19
- const packageJsonPath = join(__dirname, '..', 'package.json');
20
- let MCP_SERVER_VERSION = '1.0.0'; // fallback
21
- try {
22
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
23
- MCP_SERVER_VERSION = packageJson.version;
24
- }
25
- catch {
26
- // Try one level up (for dist folder)
27
- try {
28
- const altPath = join(__dirname, '..', '..', 'package.json');
29
- const packageJson = JSON.parse(readFileSync(altPath, 'utf-8'));
30
- MCP_SERVER_VERSION = packageJson.version;
31
- }
32
- catch {
33
- console.error('[MCP Server] Warning: Could not read package.json version');
34
- }
35
- }
36
- import { execSync } from 'child_process';
37
- import { runSkillsCli } from './skills-sync-cli.js';
38
- import { runBackgroundSync } from './skills-sync.js';
39
- // Configuration
40
- const FCP_API_URL = process.env.FCP_API_URL || 'https://fcp.fru.io';
41
- const FCP_API_TOKEN = process.env.FCP_API_TOKEN || '';
42
- // Unroo API can be called directly (legacy) or proxied through FCP (recommended)
43
- // When UNROO_API_KEY is not set, Unroo calls go through FCP's proxy at /api/mcp/unroo/*
44
- const UNROO_API_URL = process.env.UNROO_API_URL || 'https://app.unroo.io';
45
- const UNROO_API_KEY = process.env.UNROO_API_KEY || '';
46
- // User identity for Unroo attribution
47
- // Without this, all actions are attributed to whoever created the FCP API key
48
- // Falls back to git config user.email for zero-config setup
49
- function detectUserEmail() {
50
- if (process.env.FCP_USER_EMAIL)
51
- return process.env.FCP_USER_EMAIL;
52
- try {
53
- const email = execSync('git config user.email', {
54
- encoding: 'utf-8',
55
- timeout: 3000,
56
- stdio: ['pipe', 'pipe', 'ignore'],
57
- }).trim();
58
- if (email) {
59
- console.error(`[MCP Server] Auto-detected user email: ${email}`);
60
- return email;
61
- }
62
- }
63
- catch {
64
- // Not in a git repo or no email configured
65
- }
66
- console.error('[MCP Server] WARNING: FCP_USER_EMAIL not set and git email not detected. Unroo task updates will be attributed to the FCP API key owner, not you. Set FCP_USER_EMAIL in your MCP server env config.');
67
- return '';
68
- }
69
- const FCP_USER_EMAIL = detectUserEmail();
70
- // If no Unroo key, use FCP as proxy (unified key setup)
71
- const USE_FCP_UNROO_PROXY = !UNROO_API_KEY;
72
- // Helper to check if Unroo functionality is available (either mode)
73
- const UNROO_AVAILABLE = UNROO_API_KEY || (USE_FCP_UNROO_PROXY && FCP_API_TOKEN);
74
- // Unique instance ID for this MCP server process
75
- // Combines hostname + PID + startup timestamp to ensure uniqueness across:
76
- // - Multiple machines (hostname)
77
- // - Multiple processes on same machine (PID)
78
- // - Process restarts (timestamp)
79
- const INSTANCE_ID = `${process.env.HOSTNAME || 'local'}-${process.pid}-${Date.now()}`;
80
- const ROLE_HIERARCHY = [
81
- 'super_admin',
82
- 'admin',
83
- 'billing_admin',
84
- 'operator',
85
- 'viewer',
86
- 'none',
87
- ];
88
- const TOOL_PERMISSIONS = {
89
- // --- super_admin only: destructive, irreversible, or move raw prod data ---
90
- fcp_create_site: 'super_admin',
91
- fcp_delete_site: 'super_admin',
92
- fcp_delete_launch: 'super_admin',
93
- fcp_clone_to_staging: 'super_admin',
94
- fcp_clone_confirm: 'super_admin',
95
- fcp_shield_remove_domain: 'super_admin',
96
- fcp_backup_delete_pairing: 'super_admin',
97
- fcp_filesync_cancel_sync: 'super_admin',
98
- // --- admin+: mutating ops with real-world side effects ---
99
- fcp_create_launch: 'admin',
100
- fcp_update_launch: 'admin',
101
- fcp_update_site: 'admin',
102
- fcp_delete_checklist_item: 'admin',
103
- fcp_shield_add_domain: 'admin',
104
- fcp_shield_update_domain: 'admin',
105
- fcp_backup_enable: 'admin',
106
- fcp_backup_trigger: 'admin',
107
- fcp_backup_check_trigger: 'admin',
108
- fcp_backup_update_pairing: 'admin',
109
- fcp_backup_download: 'admin',
110
- fcp_backup_download_prepared: 'admin',
111
- fcp_kinsta_backup_download: 'admin',
112
- fcp_filesync_start_sync: 'admin',
113
- fcp_filesync_get_confirmation: 'admin',
114
- fcp_trigger_nuclei_scan: 'admin',
115
- fcp_scan_security_headers: 'admin',
116
- // --- operator+: routine writes (checklists, progress notes, Unroo tasks) ---
117
- fcp_add_checklist_item: 'operator',
118
- fcp_update_checklist_item: 'operator',
119
- fcp_validate_checklist_item: 'operator',
120
- fcp_add_progress_note: 'operator',
121
- unroo_create_task: 'operator',
122
- unroo_update_task: 'operator',
123
- unroo_add_comment: 'operator',
124
- unroo_create_follow_up: 'operator',
125
- unroo_log_future_work: 'operator',
126
- unroo_convert_to_backlog: 'operator',
127
- unroo_start_session: 'operator',
128
- unroo_end_session: 'operator',
129
- // --- viewer (read-only) — explicit for clarity; any unmapped tool also defaults here ---
130
- fcp_list_launches: 'viewer',
131
- fcp_get_launch: 'viewer',
132
- fcp_get_legacy_access: 'viewer',
133
- fcp_get_checklist: 'viewer',
134
- fcp_get_claude_md: 'viewer',
135
- fcp_get_success_factors: 'viewer',
136
- fcp_get_unroo_section: 'viewer',
137
- fcp_filesync_list_configs: 'viewer',
138
- fcp_filesync_get_config: 'viewer',
139
- fcp_filesync_get_job_status: 'viewer',
140
- fcp_get_nuclei_results: 'viewer',
141
- fcp_get_security_headers_results: 'viewer',
142
- fcp_list_sites: 'viewer',
143
- fcp_search_sites: 'viewer',
144
- fcp_get_site: 'viewer',
145
- fcp_get_local_setup_guide: 'viewer',
146
- fcp_shield_list_domains: 'viewer',
147
- fcp_shield_get_domain: 'viewer',
148
- fcp_shield_get_metrics: 'viewer',
149
- fcp_backup_list_sites: 'viewer',
150
- fcp_backup_get_config: 'viewer',
151
- fcp_backup_list_eligible: 'viewer',
152
- fcp_backup_list_backups: 'viewer',
153
- fcp_backup_get_status: 'viewer',
154
- fcp_backup_sanitize_status: 'viewer',
155
- fcp_backup_list_pairings: 'viewer',
156
- fcp_kinsta_backup_list_sites: 'viewer',
157
- fcp_kinsta_backup_list: 'viewer',
158
- fcp_clone_status: 'viewer',
159
- fcp_clone_list: 'viewer',
160
- fcp_get_dev_environment_info: 'viewer',
161
- unroo_list_projects: 'viewer',
162
- unroo_list_tasks: 'viewer',
163
- unroo_get_task: 'viewer',
164
- unroo_list_comments: 'viewer',
165
- unroo_get_my_tasks: 'viewer',
166
- unroo_get_parking_lot: 'viewer',
167
- unroo_get_backlog: 'viewer',
168
- };
169
- // Resolved role for the current API key. Cached only on successful lookup so
170
- // transient FCP outages don't pin us to 'none' for the rest of the session.
171
- let cachedUserRole;
172
- async function fetchUserRole() {
173
- if (cachedUserRole !== undefined)
174
- return cachedUserRole;
175
- // dev_bypass is the local-dev token; treat as super_admin to avoid
176
- // gating local development against role lookup.
177
- if (FCP_API_TOKEN === 'dev_bypass') {
178
- cachedUserRole = 'super_admin';
179
- return cachedUserRole;
180
- }
181
- if (!FCP_API_TOKEN) {
182
- return 'none';
183
- }
184
- try {
185
- const res = await fetch(`${FCP_API_URL}/api/mcp/me`, {
186
- headers: { 'X-API-Key': FCP_API_TOKEN },
187
- signal: AbortSignal.timeout(10_000),
188
- });
189
- if (!res.ok) {
190
- console.error(`[MCP Server] Role lookup failed: HTTP ${res.status}`);
191
- return 'none';
192
- }
193
- const data = await res.json();
194
- const role = data?.role ?? 'none';
195
- cachedUserRole = role;
196
- console.error(`[MCP Server] Resolved user role: ${role} (${data?.email ?? 'unknown'})`);
197
- return role;
198
- }
199
- catch (err) {
200
- console.error('[MCP Server] Role lookup error:', err);
201
- return 'none';
202
- }
203
- }
204
- function isRoleAtLeast(actual, required) {
205
- if (actual === 'none')
206
- return required === 'none';
207
- const a = ROLE_HIERARCHY.indexOf(actual);
208
- const r = ROLE_HIERARCHY.indexOf(required);
209
- if (a === -1 || r === -1)
210
- return false;
211
- return a <= r;
212
- }
213
- async function enforceToolPermission(toolName) {
214
- const required = TOOL_PERMISSIONS[toolName] ?? 'viewer';
215
- const actual = await fetchUserRole();
216
- if (!isRoleAtLeast(actual, required)) {
217
- throw new Error(`Permission denied: tool '${toolName}' requires ${required} role; ` +
218
- `your role is '${actual}'. Contact a super_admin (Brad, Mattox, or Andrea) ` +
219
- `if you need access.`);
220
- }
221
- }
222
- let currentProject = null;
223
- /**
224
- * Detect the current git remote URL
225
- */
226
- function detectGitRemote() {
227
- try {
228
- const remote = execSync('git config --get remote.origin.url', {
229
- encoding: 'utf-8',
230
- timeout: 5000,
231
- stdio: ['pipe', 'pipe', 'ignore'],
232
- }).trim();
233
- return remote || null;
234
- }
235
- catch {
236
- // Not a git repo or no remote configured
237
- return null;
238
- }
239
- }
240
- /**
241
- * Resolve project from git remote URL via FCP API
242
- */
243
- async function resolveProjectFromRepo(repoUrl) {
244
- if (!FCP_API_URL) {
245
- return null;
246
- }
247
- try {
248
- const url = `${FCP_API_URL}/api/mcp/resolve-project?repo_url=${encodeURIComponent(repoUrl)}`;
249
- const headers = {
250
- 'Content-Type': 'application/json',
251
- };
252
- if (FCP_API_TOKEN && FCP_API_TOKEN !== 'dev_bypass') {
253
- headers['X-API-Key'] = FCP_API_TOKEN;
254
- }
255
- else if (FCP_API_TOKEN === 'dev_bypass') {
256
- headers['X-Dev-Bypass'] = 'true';
257
- }
258
- const response = await fetch(url, { headers });
259
- if (!response.ok) {
260
- return null;
261
- }
262
- const data = await response.json();
263
- if (data.found && data.project) {
264
- return {
265
- project_key: data.project.project_key,
266
- website_id: data.project.website_id,
267
- domain: data.project.domain,
268
- account_name: data.project.account_name || 'Unknown',
269
- github: data.project.github,
270
- };
271
- }
272
- return null;
273
- }
274
- catch (error) {
275
- console.error('[MCP Server] Error resolving project:', error);
276
- return null;
277
- }
278
- }
279
- /**
280
- * Initialize project detection on startup
281
- */
282
- async function initializeProjectDetection() {
283
- const remoteUrl = detectGitRemote();
284
- if (!remoteUrl) {
285
- console.error('[MCP Server] No git remote detected - project will be tracked as general');
286
- return;
287
- }
288
- console.error(`[MCP Server] Detected git remote: ${remoteUrl}`);
289
- const project = await resolveProjectFromRepo(remoteUrl);
290
- if (project) {
291
- currentProject = project;
292
- console.error(`[MCP Server] Resolved project: ${project.domain} (${project.project_key})`);
293
- }
294
- else {
295
- console.error('[MCP Server] Could not resolve project from repo - check FCP website github config');
296
- }
297
- }
298
- // API Client
299
- class FCPClient {
300
- baseUrl;
301
- token;
302
- defaultTimeout = 15000; // 15 seconds
303
- constructor(baseUrl, token) {
304
- this.baseUrl = baseUrl;
305
- this.token = token;
306
- }
307
- async fetch(path, options = {}, timeoutMs) {
308
- const url = `${this.baseUrl}${path}`;
309
- const headers = {
310
- 'Content-Type': 'application/json',
311
- ...(options.headers || {}),
312
- };
313
- if (this.token) {
314
- // Support dev bypass mode for local testing
315
- if (this.token === 'dev_bypass') {
316
- headers['X-Dev-Bypass'] = 'true';
317
- }
318
- else {
319
- // Production mode: use X-API-Key header
320
- headers['X-API-Key'] = this.token;
321
- }
322
- }
323
- const response = await fetch(url, {
324
- ...options,
325
- headers,
326
- signal: AbortSignal.timeout(timeoutMs || this.defaultTimeout),
327
- });
328
- if (!response.ok) {
329
- const error = await response.text();
330
- throw new Error(`FCP API error (${response.status}): ${error}`);
331
- }
332
- return response.json();
333
- }
334
- async listLaunches(filters) {
335
- const params = new URLSearchParams();
336
- if (filters?.status)
337
- params.set('status', filters.status);
338
- if (filters?.platform)
339
- params.set('platform', filters.platform);
340
- if (filters?.upcoming)
341
- params.set('upcoming', filters.upcoming.toString());
342
- if (filters?.limit)
343
- params.set('limit', filters.limit.toString());
344
- const query = params.toString();
345
- return this.fetch(`/api/launches${query ? `?${query}` : ''}`);
346
- }
347
- async getLaunch(id) {
348
- return this.fetch(`/api/launches/${id}`);
349
- }
350
- async createChecklistItem(launchId, input) {
351
- return this.fetch(`/api/launches/${launchId}/checklist`, {
352
- method: 'POST',
353
- body: JSON.stringify(input),
354
- });
355
- }
356
- async updateChecklistItem(launchId, itemId, updates) {
357
- return this.fetch(`/api/launches/${launchId}/checklist/${itemId}`, {
358
- method: 'PUT',
359
- body: JSON.stringify(updates),
360
- });
361
- }
362
- async deleteChecklistItem(launchId, itemId) {
363
- return this.fetch(`/api/launches/${launchId}/checklist/${itemId}`, {
364
- method: 'DELETE',
365
- });
366
- }
367
- async createLaunch(input) {
368
- return this.fetch('/api/launches', {
369
- method: 'POST',
370
- body: JSON.stringify(input),
371
- });
372
- }
373
- async updateLaunch(id, updates) {
374
- return this.fetch(`/api/launches/${id}`, {
375
- method: 'PUT',
376
- body: JSON.stringify(updates),
377
- });
378
- }
379
- async deleteLaunch(id) {
380
- return this.fetch(`/api/launches/${id}`, {
381
- method: 'DELETE',
382
- });
383
- }
384
- async getSuccessFactors(launchId, itemId) {
385
- return this.fetch(`/api/launches/${launchId}/checklist/${itemId}/success-factors`);
386
- }
387
- async validateChecklistItem(launchId, itemId) {
388
- return this.fetch(`/api/launches/${launchId}/checklist/${itemId}/validate`, {
389
- method: 'POST',
390
- });
391
- }
392
- async getClaudeMd(launchId) {
393
- return this.fetch(`/api/launches/${launchId}/claude-md`);
394
- }
395
- async addNote(launchId, content) {
396
- return this.fetch(`/api/launches/${launchId}/notes`, {
397
- method: 'POST',
398
- body: JSON.stringify({ content }),
399
- });
400
- }
401
- // FileSync methods
402
- async listFileSyncConfigs(filters) {
403
- const params = new URLSearchParams();
404
- if (filters?.enabled !== undefined)
405
- params.set('enabled', String(filters.enabled));
406
- if (filters?.sync_direction)
407
- params.set('sync_direction', filters.sync_direction);
408
- const query = params.toString();
409
- return this.fetch(`/api/filesync/configs${query ? `?${query}` : ''}`);
410
- }
411
- async getFileSyncJobs(filters) {
412
- const params = new URLSearchParams();
413
- if (filters?.config_id)
414
- params.set('config_id', String(filters.config_id));
415
- if (filters?.status)
416
- params.set('status', filters.status);
417
- if (filters?.limit)
418
- params.set('limit', String(filters.limit));
419
- const query = params.toString();
420
- return this.fetch(`/api/filesync/jobs${query ? `?${query}` : ''}`);
421
- }
422
- async startFileSync(input) {
423
- return this.fetch('/api/filesync/start', {
424
- method: 'POST',
425
- body: JSON.stringify(input),
426
- });
427
- }
428
- async cancelFileSync(jobId) {
429
- return this.fetch(`/api/filesync/cancel/${encodeURIComponent(jobId)}`, {
430
- method: 'POST',
431
- });
432
- }
433
- async getFileSyncConfirmation(configId) {
434
- return this.fetch('/api/filesync/confirm', {
435
- method: 'POST',
436
- body: JSON.stringify({ config_id: configId }),
437
- });
438
- }
439
- async triggerNucleiScan(websiteId, options) {
440
- return this.fetch(`/api/sites/${websiteId}/nuclei-scan`, {
441
- method: 'POST',
442
- body: JSON.stringify(options || {}),
443
- }, 30000);
444
- }
445
- async getNucleiResults(websiteId, options) {
446
- // If scan_id provided, get detailed results for that specific scan
447
- if (options?.scan_id) {
448
- return this.fetch(`/api/sites/${websiteId}/nuclei-scans/${options.scan_id}`);
449
- }
450
- // Otherwise, get scan history list
451
- return this.fetch(`/api/sites/${websiteId}/nuclei-scans`);
452
- }
453
- // Security Headers Methods
454
- async scanSecurityHeaders(websiteId) {
455
- return this.fetch(`/api/sites/${websiteId}/security-headers`, {
456
- method: 'POST',
457
- body: JSON.stringify({}),
458
- }, 30000);
459
- }
460
- async getSecurityHeadersResults(websiteId, scanId) {
461
- if (scanId) {
462
- return this.fetch(`/api/sites/${websiteId}/security-headers/${scanId}`);
463
- }
464
- return this.fetch(`/api/sites/${websiteId}/security-headers/latest`);
465
- }
466
- // Site/Website CRUD Methods
467
- async listSites(filters) {
468
- const params = new URLSearchParams();
469
- if (filters?.account_id)
470
- params.set('account_id', filters.account_id.toString());
471
- if (filters?.cms)
472
- params.set('cms', filters.cms);
473
- if (filters?.environment)
474
- params.set('environment', filters.environment);
475
- if (filters?.fru_hosted !== undefined)
476
- params.set('fru_hosted', String(filters.fru_hosted));
477
- if (filters?.retired)
478
- params.set('retired', filters.retired);
479
- if (filters?.limit)
480
- params.set('limit', filters.limit.toString());
481
- if (filters?.offset)
482
- params.set('offset', filters.offset.toString());
483
- const query = params.toString();
484
- return this.fetch(`/api/sites${query ? `?${query}` : ''}`);
485
- }
486
- async searchSites(query, limit) {
487
- const params = new URLSearchParams({ q: query });
488
- if (limit)
489
- params.set('limit', limit.toString());
490
- return this.fetch(`/api/sites/search?${params.toString()}`);
491
- }
492
- async getSite(siteId) {
493
- return this.fetch(`/api/sites/${siteId}`);
494
- }
495
- async createSite(production, staging) {
496
- return this.fetch('/api/projects/websites/create-with-staging', {
497
- method: 'POST',
498
- body: JSON.stringify({ production, staging: staging || [] }),
499
- });
500
- }
501
- async updateSite(siteId, updates) {
502
- return this.fetch(`/api/sites/${siteId}`, {
503
- method: 'PUT',
504
- body: JSON.stringify(updates),
505
- });
506
- }
507
- async deleteSite(siteId, options) {
508
- return this.fetch(`/api/sites/${siteId}`, {
509
- method: 'DELETE',
510
- body: JSON.stringify(options || {}),
511
- });
512
- }
513
- // Local Setup Guide
514
- async getLocalSetupGuide(siteId) {
515
- return this.fetch(`/api/sites/${siteId}/local-setup`);
516
- }
517
- // Shield Domain Management
518
- async shieldListDomains(filters) {
519
- const params = new URLSearchParams();
520
- if (filters?.status)
521
- params.append('status', filters.status);
522
- if (filters?.account_id)
523
- params.append('account_id', String(filters.account_id));
524
- const qs = params.toString();
525
- return this.fetch(`/api/shield/domains${qs ? `?${qs}` : ''}`);
526
- }
527
- async shieldAddDomain(input) {
528
- return this.fetch('/api/shield/domains', {
529
- method: 'POST',
530
- body: JSON.stringify(input),
531
- });
532
- }
533
- async shieldGetDomain(domainId) {
534
- return this.fetch(`/api/shield/domains/${domainId}`);
535
- }
536
- async shieldUpdateDomain(domainId, updates) {
537
- return this.fetch(`/api/shield/domains/${domainId}`, {
538
- method: 'PUT',
539
- body: JSON.stringify(updates),
540
- });
541
- }
542
- async shieldRemoveDomain(domainId) {
543
- return this.fetch(`/api/shield/domains/${domainId}`, {
544
- method: 'DELETE',
545
- });
546
- }
547
- async shieldGetMetrics() {
548
- return this.fetch('/api/shield/metrics?include_health=true');
549
- }
550
- async shieldVerifyDomain(domainId) {
551
- return this.fetch(`/api/shield/domains/${domainId}/verify`, {
552
- method: 'POST',
553
- });
554
- }
555
- // ============================================================================
556
- // Backup Management Methods
557
- // ============================================================================
558
- async backupListSites() {
559
- return this.fetch('/api/backup/sites');
560
- }
561
- async backupGetConfig() {
562
- return this.fetch('/api/backup/config');
563
- }
564
- async backupListEligible() {
565
- return this.fetch('/api/backup/eligible');
566
- }
567
- async backupEnable(siteId) {
568
- return this.fetch('/api/backup/enable', {
569
- method: 'POST',
570
- body: JSON.stringify({ siteId }),
571
- });
572
- }
573
- async backupTrigger(websiteId, triggerType) {
574
- return this.fetch('/api/backup/trigger', {
575
- method: 'POST',
576
- body: JSON.stringify({ websiteId, triggerType }),
577
- });
578
- }
579
- async backupCheckTrigger(websiteId) {
580
- return this.fetch(`/api/backup/trigger?websiteId=${websiteId}`);
581
- }
582
- async backupListBackups(siteId) {
583
- return this.fetch(`/api/backup/list?siteId=${encodeURIComponent(siteId)}`);
584
- }
585
- async backupGetStatus(options) {
586
- const params = new URLSearchParams();
587
- if (options.backupId)
588
- params.set('backupId', options.backupId);
589
- if (options.websiteId)
590
- params.set('websiteId', options.websiteId.toString());
591
- const query = params.toString();
592
- return this.fetch(`/api/backup/status${query ? `?${query}` : ''}`);
593
- }
594
- async backupDownload(backupId) {
595
- return this.fetch(`/api/backup/download?backupId=${encodeURIComponent(backupId)}`);
596
- }
597
- async backupDownloadPrepared(input) {
598
- return this.fetch('/api/backup/download', {
599
- method: 'POST',
600
- body: JSON.stringify(input),
601
- });
602
- }
603
- async backupSanitizeStatus(jobId) {
604
- return this.fetch(`/api/backup/download/status?jobId=${encodeURIComponent(jobId)}`);
605
- }
606
- async backupListPairings(siteId) {
607
- const params = new URLSearchParams();
608
- if (siteId)
609
- params.set('siteId', siteId.toString());
610
- const query = params.toString();
611
- return this.fetch(`/api/backup/pairing${query ? `?${query}` : ''}`);
612
- }
613
- async backupUpdatePairing(siteId, pairingConfig) {
614
- return this.fetch('/api/backup/pairing', {
615
- method: 'POST',
616
- body: JSON.stringify({ siteId, pairingConfig }),
617
- });
618
- }
619
- async backupDeletePairing(siteId) {
620
- return this.fetch(`/api/backup/pairing?siteId=${siteId}`, {
621
- method: 'DELETE',
622
- });
623
- }
624
- // Kinsta backup methods (separate S3 bucket: kinsta-backups-fru)
625
- async kinstaBackupListSites() {
626
- return this.fetch('/api/backup/kinsta?action=sites');
627
- }
628
- async kinstaBackupListBackups(siteId) {
629
- return this.fetch(`/api/backup/kinsta?siteId=${siteId}`);
630
- }
631
- async kinstaBackupDownload(s3Key) {
632
- return this.fetch('/api/backup/kinsta', {
633
- method: 'POST',
634
- body: JSON.stringify({ s3Key }),
635
- });
636
- }
637
- // Clone to Staging
638
- async cloneToStagingConfirm(productionSiteId, stagingSiteId) {
639
- return this.fetch('/api/clone-staging/confirm', {
640
- method: 'POST',
641
- body: JSON.stringify({ productionSiteId, stagingSiteId }),
642
- });
643
- }
644
- async cloneToStaging(params) {
645
- return this.fetch('/api/clone-staging', {
646
- method: 'POST',
647
- body: JSON.stringify(params),
648
- });
649
- }
650
- async getCloneStatus(cloneId) {
651
- return this.fetch(`/api/clone-staging/${encodeURIComponent(cloneId)}`);
652
- }
653
- async listCloneOperations(params) {
654
- const query = new URLSearchParams();
655
- if (params?.productionSiteId)
656
- query.set('productionSiteId', String(params.productionSiteId));
657
- if (params?.stagingSiteId)
658
- query.set('stagingSiteId', String(params.stagingSiteId));
659
- if (params?.limit)
660
- query.set('limit', String(params.limit));
661
- return this.fetch(`/api/clone-staging?${query.toString()}`);
662
- }
663
- async getDevEnvironmentInfo(siteId) {
664
- return this.fetch(`/api/sites/${siteId}/dev-info`);
665
- }
666
- }
667
- // Unroo API Client
668
- // Supports two modes:
669
- // 1. Direct: Uses UNROO_API_KEY to call Unroo directly (legacy)
670
- // 2. Proxy: Routes through FCP at /api/mcp/unroo/* using FCP_API_TOKEN (recommended)
671
- class UnrooClient {
672
- baseUrl;
673
- apiKey;
674
- useProxy;
675
- fcpUrl;
676
- fcpToken;
677
- defaultTimeout = 15000; // 15 seconds
678
- constructor(baseUrl, apiKey, useProxy = false, fcpUrl = '', fcpToken = '') {
679
- this.baseUrl = baseUrl;
680
- this.apiKey = apiKey;
681
- this.useProxy = useProxy;
682
- this.fcpUrl = fcpUrl;
683
- this.fcpToken = fcpToken;
684
- if (this.useProxy) {
685
- console.error('[UnrooClient] Using FCP proxy mode (unified key)');
686
- }
687
- else {
688
- console.error('[UnrooClient] Using direct Unroo API mode');
689
- }
690
- }
691
- async fetch(path, options = {}, timeoutMs) {
692
- let url;
693
- const headers = {
694
- 'Content-Type': 'application/json',
695
- ...(options.headers || {}),
696
- };
697
- if (this.useProxy) {
698
- // Route through FCP proxy: /api/external/fcp/X -> /api/mcp/unroo/X
699
- const proxyPath = path.replace('/api/external/fcp/', '/api/mcp/unroo/');
700
- url = `${this.fcpUrl}${proxyPath}`;
701
- // Use FCP API token
702
- if (this.fcpToken && this.fcpToken !== 'dev_bypass') {
703
- headers['X-API-Key'] = this.fcpToken;
704
- }
705
- else if (this.fcpToken === 'dev_bypass') {
706
- headers['X-Dev-Bypass'] = 'true';
707
- }
708
- // Pass actual user identity so Unroo attributes actions correctly
709
- // (otherwise all actions show as the FCP API key creator)
710
- if (FCP_USER_EMAIL) {
711
- headers['X-Acting-User-Email'] = FCP_USER_EMAIL;
712
- }
713
- }
714
- else {
715
- // Direct Unroo API call
716
- url = `${this.baseUrl}${path}`;
717
- headers['X-API-Key'] = this.apiKey;
718
- // Pass user identity so Unroo attributes actions to the actual user
719
- // (otherwise all actions show as the API key owner)
720
- if (FCP_USER_EMAIL) {
721
- headers['X-FCP-User-Email'] = FCP_USER_EMAIL;
722
- }
723
- }
724
- const response = await fetch(url, {
725
- ...options,
726
- headers,
727
- signal: AbortSignal.timeout(timeoutMs || this.defaultTimeout),
728
- });
729
- if (!response.ok) {
730
- const error = await response.text();
731
- const source = this.useProxy ? 'FCP Proxy' : 'Unroo API';
732
- throw new Error(`${source} error (${response.status}): ${error}`);
733
- }
734
- return response.json();
735
- }
736
- async listProjects() {
737
- return this.fetch('/api/external/fcp/projects');
738
- }
739
- async listTasks(filters) {
740
- const params = new URLSearchParams();
741
- if (filters.project_key)
742
- params.set('project_key', filters.project_key);
743
- if (filters.status)
744
- params.set('status', filters.status);
745
- if (filters.assignee_email)
746
- params.set('assignee_email', filters.assignee_email);
747
- if (filters.external_source_type)
748
- params.set('external_source_type', filters.external_source_type);
749
- if (filters.limit)
750
- params.set('limit', filters.limit.toString());
751
- const query = params.toString();
752
- return this.fetch(`/api/external/fcp/tasks${query ? `?${query}` : ''}`);
753
- }
754
- async createTask(task) {
755
- return this.fetch('/api/external/fcp/tasks', {
756
- method: 'POST',
757
- body: JSON.stringify(task),
758
- });
759
- }
760
- async getTask(taskId) {
761
- return this.fetch(`/api/external/fcp/tasks/${encodeURIComponent(taskId)}`);
762
- }
763
- async updateTask(taskId, updates) {
764
- return this.fetch(`/api/external/fcp/tasks/${encodeURIComponent(taskId)}`, {
765
- method: 'PUT',
766
- body: JSON.stringify(updates),
767
- });
768
- }
769
- async startSession(input) {
770
- return this.fetch('/api/external/fcp/sessions', {
771
- method: 'POST',
772
- body: JSON.stringify({ action: 'start', ...input }),
773
- });
774
- }
775
- async endSession(input) {
776
- return this.fetch('/api/external/fcp/sessions', {
777
- method: 'POST',
778
- body: JSON.stringify({ action: 'end', ...input }),
779
- });
780
- }
781
- async sessionHeartbeat(input) {
782
- return this.fetch('/api/external/fcp/sessions', {
783
- method: 'POST',
784
- body: JSON.stringify({ action: 'heartbeat', ...input }),
785
- });
786
- }
787
- async logActivity(taskId, input) {
788
- return this.fetch(`/api/external/fcp/tasks/${encodeURIComponent(taskId)}/activity`, {
789
- method: 'POST',
790
- body: JSON.stringify({
791
- activity_type: input.activity_type,
792
- source: input.source || 'claude_code',
793
- field_changed: input.field_changed,
794
- old_value: input.old_value,
795
- new_value: input.new_value,
796
- metadata: input.metadata,
797
- }),
798
- });
799
- }
800
- // ============================================================================
801
- // Comment APIs
802
- // ============================================================================
803
- async addComment(taskId, input) {
804
- return this.fetch(`/api/external/fcp/tasks/${encodeURIComponent(taskId)}/comments`, {
805
- method: 'POST',
806
- body: JSON.stringify(input),
807
- });
808
- }
809
- async listComments(taskId) {
810
- return this.fetch(`/api/external/fcp/tasks/${encodeURIComponent(taskId)}/comments`);
811
- }
812
- // ============================================================================
813
- // Parking Lot / Backlog APIs
814
- // ============================================================================
815
- async logFutureWork(input) {
816
- return this.fetch('/api/external/fcp/future-work', {
817
- method: 'POST',
818
- body: JSON.stringify(input),
819
- });
820
- }
821
- async getFutureWork(filters) {
822
- const params = new URLSearchParams();
823
- if (filters.project_key)
824
- params.set('project_key', filters.project_key);
825
- if (filters.account_id)
826
- params.set('account_id', filters.account_id);
827
- if (filters.status)
828
- params.set('status', filters.status);
829
- if (filters.destination)
830
- params.set('destination', filters.destination);
831
- if (filters.limit)
832
- params.set('limit', filters.limit.toString());
833
- if (filters.offset)
834
- params.set('offset', filters.offset.toString());
835
- const query = params.toString();
836
- return this.fetch(`/api/external/fcp/future-work${query ? `?${query}` : ''}`);
837
- }
838
- async updateFutureWork(id, updates) {
839
- return this.fetch(`/api/external/fcp/future-work/${encodeURIComponent(id)}`, {
840
- method: 'PUT',
841
- body: JSON.stringify(updates),
842
- });
843
- }
844
- async getBacklog(filters) {
845
- const params = new URLSearchParams();
846
- if (filters.project_key)
847
- params.set('project_key', filters.project_key);
848
- if (filters.priority)
849
- params.set('priority', filters.priority);
850
- if (filters.limit)
851
- params.set('limit', filters.limit.toString());
852
- if (filters.offset)
853
- params.set('offset', filters.offset.toString());
854
- const query = params.toString();
855
- return this.fetch(`/api/external/fcp/backlog${query ? `?${query}` : ''}`);
856
- }
857
- }
858
- // ============================================================================
859
- // Auto Session Tracking
860
- // ============================================================================
861
- class SessionTracker {
862
- unrooClient;
863
- toolCallCount = 0;
864
- sessionActive = false;
865
- lastHeartbeat = null;
866
- activities = [];
867
- heartbeatInterval = null;
868
- currentTaskId = null;
869
- consecutiveHeartbeatFailures = 0;
870
- static MAX_HEARTBEAT_FAILURES = 3;
871
- constructor(unrooClient) {
872
- this.unrooClient = unrooClient;
873
- }
874
- /**
875
- * Track a tool call - auto-starts session if needed
876
- */
877
- async trackToolCall(toolName, description) {
878
- this.toolCallCount++;
879
- this.activities.push({
880
- timestamp: new Date().toISOString(),
881
- tool: toolName,
882
- description: description || toolName,
883
- });
884
- // Keep last 50 activities
885
- if (this.activities.length > 50) {
886
- this.activities = this.activities.slice(-50);
887
- }
888
- // Auto-start session on first tool call
889
- // Session tracking works with either direct Unroo key OR FCP proxy mode
890
- if (!this.sessionActive && UNROO_AVAILABLE) {
891
- await this.startSession();
892
- }
893
- // Send heartbeat every 30 tool calls or every 5 minutes
894
- const shouldHeartbeat = this.toolCallCount % 30 === 0 ||
895
- (this.lastHeartbeat && Date.now() - this.lastHeartbeat.getTime() > 5 * 60 * 1000);
896
- if (shouldHeartbeat && this.sessionActive) {
897
- await this.sendHeartbeat();
898
- }
899
- }
900
- /**
901
- * Set the current task being worked on
902
- */
903
- setCurrentTask(taskId) {
904
- this.currentTaskId = taskId;
905
- }
906
- /**
907
- * Start a new session
908
- */
909
- async startSession() {
910
- if (!UNROO_AVAILABLE) {
911
- return;
912
- }
913
- try {
914
- // Use auto-detected project if available
915
- const sessionInput = {
916
- task_id: this.currentTaskId || undefined,
917
- source: 'claude-code-mcp',
918
- machine_id: INSTANCE_ID, // Unique per MCP server process to prevent session collision
919
- };
920
- // Add auto-detected project info
921
- if (currentProject) {
922
- sessionInput.project_key = currentProject.project_key;
923
- sessionInput.repo_name = `${currentProject.github.owner}/${currentProject.github.repo}`;
924
- }
925
- await this.unrooClient.startSession(sessionInput);
926
- this.sessionActive = true;
927
- this.lastHeartbeat = new Date();
928
- // Set up periodic heartbeat (every 5 minutes)
929
- this.heartbeatInterval = setInterval(() => {
930
- this.sendHeartbeat().catch((err) => {
931
- this.consecutiveHeartbeatFailures++;
932
- if (this.consecutiveHeartbeatFailures >= SessionTracker.MAX_HEARTBEAT_FAILURES) {
933
- console.error(`[SessionTracker] Heartbeat failed ${this.consecutiveHeartbeatFailures}x - session tracking may not work`);
934
- }
935
- });
936
- }, 5 * 60 * 1000);
937
- if (currentProject) {
938
- console.error(`[SessionTracker] Session started for project: ${currentProject.domain}`);
939
- }
940
- else {
941
- console.error('[SessionTracker] Session started (no project detected)');
942
- }
943
- }
944
- catch (error) {
945
- console.error('[SessionTracker] Failed to start session:', error);
946
- }
947
- }
948
- /**
949
- * Send a heartbeat to keep session alive
950
- */
951
- async sendHeartbeat() {
952
- if (!this.sessionActive || !UNROO_AVAILABLE) {
953
- return;
954
- }
955
- try {
956
- await this.unrooClient.sessionHeartbeat({
957
- tool_calls_delta: this.toolCallCount,
958
- activity: this.activities.slice(-10), // Send last 10 activities
959
- machine_id: INSTANCE_ID, // Required for session key lookup
960
- repo_name: currentProject ? `${currentProject.github.owner}/${currentProject.github.repo}` : undefined,
961
- });
962
- this.lastHeartbeat = new Date();
963
- this.toolCallCount = 0;
964
- this.consecutiveHeartbeatFailures = 0; // Reset on success
965
- console.error('[SessionTracker] Heartbeat sent');
966
- }
967
- catch (error) {
968
- this.consecutiveHeartbeatFailures++;
969
- console.error(`[SessionTracker] Failed to send heartbeat (attempt ${this.consecutiveHeartbeatFailures}):`, error);
970
- throw error; // Re-throw for interval handler
971
- }
972
- }
973
- /**
974
- * End the current session and log activity summary to task
975
- */
976
- async endSession() {
977
- if (!this.sessionActive || !UNROO_AVAILABLE) {
978
- return;
979
- }
980
- if (this.heartbeatInterval) {
981
- clearInterval(this.heartbeatInterval);
982
- this.heartbeatInterval = null;
983
- }
984
- try {
985
- // End the session first - include machine_id and repo_name for session key lookup
986
- const result = await this.unrooClient.endSession({
987
- machine_id: INSTANCE_ID,
988
- repo_name: currentProject ? `${currentProject.github.owner}/${currentProject.github.repo}` : undefined,
989
- });
990
- this.sessionActive = false;
991
- console.error('[SessionTracker] Session ended');
992
- // Log activity summary to the task if we have a current task
993
- if (this.currentTaskId && result.session) {
994
- try {
995
- const session = result.session;
996
- const uniqueTools = [...new Set(this.activities.map(a => a.tool))];
997
- await this.unrooClient.logActivity(this.currentTaskId, {
998
- activity_type: 'claude_code_session',
999
- source: 'claude_code',
1000
- metadata: {
1001
- session_id: session.id,
1002
- duration_seconds: session.duration_minutes ? session.duration_minutes * 60 : 0,
1003
- total_tool_calls: session.total_tool_calls || this.toolCallCount,
1004
- tools_used: uniqueTools.slice(0, 20), // Limit to 20 tools
1005
- started_at: session.started_at,
1006
- ended_at: session.ended_at || new Date().toISOString(),
1007
- },
1008
- });
1009
- console.error(`[SessionTracker] Logged session activity to task ${this.currentTaskId}`);
1010
- }
1011
- catch (activityError) {
1012
- console.error('[SessionTracker] Failed to log activity to task:', activityError);
1013
- // Don't throw - session end was successful
1014
- }
1015
- }
1016
- }
1017
- catch (error) {
1018
- console.error('[SessionTracker] Failed to end session:', error);
1019
- }
1020
- }
1021
- /**
1022
- * Get session stats
1023
- */
1024
- getStats() {
1025
- return {
1026
- active: this.sessionActive,
1027
- toolCalls: this.toolCallCount,
1028
- lastHeartbeat: this.lastHeartbeat?.toISOString() || null,
1029
- };
1030
- }
1031
- }
1032
- // Session tracker instance created after clients below
1033
- // Create server
1034
- const server = new Server({
1035
- name: 'fcp-mcp-server',
1036
- version: MCP_SERVER_VERSION,
1037
- }, {
1038
- capabilities: {
1039
- tools: {},
1040
- resources: {},
1041
- },
1042
- });
1043
- const client = new FCPClient(FCP_API_URL, FCP_API_TOKEN);
1044
- const unrooClient = new UnrooClient(UNROO_API_URL, UNROO_API_KEY, USE_FCP_UNROO_PROXY, FCP_API_URL, FCP_API_TOKEN);
1045
- const sessionTracker = new SessionTracker(unrooClient);
1046
- // Tool definitions
1047
- const TOOLS = [
1048
- {
1049
- name: 'fcp_list_launches',
1050
- description: 'List launches from FCP with optional filters. Returns upcoming launches, their status, and basic info.',
1051
- inputSchema: {
1052
- type: 'object',
1053
- properties: {
1054
- status: {
1055
- type: 'string',
1056
- description: 'Filter by status: planning, in_progress, soft_launched, launched, on_hold, cancelled',
1057
- },
1058
- platform: {
1059
- type: 'string',
1060
- description: 'Filter by platform: wordpress, drupal, nextjs, other',
1061
- },
1062
- upcoming: {
1063
- type: 'number',
1064
- description: 'Show launches within next N days',
1065
- },
1066
- limit: {
1067
- type: 'number',
1068
- description: 'Maximum number of launches to return',
1069
- },
1070
- },
1071
- },
1072
- },
1073
- {
1074
- name: 'fcp_get_launch',
1075
- description: 'Get detailed information about a specific launch including checklist items, legacy access info, and team assignments.',
1076
- inputSchema: {
1077
- type: 'object',
1078
- properties: {
1079
- launch_id: {
1080
- type: 'number',
1081
- description: 'The ID of the launch to retrieve',
1082
- },
1083
- },
1084
- required: ['launch_id'],
1085
- },
1086
- },
1087
- {
1088
- name: 'fcp_get_legacy_access',
1089
- description: 'Get legacy hosting access information for a migration launch. Includes hosting provider, database access, file access, git repo, and DNS details.',
1090
- inputSchema: {
1091
- type: 'object',
1092
- properties: {
1093
- launch_id: {
1094
- type: 'number',
1095
- description: 'The ID of the launch',
1096
- },
1097
- },
1098
- required: ['launch_id'],
1099
- },
1100
- },
1101
- {
1102
- name: 'fcp_get_checklist',
1103
- description: 'Get the launch checklist with item statuses and any Claude-specific instructions for completing tasks.',
1104
- inputSchema: {
1105
- type: 'object',
1106
- properties: {
1107
- launch_id: {
1108
- type: 'number',
1109
- description: 'The ID of the launch',
1110
- },
1111
- category: {
1112
- type: 'string',
1113
- description: 'Optional: filter by category (infrastructure, security, pre_launch_webops, pre_launch_devops, pre_launch_dev, launch_day_devops, launch_day_seo, post_launch_day1, post_launch_week1, post_launch_day30)',
1114
- },
1115
- status: {
1116
- type: 'string',
1117
- description: 'Optional: filter by status (pending, in_progress, completed, blocked, skipped)',
1118
- },
1119
- },
1120
- required: ['launch_id'],
1121
- },
1122
- },
1123
- {
1124
- name: 'fcp_add_checklist_item',
1125
- description: 'Add a new checklist item to a launch. Requires title and category at minimum.',
1126
- inputSchema: {
1127
- type: 'object',
1128
- properties: {
1129
- launch_id: {
1130
- type: 'number',
1131
- description: 'The ID of the launch',
1132
- },
1133
- title: {
1134
- type: 'string',
1135
- description: 'Title of the checklist item',
1136
- },
1137
- category: {
1138
- type: 'string',
1139
- enum: ['infrastructure', 'security', 'pre_launch_webops', 'pre_launch_devops', 'pre_launch_dev', 'launch_day_devops', 'launch_day_seo', 'post_launch_day1', 'post_launch_week1', 'post_launch_day30'],
1140
- description: 'Category for the checklist item',
1141
- },
1142
- description: {
1143
- type: 'string',
1144
- description: 'Detailed description of the item',
1145
- },
1146
- role: {
1147
- type: 'string',
1148
- description: 'Team role responsible: webops, devops, dev, seo, client',
1149
- },
1150
- environment: {
1151
- type: 'string',
1152
- description: 'Environment: production, staging, both',
1153
- },
1154
- assigned_to_id: {
1155
- type: 'number',
1156
- description: 'User ID to assign the item to',
1157
- },
1158
- due_date: {
1159
- type: 'string',
1160
- description: 'Due date in ISO format',
1161
- },
1162
- sort_order: {
1163
- type: 'number',
1164
- description: 'Sort order within the category',
1165
- },
1166
- depends_on_item_id: {
1167
- type: 'number',
1168
- description: 'ID of another checklist item this depends on',
1169
- },
1170
- is_required: {
1171
- type: 'boolean',
1172
- description: 'Whether this item is required for launch (default: true)',
1173
- },
1174
- is_blocking: {
1175
- type: 'boolean',
1176
- description: 'Whether this item blocks the launch (default: false)',
1177
- },
1178
- external_link: {
1179
- type: 'string',
1180
- description: 'External URL related to this item',
1181
- },
1182
- },
1183
- required: ['launch_id', 'title', 'category'],
1184
- },
1185
- },
1186
- {
1187
- name: 'fcp_update_checklist_item',
1188
- description: 'Update a checklist item. Can change status, title, description, category, assignment, and other fields.',
1189
- inputSchema: {
1190
- type: 'object',
1191
- properties: {
1192
- launch_id: {
1193
- type: 'number',
1194
- description: 'The ID of the launch',
1195
- },
1196
- item_id: {
1197
- type: 'number',
1198
- description: 'The ID of the checklist item',
1199
- },
1200
- status: {
1201
- type: 'string',
1202
- enum: ['pending', 'in_progress', 'completed', 'blocked', 'skipped'],
1203
- description: 'New status for the item',
1204
- },
1205
- notes: {
1206
- type: 'string',
1207
- description: 'Notes about the status change (alias for completion_notes)',
1208
- },
1209
- title: {
1210
- type: 'string',
1211
- description: 'Updated title',
1212
- },
1213
- description: {
1214
- type: 'string',
1215
- description: 'Updated description (null to clear)',
1216
- },
1217
- category: {
1218
- type: 'string',
1219
- enum: ['infrastructure', 'security', 'pre_launch_webops', 'pre_launch_devops', 'pre_launch_dev', 'launch_day_devops', 'launch_day_seo', 'post_launch_day1', 'post_launch_week1', 'post_launch_day30'],
1220
- description: 'Updated category',
1221
- },
1222
- role: {
1223
- type: 'string',
1224
- description: 'Updated team role: webops, devops, dev, seo, client',
1225
- },
1226
- environment: {
1227
- type: 'string',
1228
- description: 'Updated environment: production, staging, both',
1229
- },
1230
- assigned_to_id: {
1231
- type: 'number',
1232
- description: 'User ID to assign to (null to unassign)',
1233
- },
1234
- due_date: {
1235
- type: 'string',
1236
- description: 'Updated due date in ISO format (null to clear)',
1237
- },
1238
- sort_order: {
1239
- type: 'number',
1240
- description: 'Updated sort order',
1241
- },
1242
- depends_on_item_id: {
1243
- type: 'number',
1244
- description: 'Updated dependency item ID (null to clear)',
1245
- },
1246
- is_required: {
1247
- type: 'boolean',
1248
- description: 'Whether this item is required for launch',
1249
- },
1250
- is_blocking: {
1251
- type: 'boolean',
1252
- description: 'Whether this item blocks the launch',
1253
- },
1254
- external_link: {
1255
- type: 'string',
1256
- description: 'External URL (null to clear)',
1257
- },
1258
- evidence_url: {
1259
- type: 'string',
1260
- description: 'URL to evidence of completion (null to clear)',
1261
- },
1262
- jira_ticket_key: {
1263
- type: 'string',
1264
- description: 'Associated Jira ticket key (null to clear)',
1265
- },
1266
- },
1267
- required: ['launch_id', 'item_id'],
1268
- },
1269
- },
1270
- {
1271
- name: 'fcp_delete_checklist_item',
1272
- description: 'Delete a checklist item from a launch. This action cannot be undone.',
1273
- inputSchema: {
1274
- type: 'object',
1275
- properties: {
1276
- launch_id: {
1277
- type: 'number',
1278
- description: 'The ID of the launch',
1279
- },
1280
- item_id: {
1281
- type: 'number',
1282
- description: 'The ID of the checklist item to delete',
1283
- },
1284
- },
1285
- required: ['launch_id', 'item_id'],
1286
- },
1287
- },
1288
- {
1289
- name: 'fcp_add_progress_note',
1290
- description: 'Add a progress note to a launch. Use this to document what work was done or any issues encountered.',
1291
- inputSchema: {
1292
- type: 'object',
1293
- properties: {
1294
- launch_id: {
1295
- type: 'number',
1296
- description: 'The ID of the launch',
1297
- },
1298
- content: {
1299
- type: 'string',
1300
- description: 'The note content',
1301
- },
1302
- },
1303
- required: ['launch_id', 'content'],
1304
- },
1305
- },
1306
- {
1307
- name: 'fcp_get_claude_md',
1308
- description: 'Generate CLAUDE.md content for a launch. Returns formatted markdown with project context, legacy access info, and checklist.',
1309
- inputSchema: {
1310
- type: 'object',
1311
- properties: {
1312
- launch_id: {
1313
- type: 'number',
1314
- description: 'The ID of the launch',
1315
- },
1316
- },
1317
- required: ['launch_id'],
1318
- },
1319
- },
1320
- // Launch CRUD Tools
1321
- {
1322
- name: 'fcp_create_launch',
1323
- description: 'Create a new launch in FCP. Requires name, platform, launch_type, and target_launch_date. Optionally creates a default checklist.',
1324
- inputSchema: {
1325
- type: 'object',
1326
- properties: {
1327
- name: {
1328
- type: 'string',
1329
- description: 'Launch name (required)',
1330
- },
1331
- platform: {
1332
- type: 'string',
1333
- enum: ['wordpress', 'drupal', 'nextjs', 'other'],
1334
- description: 'Platform type (required)',
1335
- },
1336
- launch_type: {
1337
- type: 'string',
1338
- enum: ['new_site', 'migration', 'redesign', 'replatform', 'hosting_only', 'maintenance'],
1339
- description: 'Type of launch (required)',
1340
- },
1341
- target_launch_date: {
1342
- type: 'string',
1343
- description: 'Target launch date in ISO format (required)',
1344
- },
1345
- website_id: {
1346
- type: 'number',
1347
- description: 'Associated website ID',
1348
- },
1349
- account_id: {
1350
- type: 'number',
1351
- description: 'Associated account/client ID',
1352
- },
1353
- description: {
1354
- type: 'string',
1355
- description: 'Launch description',
1356
- },
1357
- soft_launch_date: {
1358
- type: 'string',
1359
- description: 'Soft launch date in ISO format',
1360
- },
1361
- kickoff_date: {
1362
- type: 'string',
1363
- description: 'Kickoff date in ISO format',
1364
- },
1365
- priority: {
1366
- type: 'string',
1367
- enum: ['low', 'medium', 'high', 'critical'],
1368
- description: 'Launch priority (default: medium)',
1369
- },
1370
- jira_project_key: {
1371
- type: 'string',
1372
- description: 'Jira project key',
1373
- },
1374
- unroo_project_id: {
1375
- type: 'number',
1376
- description: 'Unroo project ID to link',
1377
- },
1378
- webops_lead_id: {
1379
- type: 'number',
1380
- description: 'WebOps lead user ID',
1381
- },
1382
- devops_lead_id: {
1383
- type: 'number',
1384
- description: 'DevOps lead user ID',
1385
- },
1386
- dev_lead_id: {
1387
- type: 'number',
1388
- description: 'Dev lead user ID',
1389
- },
1390
- seo_lead_id: {
1391
- type: 'number',
1392
- description: 'SEO lead user ID',
1393
- },
1394
- client_contact: {
1395
- type: 'string',
1396
- description: 'Client contact name',
1397
- },
1398
- client_contact_email: {
1399
- type: 'string',
1400
- description: 'Client contact email',
1401
- },
1402
- use_default_checklist: {
1403
- type: 'boolean',
1404
- description: 'Whether to create default checklist items (default: true)',
1405
- },
1406
- slack_channel: {
1407
- type: 'string',
1408
- description: 'Slack channel for launch countdown notifications, e.g. #channel-name',
1409
- },
1410
- },
1411
- required: ['name', 'platform', 'launch_type', 'target_launch_date'],
1412
- },
1413
- },
1414
- {
1415
- name: 'fcp_update_launch',
1416
- description: 'Update an existing launch. Only provided fields are updated. Can change status, dates, assignments, and other properties.',
1417
- inputSchema: {
1418
- type: 'object',
1419
- properties: {
1420
- launch_id: {
1421
- type: 'number',
1422
- description: 'The ID of the launch to update (required)',
1423
- },
1424
- website_id: {
1425
- type: 'number',
1426
- description: 'Associated website ID (null to unlink)',
1427
- },
1428
- account_id: {
1429
- type: 'number',
1430
- description: 'Associated account/client ID (null to unlink)',
1431
- },
1432
- name: {
1433
- type: 'string',
1434
- description: 'Updated launch name',
1435
- },
1436
- description: {
1437
- type: 'string',
1438
- description: 'Updated description',
1439
- },
1440
- status: {
1441
- type: 'string',
1442
- enum: ['planning', 'in_progress', 'soft_launched', 'launched', 'on_hold', 'cancelled'],
1443
- description: 'Updated status',
1444
- },
1445
- platform: {
1446
- type: 'string',
1447
- enum: ['wordpress', 'drupal', 'nextjs', 'other'],
1448
- description: 'Updated platform',
1449
- },
1450
- launch_type: {
1451
- type: 'string',
1452
- enum: ['new_site', 'migration', 'redesign', 'replatform', 'hosting_only', 'maintenance'],
1453
- description: 'Updated launch type',
1454
- },
1455
- target_launch_date: {
1456
- type: 'string',
1457
- description: 'Updated target launch date',
1458
- },
1459
- soft_launch_date: {
1460
- type: 'string',
1461
- description: 'Updated soft launch date (null to clear)',
1462
- },
1463
- actual_launch_date: {
1464
- type: 'string',
1465
- description: 'Actual launch date when launched',
1466
- },
1467
- kickoff_date: {
1468
- type: 'string',
1469
- description: 'Updated kickoff date (null to clear)',
1470
- },
1471
- priority: {
1472
- type: 'string',
1473
- enum: ['low', 'medium', 'high', 'critical'],
1474
- description: 'Updated priority',
1475
- },
1476
- jira_project_key: {
1477
- type: 'string',
1478
- description: 'Jira project key (null to clear)',
1479
- },
1480
- unroo_project_id: {
1481
- type: 'number',
1482
- description: 'Unroo project ID (null to unlink)',
1483
- },
1484
- webops_lead_id: {
1485
- type: 'number',
1486
- description: 'WebOps lead user ID (null to clear)',
1487
- },
1488
- devops_lead_id: {
1489
- type: 'number',
1490
- description: 'DevOps lead user ID (null to clear)',
1491
- },
1492
- dev_lead_id: {
1493
- type: 'number',
1494
- description: 'Dev lead user ID (null to clear)',
1495
- },
1496
- seo_lead_id: {
1497
- type: 'number',
1498
- description: 'SEO lead user ID (null to clear)',
1499
- },
1500
- client_contact: {
1501
- type: 'string',
1502
- description: 'Client contact name (null to clear)',
1503
- },
1504
- client_contact_email: {
1505
- type: 'string',
1506
- description: 'Client contact email (null to clear)',
1507
- },
1508
- slack_channel: {
1509
- type: 'string',
1510
- description: 'Slack channel for launch countdown notifications, e.g. #channel-name (null to clear)',
1511
- },
1512
- },
1513
- required: ['launch_id'],
1514
- },
1515
- },
1516
- {
1517
- name: 'fcp_delete_launch',
1518
- description: 'Delete a launch and all associated checklist items. This action cannot be undone.',
1519
- inputSchema: {
1520
- type: 'object',
1521
- properties: {
1522
- launch_id: {
1523
- type: 'number',
1524
- description: 'The ID of the launch to delete',
1525
- },
1526
- },
1527
- required: ['launch_id'],
1528
- },
1529
- },
1530
- // Validation / Success Factor Tools
1531
- {
1532
- name: 'fcp_get_success_factors',
1533
- description: 'Get the success factors (validation criteria) for a checklist item. Shows what automated checks are configured.',
1534
- inputSchema: {
1535
- type: 'object',
1536
- properties: {
1537
- launch_id: {
1538
- type: 'number',
1539
- description: 'The ID of the launch',
1540
- },
1541
- item_id: {
1542
- type: 'number',
1543
- description: 'The ID of the checklist item',
1544
- },
1545
- },
1546
- required: ['launch_id', 'item_id'],
1547
- },
1548
- },
1549
- {
1550
- name: 'fcp_validate_checklist_item',
1551
- description: 'Run automated validation checks on a checklist item. Executes all configured success factors (HTTP checks, DNS checks, SSL checks, etc.) and returns results.',
1552
- inputSchema: {
1553
- type: 'object',
1554
- properties: {
1555
- launch_id: {
1556
- type: 'number',
1557
- description: 'The ID of the launch',
1558
- },
1559
- item_id: {
1560
- type: 'number',
1561
- description: 'The ID of the checklist item to validate',
1562
- },
1563
- },
1564
- required: ['launch_id', 'item_id'],
1565
- },
1566
- },
1567
- {
1568
- name: 'fcp_get_unroo_section',
1569
- description: 'Generate the Unroo task management section for a CLAUDE.md file. Auto-detects project key from git remote. Use this to add Unroo integration instructions to any project.',
1570
- inputSchema: {
1571
- type: 'object',
1572
- properties: {
1573
- repo_url: {
1574
- type: 'string',
1575
- description: 'Git remote URL (e.g., git@github.com:owner/repo.git). If not provided, uses current directory.',
1576
- },
1577
- project_key: {
1578
- type: 'string',
1579
- description: 'Manual project key override. Use if auto-detection fails.',
1580
- },
1581
- },
1582
- },
1583
- },
1584
- // Unroo Task Management Tools
1585
- {
1586
- name: 'unroo_list_projects',
1587
- description: 'List all Unroo projects (including those not mapped to FCP). Returns project details with organization, company, and JIRA key info.',
1588
- inputSchema: {
1589
- type: 'object',
1590
- properties: {},
1591
- },
1592
- },
1593
- {
1594
- name: 'unroo_list_tasks',
1595
- description: 'List tasks from Unroo with optional filters. Use to find tasks for a project, by status, or by assignee.',
1596
- inputSchema: {
1597
- type: 'object',
1598
- properties: {
1599
- project_key: {
1600
- type: 'string',
1601
- description: 'Filter by JIRA project key or FCP-SITE-{id}',
1602
- },
1603
- status: {
1604
- type: 'string',
1605
- description: 'Filter by status: To Do, In Progress, Done, Blocked (comma-separated for multiple)',
1606
- },
1607
- assignee_email: {
1608
- type: 'string',
1609
- description: 'Filter by assignee email',
1610
- },
1611
- external_source_type: {
1612
- type: 'string',
1613
- description: 'Filter by source: fcp, fcp_checklist, fcp_launch',
1614
- },
1615
- limit: {
1616
- type: 'number',
1617
- description: 'Maximum number of tasks to return (default 100)',
1618
- },
1619
- },
1620
- },
1621
- },
1622
- {
1623
- name: 'unroo_create_task',
1624
- description: 'Create a new task in Unroo. Requires title and project_key. Use for follow-up tasks, discovered issues, or new work items.',
1625
- inputSchema: {
1626
- type: 'object',
1627
- properties: {
1628
- title: {
1629
- type: 'string',
1630
- description: 'Task title (required)',
1631
- },
1632
- description: {
1633
- type: 'string',
1634
- description: 'Detailed description of the task',
1635
- },
1636
- project_key: {
1637
- type: 'string',
1638
- description: 'JIRA project key or FCP-SITE-{id} (required)',
1639
- },
1640
- priority: {
1641
- type: 'string',
1642
- enum: ['urgent', 'high', 'medium', 'low'],
1643
- description: 'Task priority (default: medium)',
1644
- },
1645
- status: {
1646
- type: 'string',
1647
- description: 'Initial status (default: To Do)',
1648
- },
1649
- assignee_email: {
1650
- type: 'string',
1651
- description: 'Email of person to assign the task to',
1652
- },
1653
- due_date: {
1654
- type: 'string',
1655
- description: 'Due date in ISO format',
1656
- },
1657
- hours_estimated: {
1658
- type: 'number',
1659
- description: 'Estimated hours to complete',
1660
- },
1661
- labels: {
1662
- type: 'array',
1663
- items: { type: 'string' },
1664
- description: 'Labels/tags for the task',
1665
- },
1666
- parent_issue_id: {
1667
- type: 'string',
1668
- description: 'Parent task ID if this is a subtask',
1669
- },
1670
- github_branch: {
1671
- type: 'string',
1672
- description: 'Git branch name associated with this task',
1673
- },
1674
- github_repo: {
1675
- type: 'string',
1676
- description: 'GitHub repository (owner/repo format)',
1677
- },
1678
- },
1679
- required: ['title', 'project_key'],
1680
- },
1681
- },
1682
- {
1683
- name: 'unroo_get_task',
1684
- description: 'Get detailed information about a specific task by ID. Returns full task details including description, status, priority, labels, hours, and activity.',
1685
- inputSchema: {
1686
- type: 'object',
1687
- properties: {
1688
- task_id: {
1689
- type: 'string',
1690
- description: 'The ID of the task to retrieve (required)',
1691
- },
1692
- },
1693
- required: ['task_id'],
1694
- },
1695
- },
1696
- {
1697
- name: 'unroo_update_task',
1698
- description: 'Update an existing task in Unroo. Use to change status, log hours, update priority, or reassign.',
1699
- inputSchema: {
1700
- type: 'object',
1701
- properties: {
1702
- task_id: {
1703
- type: 'string',
1704
- description: 'The ID of the task to update (required)',
1705
- },
1706
- title: {
1707
- type: 'string',
1708
- description: 'Updated title',
1709
- },
1710
- description: {
1711
- type: 'string',
1712
- description: 'Updated description',
1713
- },
1714
- status: {
1715
- type: 'string',
1716
- description: 'New status: To Do, In Progress, Done, Blocked',
1717
- },
1718
- priority: {
1719
- type: 'string',
1720
- enum: ['urgent', 'high', 'medium', 'low'],
1721
- description: 'Updated priority',
1722
- },
1723
- assignee_email: {
1724
- type: 'string',
1725
- description: 'New assignee email (null to unassign)',
1726
- },
1727
- hours_logged: {
1728
- type: 'number',
1729
- description: 'Total hours logged on the task',
1730
- },
1731
- resolution: {
1732
- type: 'string',
1733
- description: 'Resolution when marking as Done',
1734
- },
1735
- github_branch: {
1736
- type: 'string',
1737
- description: 'Git branch name associated with this task',
1738
- },
1739
- github_pr_url: {
1740
- type: 'string',
1741
- description: 'GitHub pull request URL',
1742
- },
1743
- github_pr_number: {
1744
- type: 'number',
1745
- description: 'GitHub pull request number',
1746
- },
1747
- github_repo: {
1748
- type: 'string',
1749
- description: 'GitHub repository (owner/repo format)',
1750
- },
1751
- },
1752
- required: ['task_id'],
1753
- },
1754
- },
1755
- {
1756
- name: 'unroo_add_comment',
1757
- description: 'Add a comment to an Unroo task. Use this for progress updates, notes, or tagging people with @mentions. Comments sync to Jira and notify watchers. Use is_internal=true for team-only notes.',
1758
- inputSchema: {
1759
- type: 'object',
1760
- properties: {
1761
- task_id: {
1762
- type: 'string',
1763
- description: 'Task ID or Jira key (e.g., task_abc123 or FLYFRU-189)',
1764
- },
1765
- comment_text: {
1766
- type: 'string',
1767
- description: 'The comment content. Use @email to mention people (e.g., @tony.diaz@fruition.net)',
1768
- },
1769
- is_internal: {
1770
- type: 'boolean',
1771
- description: 'If true, comment is team-only (not synced to Jira, not visible to customers). Default: false',
1772
- },
1773
- mentions: {
1774
- type: 'array',
1775
- items: { type: 'string' },
1776
- description: 'Email addresses of people to mention/notify (e.g., ["tony.diaz@fruition.net"])',
1777
- },
1778
- },
1779
- required: ['task_id', 'comment_text'],
1780
- },
1781
- },
1782
- {
1783
- name: 'unroo_list_comments',
1784
- description: 'List comments on an Unroo task. Returns all comments in reverse chronological order.',
1785
- inputSchema: {
1786
- type: 'object',
1787
- properties: {
1788
- task_id: {
1789
- type: 'string',
1790
- description: 'Task ID or Jira key (e.g., task_abc123 or FLYFRU-189)',
1791
- },
1792
- },
1793
- required: ['task_id'],
1794
- },
1795
- },
1796
- {
1797
- name: 'unroo_get_my_tasks',
1798
- description: 'Get tasks assigned to the current user (based on API key). Useful for finding your work items.',
1799
- inputSchema: {
1800
- type: 'object',
1801
- properties: {
1802
- status: {
1803
- type: 'string',
1804
- description: 'Filter by status (comma-separated for multiple)',
1805
- },
1806
- limit: {
1807
- type: 'number',
1808
- description: 'Maximum number of tasks to return',
1809
- },
1810
- },
1811
- },
1812
- },
1813
- {
1814
- name: 'unroo_start_session',
1815
- description: 'Start a work session for tracking time and activity. Call this when beginning work on a task.',
1816
- inputSchema: {
1817
- type: 'object',
1818
- properties: {
1819
- task_id: {
1820
- type: 'string',
1821
- description: 'The task you are working on',
1822
- },
1823
- task_jira_key: {
1824
- type: 'string',
1825
- description: 'JIRA issue key if applicable',
1826
- },
1827
- project_key: {
1828
- type: 'string',
1829
- description: 'Project being worked on',
1830
- },
1831
- repo_name: {
1832
- type: 'string',
1833
- description: 'Repository name if working on code',
1834
- },
1835
- },
1836
- },
1837
- },
1838
- {
1839
- name: 'unroo_end_session',
1840
- description: 'End the current work session and log time. Call this when finishing work.',
1841
- inputSchema: {
1842
- type: 'object',
1843
- properties: {},
1844
- },
1845
- },
1846
- {
1847
- name: 'unroo_create_follow_up',
1848
- description: 'Create a follow-up task linked to a parent task. Use for discovered issues, tech debt, or next steps.',
1849
- inputSchema: {
1850
- type: 'object',
1851
- properties: {
1852
- parent_task_id: {
1853
- type: 'string',
1854
- description: 'The parent task this follows up on (required)',
1855
- },
1856
- title: {
1857
- type: 'string',
1858
- description: 'Follow-up task title (required)',
1859
- },
1860
- description: {
1861
- type: 'string',
1862
- description: 'Description of the follow-up work',
1863
- },
1864
- priority: {
1865
- type: 'string',
1866
- enum: ['urgent', 'high', 'medium', 'low'],
1867
- description: 'Priority (default: same as parent or medium)',
1868
- },
1869
- },
1870
- required: ['parent_task_id', 'title'],
1871
- },
1872
- },
1873
- // Parking Lot / Backlog Tools
1874
- {
1875
- name: 'unroo_log_future_work',
1876
- description: 'Log future work items to parking lot or backlog. Use when you discover work that should be done later - bugs, tech debt, features, documentation needs, etc.',
1877
- inputSchema: {
1878
- type: 'object',
1879
- properties: {
1880
- title: {
1881
- type: 'string',
1882
- description: 'Title of the future work item (required)',
1883
- },
1884
- project_key: {
1885
- type: 'string',
1886
- description: 'JIRA project key or FCP-SITE-{id} (required)',
1887
- },
1888
- description: {
1889
- type: 'string',
1890
- description: 'Detailed description of the work needed',
1891
- },
1892
- priority: {
1893
- type: 'string',
1894
- enum: ['Urgent', 'High', 'Medium', 'Low'],
1895
- description: 'Priority level (default: Medium)',
1896
- },
1897
- task_type: {
1898
- type: 'string',
1899
- enum: ['bug', 'tech_debt', 'feature', 'documentation', 'security', 'performance'],
1900
- description: 'Type of work item',
1901
- },
1902
- estimated_hours: {
1903
- type: 'number',
1904
- description: 'Estimated hours to complete',
1905
- },
1906
- launch_id: {
1907
- type: 'number',
1908
- description: 'FCP launch ID if related to a launch',
1909
- },
1910
- checklist_item_id: {
1911
- type: 'number',
1912
- description: 'FCP checklist item ID if discovered during checklist work',
1913
- },
1914
- notes: {
1915
- type: 'string',
1916
- description: 'Additional notes or context',
1917
- },
1918
- destination: {
1919
- type: 'string',
1920
- enum: ['parking_lot', 'backlog'],
1921
- description: 'Where to put the item: parking_lot (needs review) or backlog (ready for sprint). Default: parking_lot',
1922
- },
1923
- },
1924
- required: ['title', 'project_key'],
1925
- },
1926
- },
1927
- {
1928
- name: 'unroo_get_parking_lot',
1929
- description: 'Get items from the parking lot - discovered work that needs review before being added to backlog.',
1930
- inputSchema: {
1931
- type: 'object',
1932
- properties: {
1933
- project_key: {
1934
- type: 'string',
1935
- description: 'Filter by JIRA project key or FCP-SITE-{id}',
1936
- },
1937
- status: {
1938
- type: 'string',
1939
- description: 'Filter by status: pending, approved, rejected, converted (default: pending)',
1940
- },
1941
- limit: {
1942
- type: 'number',
1943
- description: 'Maximum number of items to return (default: 100)',
1944
- },
1945
- },
1946
- },
1947
- },
1948
- {
1949
- name: 'unroo_get_backlog',
1950
- description: 'Get backlog items - tasks ready to be scheduled into sprints.',
1951
- inputSchema: {
1952
- type: 'object',
1953
- properties: {
1954
- project_key: {
1955
- type: 'string',
1956
- description: 'Filter by JIRA project key or FCP-SITE-{id}',
1957
- },
1958
- priority: {
1959
- type: 'string',
1960
- enum: ['Urgent', 'High', 'Medium', 'Low'],
1961
- description: 'Filter by priority',
1962
- },
1963
- limit: {
1964
- type: 'number',
1965
- description: 'Maximum number of items to return (default: 100)',
1966
- },
1967
- },
1968
- },
1969
- },
1970
- {
1971
- name: 'unroo_convert_to_backlog',
1972
- description: 'Convert a parking lot item to backlog. Use after reviewing a discovered item and deciding it should be done.',
1973
- inputSchema: {
1974
- type: 'object',
1975
- properties: {
1976
- id: {
1977
- type: 'string',
1978
- description: 'The ID of the parking lot item to convert (required)',
1979
- },
1980
- priority: {
1981
- type: 'string',
1982
- enum: ['Urgent', 'High', 'Medium', 'Low'],
1983
- description: 'Priority for the backlog item (optional, keeps original if not specified)',
1984
- },
1985
- notes: {
1986
- type: 'string',
1987
- description: 'Notes about the conversion decision',
1988
- },
1989
- },
1990
- required: ['id'],
1991
- },
1992
- },
1993
- // FileSync Tools
1994
- {
1995
- name: 'fcp_filesync_list_configs',
1996
- description: 'List all file sync configurations with current status. Shows prod/staging PVC pairs, scheduling, sync direction, and last sync info.',
1997
- inputSchema: {
1998
- type: 'object',
1999
- properties: {
2000
- enabled: {
2001
- type: 'boolean',
2002
- description: 'Filter by enabled status',
2003
- },
2004
- search: {
2005
- type: 'string',
2006
- description: 'Search across site names, clusters, namespaces',
2007
- },
2008
- sync_direction: {
2009
- type: 'string',
2010
- enum: ['prod_to_staging', 'staging_to_prod'],
2011
- description: 'Filter by sync direction',
2012
- },
2013
- },
2014
- },
2015
- },
2016
- {
2017
- name: 'fcp_filesync_get_config',
2018
- description: 'Get detailed file sync config with recent job history. Returns full config, last 5 jobs, and scheduling info.',
2019
- inputSchema: {
2020
- type: 'object',
2021
- properties: {
2022
- config_id: {
2023
- type: 'number',
2024
- description: 'The config ID to retrieve',
2025
- },
2026
- },
2027
- required: ['config_id'],
2028
- },
2029
- },
2030
- {
2031
- name: 'fcp_filesync_start_sync',
2032
- description: 'Trigger a file sync immediately. For prod-to-staging syncs, executes directly. For staging-to-prod syncs, requires a confirmation_token from fcp_filesync_get_confirmation.',
2033
- inputSchema: {
2034
- type: 'object',
2035
- properties: {
2036
- config_id: {
2037
- type: 'number',
2038
- description: 'The config ID to sync',
2039
- },
2040
- confirmation_token: {
2041
- type: 'string',
2042
- description: 'Required for staging-to-prod syncs. Get from fcp_filesync_get_confirmation.',
2043
- },
2044
- },
2045
- required: ['config_id'],
2046
- },
2047
- },
2048
- {
2049
- name: 'fcp_filesync_get_job_status',
2050
- description: 'Get the status of a file sync job. Returns progress, current step, rsync stats, and timing info. Call in a loop to monitor a running sync.',
2051
- inputSchema: {
2052
- type: 'object',
2053
- properties: {
2054
- config_id: {
2055
- type: 'number',
2056
- description: 'The config ID to check jobs for',
2057
- },
2058
- status: {
2059
- type: 'string',
2060
- description: 'Filter by job status (e.g. pending, syncing, completed, failed)',
2061
- },
2062
- limit: {
2063
- type: 'number',
2064
- description: 'Number of jobs to return (default 5)',
2065
- },
2066
- },
2067
- required: ['config_id'],
2068
- },
2069
- },
2070
- {
2071
- name: 'fcp_filesync_cancel_sync',
2072
- description: 'Cancel a running file sync. Kills the K8s Job and marks the job as cancelled.',
2073
- inputSchema: {
2074
- type: 'object',
2075
- properties: {
2076
- job_id: {
2077
- type: 'string',
2078
- description: 'The job ID (UUID) to cancel',
2079
- },
2080
- },
2081
- required: ['job_id'],
2082
- },
2083
- },
2084
- {
2085
- name: 'fcp_filesync_get_confirmation',
2086
- description: 'Generate a confirmation token for staging-to-prod syncs. Token is valid for 5 minutes. Use before fcp_filesync_start_sync for staging_to_prod direction.',
2087
- inputSchema: {
2088
- type: 'object',
2089
- properties: {
2090
- config_id: {
2091
- type: 'number',
2092
- description: 'The config ID requiring confirmation',
2093
- },
2094
- },
2095
- required: ['config_id'],
2096
- },
2097
- },
2098
- // Nuclei Security Scanning
2099
- {
2100
- name: 'fcp_trigger_nuclei_scan',
2101
- description: 'Trigger a Nuclei security scan for a website. Creates a Kubernetes Job that runs the scan and stores results in the FCP database.',
2102
- inputSchema: {
2103
- type: 'object',
2104
- properties: {
2105
- website_id: {
2106
- type: 'number',
2107
- description: 'The website ID to scan (required)',
2108
- },
2109
- url: {
2110
- type: 'string',
2111
- description: 'Override target URL (optional, auto-resolved from website record if not provided)',
2112
- },
2113
- severity: {
2114
- type: 'string',
2115
- description: 'Comma-separated severity levels to scan for (default: critical,high,medium)',
2116
- },
2117
- templates: {
2118
- type: 'string',
2119
- description: 'Comma-separated Nuclei template categories (default: cves,exposures,misconfiguration). Options: cves, vulnerabilities, exposures, misconfiguration, technologies',
2120
- },
2121
- },
2122
- required: ['website_id'],
2123
- },
2124
- },
2125
- {
2126
- name: 'fcp_get_nuclei_results',
2127
- description: 'Get Nuclei security scan results for a website. Returns scan history with findings summary and individual vulnerability details.',
2128
- inputSchema: {
2129
- type: 'object',
2130
- properties: {
2131
- website_id: {
2132
- type: 'number',
2133
- description: 'The website ID to get results for (required)',
2134
- },
2135
- scan_id: {
2136
- type: 'string',
2137
- description: 'Specific scan ID to get detailed results for (optional)',
2138
- },
2139
- limit: {
2140
- type: 'number',
2141
- description: 'Maximum number of scans to return (default: 5)',
2142
- },
2143
- status: {
2144
- type: 'string',
2145
- description: 'Filter findings by status: active, fixed, reopened, false_positive',
2146
- },
2147
- },
2148
- required: ['website_id'],
2149
- },
2150
- },
2151
- // Security Headers Tools
2152
- {
2153
- name: 'fcp_scan_security_headers',
2154
- description: 'Scan a website for HTTP security headers (CSP, HSTS, X-Frame-Options, etc.). Returns grade (A-F), score (0-100), and per-header analysis with issues and recommendations.',
2155
- inputSchema: {
2156
- type: 'object',
2157
- properties: {
2158
- website_id: {
2159
- type: 'number',
2160
- description: 'The website ID to scan (required)',
2161
- },
2162
- },
2163
- required: ['website_id'],
2164
- },
2165
- },
2166
- {
2167
- name: 'fcp_get_security_headers_results',
2168
- description: 'Get security headers scan results for a website. Returns the latest scan grade, score, and per-header findings with issues and recommendations.',
2169
- inputSchema: {
2170
- type: 'object',
2171
- properties: {
2172
- website_id: {
2173
- type: 'number',
2174
- description: 'The website ID to get results for (required)',
2175
- },
2176
- scan_id: {
2177
- type: 'string',
2178
- description: 'Specific scan ID to get detailed results for (optional, defaults to latest)',
2179
- },
2180
- },
2181
- required: ['website_id'],
2182
- },
2183
- },
2184
- // Site/Website CRUD Tools
2185
- {
2186
- name: 'fcp_list_sites',
2187
- description: 'List websites managed by FCP with optional filters. Returns paginated results with account info.',
2188
- inputSchema: {
2189
- type: 'object',
2190
- properties: {
2191
- account_id: {
2192
- type: 'number',
2193
- description: 'Filter by account/client ID',
2194
- },
2195
- cms: {
2196
- type: 'string',
2197
- description: 'Filter by CMS type: WordPress, Drupal, Strapi, Other',
2198
- },
2199
- environment: {
2200
- type: 'string',
2201
- description: 'Filter by environment: production, staging, development',
2202
- },
2203
- retired: {
2204
- type: 'string',
2205
- description: 'Filter retired sites: "true" (only retired), "all" (include retired), omit for active only',
2206
- },
2207
- limit: {
2208
- type: 'number',
2209
- description: 'Maximum results to return (default: 50, max: 200)',
2210
- },
2211
- offset: {
2212
- type: 'number',
2213
- description: 'Offset for pagination (default: 0)',
2214
- },
2215
- },
2216
- },
2217
- },
2218
- {
2219
- name: 'fcp_search_sites',
2220
- description: 'Search websites by domain name, account name, or URL. Returns matching sites ranked by relevance.',
2221
- inputSchema: {
2222
- type: 'object',
2223
- properties: {
2224
- query: {
2225
- type: 'string',
2226
- description: 'Search query (min 2 characters) - matches against domain, account name, and URL',
2227
- },
2228
- limit: {
2229
- type: 'number',
2230
- description: 'Maximum results to return (default: 10)',
2231
- },
2232
- },
2233
- required: ['query'],
2234
- },
2235
- },
2236
- {
2237
- name: 'fcp_get_site',
2238
- description: 'Get detailed information about a specific website including account info, infrastructure details, and lead developer.',
2239
- inputSchema: {
2240
- type: 'object',
2241
- properties: {
2242
- website_id: {
2243
- type: 'number',
2244
- description: 'The website ID to retrieve',
2245
- },
2246
- },
2247
- required: ['website_id'],
2248
- },
2249
- },
2250
- {
2251
- name: 'fcp_get_local_setup_guide',
2252
- description: 'Generate a local development setup guide for a website. Returns CMS-specific instructions for cloning, DDEV configuration, .env template, database import, and verification steps. Works for Bedrock WordPress, standard WordPress, and Drupal sites.',
2253
- inputSchema: {
2254
- type: 'object',
2255
- properties: {
2256
- website_id: {
2257
- type: 'number',
2258
- description: 'The website ID to generate setup guide for',
2259
- },
2260
- },
2261
- required: ['website_id'],
2262
- },
2263
- },
2264
- {
2265
- name: 'fcp_create_site',
2266
- description: 'Create a new website with optional staging environments. Production site is created first, then staging sites are linked to it.',
2267
- inputSchema: {
2268
- type: 'object',
2269
- properties: {
2270
- account_id: {
2271
- type: 'number',
2272
- description: 'Account/client ID the site belongs to (required)',
2273
- },
2274
- domain: {
2275
- type: 'string',
2276
- description: 'Primary domain name, e.g. "example.com" (required)',
2277
- },
2278
- cms: {
2279
- type: 'string',
2280
- description: 'CMS type: WordPress, Drupal, Strapi, Other (required)',
2281
- },
2282
- url_full: {
2283
- type: 'string',
2284
- description: 'Full URL including protocol (default: https://<domain>)',
2285
- },
2286
- git_provider: {
2287
- type: 'string',
2288
- description: 'Git provider: GitHub, GitLab, Bitbucket (default: GitHub)',
2289
- },
2290
- git_link: {
2291
- type: 'string',
2292
- description: 'URL to the git repository',
2293
- },
2294
- hosting_provider: {
2295
- type: 'string',
2296
- description: 'Hosting provider name',
2297
- },
2298
- fru_hosted: {
2299
- type: 'boolean',
2300
- description: 'Whether the site is hosted on FruCloud (default: false)',
2301
- },
2302
- k8s_cluster: {
2303
- type: 'string',
2304
- description: 'Kubernetes cluster name if FruCloud hosted',
2305
- },
2306
- k8s_namespace: {
2307
- type: 'string',
2308
- description: 'Kubernetes namespace if FruCloud hosted',
2309
- },
2310
- staging: {
2311
- type: 'array',
2312
- items: {
2313
- type: 'object',
2314
- properties: {
2315
- domain: { type: 'string', description: 'Staging domain' },
2316
- k8s_namespace: { type: 'string', description: 'Staging K8s namespace' },
2317
- },
2318
- required: ['domain', 'k8s_namespace'],
2319
- },
2320
- description: 'Optional staging environments to create',
2321
- },
2322
- },
2323
- required: ['account_id', 'domain', 'cms'],
2324
- },
2325
- },
2326
- {
2327
- name: 'fcp_update_site',
2328
- description: 'Update properties of an existing website. Only provided fields are updated.',
2329
- inputSchema: {
2330
- type: 'object',
2331
- properties: {
2332
- website_id: {
2333
- type: 'number',
2334
- description: 'The website ID to update (required)',
2335
- },
2336
- domain: {
2337
- type: 'string',
2338
- description: 'Updated domain name',
2339
- },
2340
- url_full: {
2341
- type: 'string',
2342
- description: 'Updated full URL',
2343
- },
2344
- cms: {
2345
- type: 'string',
2346
- description: 'Updated CMS type',
2347
- },
2348
- git_provider: {
2349
- type: 'string',
2350
- description: 'Updated git provider',
2351
- },
2352
- git_link: {
2353
- type: 'string',
2354
- description: 'Updated git repository URL',
2355
- },
2356
- staging_url: {
2357
- type: 'string',
2358
- description: 'Updated staging URL',
2359
- },
2360
- hosting_provider: {
2361
- type: 'string',
2362
- description: 'Updated hosting provider',
2363
- },
2364
- fru_hosted: {
2365
- type: 'boolean',
2366
- description: 'Updated FruCloud hosting flag',
2367
- },
2368
- k8s_cluster: {
2369
- type: 'string',
2370
- description: 'Updated K8s cluster',
2371
- },
2372
- k8s_namespace: {
2373
- type: 'string',
2374
- description: 'Updated K8s namespace',
2375
- },
2376
- environment: {
2377
- type: 'string',
2378
- description: 'Updated environment: production, staging, development',
2379
- },
2380
- lead_developer: {
2381
- type: 'number',
2382
- description: 'Updated lead developer user ID',
2383
- },
2384
- frucare_site: {
2385
- type: 'boolean',
2386
- description: 'Updated FruCare maintenance flag',
2387
- },
2388
- backup_enabled: {
2389
- type: 'boolean',
2390
- description: 'Updated backup enabled flag',
2391
- },
2392
- backup_schedule: {
2393
- type: 'string',
2394
- description: 'Updated backup cron schedule',
2395
- },
2396
- backup_excluded: {
2397
- type: 'boolean',
2398
- description: 'Exclude site from backup eligibility (e.g. ephemeral sites, non-K8s hosting)',
2399
- },
2400
- backup_exclusion_reason: {
2401
- type: 'string',
2402
- description: 'Reason for backup exclusion (e.g. "Ephemeral captive portal site", "External hosting - not on K8s")',
2403
- },
2404
- multisite_root_id: {
2405
- type: 'number',
2406
- description: 'Root site ID for WordPress multisite networks. Set to link as a subsite, null to unlink.',
2407
- },
2408
- uptime_monitor_id: {
2409
- type: 'number',
2410
- description: 'UptimeRobot monitor ID for this site. Used by scheduler to pause/resume monitors on scale events.',
2411
- },
2412
- pvc_name: {
2413
- type: 'string',
2414
- description: 'PVC name in the K8s namespace (e.g., "public-files-nfs"). Update after RWO→RWX migration so the snapshot scheduler targets the new volume.',
2415
- },
2416
- pvc_storage_class: {
2417
- type: 'string',
2418
- description: 'PVC storage class (e.g., "openebs-kernel-nfs", "do-block-storage-xfs-retain"). Update after a storage migration.',
2419
- },
2420
- pvc_size: {
2421
- type: 'string',
2422
- description: 'PVC size with unit (e.g., "10Gi"). Update after expanding the volume.',
2423
- },
2424
- pvc_mount_path: {
2425
- type: 'string',
2426
- description: 'Where the PVC is mounted inside the container (e.g., "/var/www/html/wp-content/uploads").',
2427
- },
2428
- pvc_sidecar_detected: {
2429
- type: 'boolean',
2430
- description: 'Whether a backup sidecar was auto-detected on this PVC.',
2431
- },
2432
- },
2433
- required: ['website_id'],
2434
- },
2435
- },
2436
- {
2437
- name: 'fcp_delete_site',
2438
- description: 'Soft-delete (retire) a website. Sets retired flag and preserves all data. Admin only.',
2439
- inputSchema: {
2440
- type: 'object',
2441
- properties: {
2442
- website_id: {
2443
- type: 'number',
2444
- description: 'The website ID to retire (required)',
2445
- },
2446
- reason: {
2447
- type: 'string',
2448
- description: 'Reason for retiring the site',
2449
- },
2450
- forward_url: {
2451
- type: 'string',
2452
- description: 'URL to forward the domain to after retirement',
2453
- },
2454
- },
2455
- required: ['website_id'],
2456
- },
2457
- },
2458
- // Fruition Shield - Shared WAF Gateway
2459
- {
2460
- name: 'fcp_shield_list_domains',
2461
- description: 'List all domains protected by Fruition Shield WAF. Filter by status (pending_dns, pending_cert, active, suspended) or account_id.',
2462
- inputSchema: {
2463
- type: 'object',
2464
- properties: {
2465
- status: {
2466
- type: 'string',
2467
- description: 'Filter by status: pending_dns, pending_cert, cert_validating, active, suspended, removed',
2468
- },
2469
- account_id: {
2470
- type: 'number',
2471
- description: 'Filter by account/client ID',
2472
- },
2473
- },
2474
- },
2475
- },
2476
- {
2477
- name: 'fcp_shield_add_domain',
2478
- description: 'Add a domain to Fruition Shield WAF protection. Returns DNS records (traffic CNAME + cert validation CNAME) that must be added at the registrar.',
2479
- inputSchema: {
2480
- type: 'object',
2481
- properties: {
2482
- domain: {
2483
- type: 'string',
2484
- description: 'Domain to protect (e.g., www.butterflies.org)',
2485
- },
2486
- origin_host: {
2487
- type: 'string',
2488
- description: 'Origin server IP or hostname (e.g., 146.190.2.169 for K8s prod)',
2489
- },
2490
- origin_port: {
2491
- type: 'number',
2492
- description: 'Origin port (default: 443)',
2493
- },
2494
- origin_protocol: {
2495
- type: 'string',
2496
- description: 'Origin protocol: http or https (default: https)',
2497
- },
2498
- website_id: {
2499
- type: 'number',
2500
- description: 'Link to existing FCP website ID',
2501
- },
2502
- account_id: {
2503
- type: 'number',
2504
- description: 'Associated account/client ID',
2505
- },
2506
- cache_profile: {
2507
- type: 'string',
2508
- description: 'Cache profile: standard, aggressive, minimal, none (default: standard)',
2509
- },
2510
- notes: {
2511
- type: 'string',
2512
- description: 'Notes about this domain',
2513
- },
2514
- },
2515
- required: ['domain', 'origin_host'],
2516
- },
2517
- },
2518
- {
2519
- name: 'fcp_shield_get_domain',
2520
- description: 'Get Shield domain details including onboarding status, DNS records needed, and WAF configuration.',
2521
- inputSchema: {
2522
- type: 'object',
2523
- properties: {
2524
- domain_id: {
2525
- type: 'number',
2526
- description: 'The Shield domain ID',
2527
- },
2528
- },
2529
- required: ['domain_id'],
2530
- },
2531
- },
2532
- {
2533
- name: 'fcp_shield_update_domain',
2534
- description: 'Update Shield domain configuration (origin, cache profile, WAF rules, geo-blocking, rate limiting).',
2535
- inputSchema: {
2536
- type: 'object',
2537
- properties: {
2538
- domain_id: {
2539
- type: 'number',
2540
- description: 'The Shield domain ID',
2541
- },
2542
- origin_host: { type: 'string', description: 'Updated origin host' },
2543
- origin_port: { type: 'number', description: 'Updated origin port' },
2544
- cache_profile: { type: 'string', description: 'Cache profile: standard, aggressive, minimal, none' },
2545
- waf_profile: { type: 'string', description: 'WAF profile: standard, premium, custom' },
2546
- geo_block_countries: {
2547
- type: 'array',
2548
- items: { type: 'string' },
2549
- description: 'Country codes to block (e.g., ["CN","RU","KP","IR","BY"])',
2550
- },
2551
- rate_limit_tier: { type: 'string', description: 'Rate limit: standard, strict, relaxed' },
2552
- bot_control_enabled: { type: 'boolean', description: 'Enable AWS Bot Control (+$10/mo)' },
2553
- cache_no_cache_paths: {
2554
- type: 'array',
2555
- items: { type: 'string' },
2556
- description: 'Path patterns to never cache (e.g., ["/api/*", "/wp-admin/*", "/cart/*"])',
2557
- },
2558
- enabled: { type: 'boolean', description: 'Enable/disable domain' },
2559
- notes: { type: 'string', description: 'Updated notes' },
2560
- },
2561
- required: ['domain_id'],
2562
- },
2563
- },
2564
- {
2565
- name: 'fcp_shield_remove_domain',
2566
- description: 'Remove a domain from Fruition Shield protection. Removes from CloudFront, DynamoDB, and deletes ACM certificate.',
2567
- inputSchema: {
2568
- type: 'object',
2569
- properties: {
2570
- domain_id: {
2571
- type: 'number',
2572
- description: 'The Shield domain ID to remove',
2573
- },
2574
- },
2575
- required: ['domain_id'],
2576
- },
2577
- },
2578
- {
2579
- name: 'fcp_shield_get_metrics',
2580
- description: 'Get aggregate Fruition Shield WAF metrics: total domains, requests, blocks, block rate, and infrastructure health.',
2581
- inputSchema: {
2582
- type: 'object',
2583
- properties: {},
2584
- },
2585
- },
2586
- // ============================================================================
2587
- // Backup Management Tools
2588
- // ============================================================================
2589
- {
2590
- name: 'fcp_backup_list_sites',
2591
- description: 'List websites with backups enabled. Returns site details including domain, cluster, namespace, and backup schedule.',
2592
- inputSchema: {
2593
- type: 'object',
2594
- properties: {},
2595
- },
2596
- },
2597
- {
2598
- name: 'fcp_backup_get_config',
2599
- description: 'Get S3 backup configuration. Admin only. Returns bucket, region, endpoint info (credentials are excluded).',
2600
- inputSchema: {
2601
- type: 'object',
2602
- properties: {},
2603
- },
2604
- },
2605
- {
2606
- name: 'fcp_backup_list_eligible',
2607
- description: 'List sites eligible for backup but not yet enabled. Returns sites grouped by environment (production, staging, development).',
2608
- inputSchema: {
2609
- type: 'object',
2610
- properties: {},
2611
- },
2612
- },
2613
- {
2614
- name: 'fcp_backup_enable',
2615
- description: 'Enable backup for a site and trigger the first backup automatically.',
2616
- inputSchema: {
2617
- type: 'object',
2618
- properties: {
2619
- siteId: {
2620
- type: 'number',
2621
- description: 'The website ID to enable backups for',
2622
- },
2623
- },
2624
- required: ['siteId'],
2625
- },
2626
- },
2627
- {
2628
- name: 'fcp_backup_trigger',
2629
- description: 'Manually trigger a backup for a website. Admin only. Creates a queued backup job.',
2630
- inputSchema: {
2631
- type: 'object',
2632
- properties: {
2633
- websiteId: {
2634
- type: 'number',
2635
- description: 'The website ID to backup',
2636
- },
2637
- triggerType: {
2638
- type: 'string',
2639
- description: 'Trigger type (default: manual)',
2640
- },
2641
- },
2642
- required: ['websiteId'],
2643
- },
2644
- },
2645
- {
2646
- name: 'fcp_backup_check_trigger',
2647
- description: 'Check if a backup can be triggered for a website. Returns hosting status, K8s config, enabled status, and queue length.',
2648
- inputSchema: {
2649
- type: 'object',
2650
- properties: {
2651
- websiteId: {
2652
- type: 'number',
2653
- description: 'The website ID to check',
2654
- },
2655
- },
2656
- required: ['websiteId'],
2657
- },
2658
- },
2659
- {
2660
- name: 'fcp_backup_list_backups',
2661
- description: 'List backup history for a site. Returns backup jobs with status, size, duration, and location.',
2662
- inputSchema: {
2663
- type: 'object',
2664
- properties: {
2665
- siteId: {
2666
- type: 'number',
2667
- description: 'The website ID (will be formatted as site-{id} for the API)',
2668
- },
2669
- },
2670
- required: ['siteId'],
2671
- },
2672
- },
2673
- {
2674
- name: 'fcp_backup_get_status',
2675
- description: 'Get backup job status. Provide backupId for a specific job, or websiteId for backup history of a site.',
2676
- inputSchema: {
2677
- type: 'object',
2678
- properties: {
2679
- backupId: {
2680
- type: 'string',
2681
- description: 'Specific backup job ID to get status for',
2682
- },
2683
- websiteId: {
2684
- type: 'number',
2685
- description: 'Website ID to get backup history for',
2686
- },
2687
- },
2688
- },
2689
- },
2690
- {
2691
- name: 'fcp_backup_download',
2692
- description: 'Generate a presigned S3 download URL for a completed backup. URL expires in 1 hour.',
2693
- inputSchema: {
2694
- type: 'object',
2695
- properties: {
2696
- backupId: {
2697
- type: 'string',
2698
- description: 'The backup ID to download',
2699
- },
2700
- },
2701
- required: ['backupId'],
2702
- },
2703
- },
2704
- {
2705
- name: 'fcp_backup_download_prepared',
2706
- description: 'Prepare a backup download with optional sanitization. For production backups, sanitization is the default — this returns a jobId to poll via fcp_backup_sanitize_status. For non-production backups, returns a presigned download URL directly.',
2707
- inputSchema: {
2708
- type: 'object',
2709
- properties: {
2710
- siteId: {
2711
- type: 'string',
2712
- description: 'The site ID (e.g., site-123)',
2713
- },
2714
- backupId: {
2715
- type: 'string',
2716
- description: 'The backup ID to download',
2717
- },
2718
- downloadType: {
2719
- type: 'string',
2720
- description: 'Type of download (e.g., database, files)',
2721
- },
2722
- format: {
2723
- type: 'string',
2724
- description: 'Download format (optional)',
2725
- },
2726
- sanitize: {
2727
- type: 'boolean',
2728
- description: 'Whether to sanitize production data. Defaults to true for production environments. Set to false to skip sanitization (non-production only).',
2729
- },
2730
- },
2731
- required: ['siteId', 'backupId', 'downloadType'],
2732
- },
2733
- },
2734
- {
2735
- name: 'fcp_backup_sanitize_status',
2736
- description: 'Check the status of a database sanitization job. Poll this after fcp_backup_download_prepared returns a jobId. When status is "completed", the response includes a presigned downloadUrl for the sanitized backup.',
2737
- inputSchema: {
2738
- type: 'object',
2739
- properties: {
2740
- jobId: {
2741
- type: 'string',
2742
- description: 'The sanitization job ID returned by fcp_backup_download_prepared',
2743
- },
2744
- },
2745
- required: ['jobId'],
2746
- },
2747
- },
2748
- {
2749
- name: 'fcp_backup_list_pairings',
2750
- description: 'List site pairings for backup management. Optionally filter by site ID. Shows production/staging/dev relationships grouped by account.',
2751
- inputSchema: {
2752
- type: 'object',
2753
- properties: {
2754
- siteId: {
2755
- type: 'number',
2756
- description: 'Optional: specific site ID to get pairing for',
2757
- },
2758
- },
2759
- },
2760
- },
2761
- {
2762
- name: 'fcp_backup_update_pairing',
2763
- description: 'Create or update a site pairing configuration. Defines staging/development relationships for a site.',
2764
- inputSchema: {
2765
- type: 'object',
2766
- properties: {
2767
- siteId: {
2768
- type: 'number',
2769
- description: 'The site ID to update pairing for',
2770
- },
2771
- pairingConfig: {
2772
- type: 'object',
2773
- description: 'Pairing configuration with staging (string|null) and development (array) site references',
2774
- properties: {
2775
- staging: { type: 'string', description: 'Staging site domain or null' },
2776
- development: {
2777
- type: 'array',
2778
- items: { type: 'string' },
2779
- description: 'Development site domains',
2780
- },
2781
- },
2782
- },
2783
- },
2784
- required: ['siteId', 'pairingConfig'],
2785
- },
2786
- },
2787
- {
2788
- name: 'fcp_backup_delete_pairing',
2789
- description: 'Remove a site pairing configuration. Clears the pairing config for a site.',
2790
- inputSchema: {
2791
- type: 'object',
2792
- properties: {
2793
- siteId: {
2794
- type: 'number',
2795
- description: 'The site ID to remove pairing for',
2796
- },
2797
- },
2798
- required: ['siteId'],
2799
- },
2800
- },
2801
- // Kinsta backup tools (separate S3 bucket with weekly full-site zip backups)
2802
- {
2803
- name: 'fcp_kinsta_backup_list_sites',
2804
- description: 'List all Kinsta-hosted sites that have S3 backup mappings configured. Returns site IDs, domains, and S3 prefixes.',
2805
- inputSchema: {
2806
- type: 'object',
2807
- properties: {},
2808
- },
2809
- },
2810
- {
2811
- name: 'fcp_kinsta_backup_list',
2812
- description: 'List available Kinsta backups for a specific site. Returns backup files with sizes, dates, and S3 keys for downloading.',
2813
- inputSchema: {
2814
- type: 'object',
2815
- properties: {
2816
- siteId: {
2817
- type: 'string',
2818
- description: 'The site ID (e.g., site-215 for bnpassociates.com)',
2819
- },
2820
- },
2821
- required: ['siteId'],
2822
- },
2823
- },
2824
- {
2825
- name: 'fcp_kinsta_backup_download',
2826
- description: 'Generate a presigned S3 download URL for a Kinsta backup zip file. URL expires in 1 hour.',
2827
- inputSchema: {
2828
- type: 'object',
2829
- properties: {
2830
- s3Key: {
2831
- type: 'string',
2832
- description: 'The S3 key from fcp_kinsta_backup_list (e.g., bnpassociates/filename.zip)',
2833
- },
2834
- },
2835
- required: ['s3Key'],
2836
- },
2837
- },
2838
- // Clone to Staging (unified prod → staging with files + database + search/replace)
2839
- {
2840
- name: 'fcp_clone_to_staging',
2841
- description: 'Clone production to staging environment. Combines file sync (from DO snapshots) and database sync (from S3 backups pulled from RO replicas) into a single tracked operation. Requires a confirmation token from fcp_clone_confirm first, unless triggerType is "mcp".',
2842
- inputSchema: {
2843
- type: 'object',
2844
- properties: {
2845
- productionSiteId: {
2846
- type: 'number',
2847
- description: 'Production site ID',
2848
- },
2849
- stagingSiteId: {
2850
- type: 'number',
2851
- description: 'Staging site ID to clone into',
2852
- },
2853
- includeFiles: {
2854
- type: 'boolean',
2855
- description: 'Whether to sync files from production snapshot (default: true)',
2856
- },
2857
- includeDatabase: {
2858
- type: 'boolean',
2859
- description: 'Whether to sync database from latest backup (default: true)',
2860
- },
2861
- runSearchReplace: {
2862
- type: 'boolean',
2863
- description: 'Whether to run domain search/replace after DB sync (default: true)',
2864
- },
2865
- backupId: {
2866
- type: 'string',
2867
- description: 'Specific backup ID to restore from (optional, defaults to latest)',
2868
- },
2869
- },
2870
- required: ['productionSiteId', 'stagingSiteId'],
2871
- },
2872
- },
2873
- {
2874
- name: 'fcp_clone_confirm',
2875
- description: 'Generate a safety confirmation token for a clone-to-staging operation. Returns token (5-min TTL) plus site info and latest backup age.',
2876
- inputSchema: {
2877
- type: 'object',
2878
- properties: {
2879
- productionSiteId: {
2880
- type: 'number',
2881
- description: 'Production site ID',
2882
- },
2883
- stagingSiteId: {
2884
- type: 'number',
2885
- description: 'Staging site ID',
2886
- },
2887
- },
2888
- required: ['productionSiteId', 'stagingSiteId'],
2889
- },
2890
- },
2891
- {
2892
- name: 'fcp_clone_status',
2893
- description: 'Get the status of a clone-to-staging operation. Returns progress, step statuses (files, database, search/replace), and errors.',
2894
- inputSchema: {
2895
- type: 'object',
2896
- properties: {
2897
- cloneId: {
2898
- type: 'string',
2899
- description: 'Clone operation ID (e.g., clone_abc12345_lxyz)',
2900
- },
2901
- },
2902
- required: ['cloneId'],
2903
- },
2904
- },
2905
- {
2906
- name: 'fcp_clone_list',
2907
- description: 'List recent clone-to-staging operations for a site. Shows history of all clone operations with status.',
2908
- inputSchema: {
2909
- type: 'object',
2910
- properties: {
2911
- productionSiteId: {
2912
- type: 'number',
2913
- description: 'Filter by production site ID',
2914
- },
2915
- stagingSiteId: {
2916
- type: 'number',
2917
- description: 'Filter by staging site ID',
2918
- },
2919
- limit: {
2920
- type: 'number',
2921
- description: 'Max results (default: 20)',
2922
- },
2923
- },
2924
- },
2925
- },
2926
- {
2927
- name: 'fcp_get_dev_environment_info',
2928
- description: 'Get developer environment info for a site. Shows what capabilities are available (clone to staging, download database, file sync, local setup), current state (latest backup age, clone status), staging sites, and quick action URLs. Use this to help developers understand what they can do with a site.',
2929
- inputSchema: {
2930
- type: 'object',
2931
- properties: {
2932
- siteId: {
2933
- type: 'number',
2934
- description: 'The production site ID to get dev environment info for',
2935
- },
2936
- },
2937
- required: ['siteId'],
2938
- },
2939
- },
2940
- ];
2941
- // Register tool handlers
2942
- server.setRequestHandler(ListToolsRequestSchema, async () => {
2943
- return { tools: TOOLS };
2944
- });
2945
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
2946
- const { name, arguments: args } = request.params;
2947
- // Track tool call for session management
2948
- await sessionTracker.trackToolCall(name, `Called ${name}`);
2949
- try {
2950
- // Role-based access control: gate destructive / sensitive tools by RBAC role
2951
- // resolved from /api/auth/me. Throws on insufficient privilege; the catch
2952
- // block below converts the error into the standard MCP error response.
2953
- await enforceToolPermission(name);
2954
- switch (name) {
2955
- case 'fcp_list_launches': {
2956
- const result = await client.listLaunches(args);
2957
- return {
2958
- content: [
2959
- {
2960
- type: 'text',
2961
- text: JSON.stringify(result, null, 2),
2962
- },
2963
- ],
2964
- };
2965
- }
2966
- case 'fcp_get_launch': {
2967
- const { launch_id } = args;
2968
- const result = await client.getLaunch(launch_id);
2969
- return {
2970
- content: [
2971
- {
2972
- type: 'text',
2973
- text: JSON.stringify(result, null, 2),
2974
- },
2975
- ],
2976
- };
2977
- }
2978
- case 'fcp_get_legacy_access': {
2979
- const { launch_id } = args;
2980
- const result = await client.getLaunch(launch_id);
2981
- const legacyAccess = {
2982
- launch_id,
2983
- launch_name: result.launch.name,
2984
- launch_type: result.launch.launch_type,
2985
- legacy_domain: result.launch.legacy_domain,
2986
- legacy_access_info: result.launch.legacy_access_info || {},
2987
- };
2988
- return {
2989
- content: [
2990
- {
2991
- type: 'text',
2992
- text: JSON.stringify(legacyAccess, null, 2),
2993
- },
2994
- ],
2995
- };
2996
- }
2997
- case 'fcp_get_checklist': {
2998
- const { launch_id, category, status } = args;
2999
- const result = await client.getLaunch(launch_id);
3000
- let checklist = result.checklist || [];
3001
- if (category) {
3002
- checklist = checklist.filter((item) => item.category === category);
3003
- }
3004
- if (status) {
3005
- checklist = checklist.filter((item) => item.status === status);
3006
- }
3007
- // Format for readability
3008
- const formatted = checklist.map((item) => ({
3009
- id: item.id,
3010
- title: item.title,
3011
- status: item.status,
3012
- category: item.category,
3013
- is_blocker: item.is_blocker,
3014
- description: item.description,
3015
- claude_instructions: item.claude_instructions,
3016
- }));
3017
- return {
3018
- content: [
3019
- {
3020
- type: 'text',
3021
- text: JSON.stringify({
3022
- launch_id,
3023
- launch_name: result.launch.name,
3024
- total_items: formatted.length,
3025
- completed: formatted.filter((i) => i.status === 'completed').length,
3026
- blockers: formatted.filter((i) => i.is_blocker && i.status !== 'completed').length,
3027
- items: formatted,
3028
- }, null, 2),
3029
- },
3030
- ],
3031
- };
3032
- }
3033
- case 'fcp_add_checklist_item': {
3034
- const { launch_id, title, category, ...rest } = args;
3035
- const result = await client.createChecklistItem(launch_id, {
3036
- title,
3037
- category,
3038
- ...rest,
3039
- });
3040
- return {
3041
- content: [
3042
- {
3043
- type: 'text',
3044
- text: JSON.stringify({
3045
- success: true,
3046
- message: `Checklist item created: "${title}"`,
3047
- item: result,
3048
- }, null, 2),
3049
- },
3050
- ],
3051
- };
3052
- }
3053
- case 'fcp_update_checklist_item': {
3054
- const { launch_id, item_id, notes, ...updates } = args;
3055
- // Map 'notes' to 'completion_notes' for the API
3056
- const payload = { ...updates };
3057
- if (notes !== undefined) {
3058
- payload.completion_notes = notes;
3059
- }
3060
- const result = await client.updateChecklistItem(launch_id, item_id, payload);
3061
- return {
3062
- content: [
3063
- {
3064
- type: 'text',
3065
- text: JSON.stringify({
3066
- success: true,
3067
- message: `Checklist item ${item_id} updated`,
3068
- item: result,
3069
- }, null, 2),
3070
- },
3071
- ],
3072
- };
3073
- }
3074
- case 'fcp_delete_checklist_item': {
3075
- const { launch_id, item_id } = args;
3076
- await client.deleteChecklistItem(launch_id, item_id);
3077
- return {
3078
- content: [
3079
- {
3080
- type: 'text',
3081
- text: JSON.stringify({
3082
- success: true,
3083
- message: `Checklist item ${item_id} deleted from launch ${launch_id}`,
3084
- }, null, 2),
3085
- },
3086
- ],
3087
- };
3088
- }
3089
- case 'fcp_add_progress_note': {
3090
- const { launch_id, content } = args;
3091
- const result = await client.addNote(launch_id, content);
3092
- return {
3093
- content: [
3094
- {
3095
- type: 'text',
3096
- text: JSON.stringify({
3097
- success: true,
3098
- message: 'Progress note added',
3099
- note: result,
3100
- }, null, 2),
3101
- },
3102
- ],
3103
- };
3104
- }
3105
- case 'fcp_get_claude_md': {
3106
- const { launch_id } = args;
3107
- const result = await client.getClaudeMd(launch_id);
3108
- return {
3109
- content: [
3110
- {
3111
- type: 'text',
3112
- text: result.content,
3113
- },
3114
- ],
3115
- };
3116
- }
3117
- // Nuclei Security Scanning Handlers
3118
- case 'fcp_trigger_nuclei_scan': {
3119
- const { website_id, url, severity, templates } = args;
3120
- const result = await client.triggerNucleiScan(website_id, { url, severity, templates });
3121
- return {
3122
- content: [
3123
- {
3124
- type: 'text',
3125
- text: JSON.stringify(result, null, 2),
3126
- },
3127
- ],
3128
- };
3129
- }
3130
- case 'fcp_get_nuclei_results': {
3131
- const { website_id, scan_id, limit, status } = args;
3132
- const result = await client.getNucleiResults(website_id, { scan_id, limit, status });
3133
- return {
3134
- content: [
3135
- {
3136
- type: 'text',
3137
- text: JSON.stringify(result, null, 2),
3138
- },
3139
- ],
3140
- };
3141
- }
3142
- // Security Headers Handlers
3143
- case 'fcp_scan_security_headers': {
3144
- const { website_id } = args;
3145
- const result = await client.scanSecurityHeaders(website_id);
3146
- return {
3147
- content: [
3148
- {
3149
- type: 'text',
3150
- text: JSON.stringify(result, null, 2),
3151
- },
3152
- ],
3153
- };
3154
- }
3155
- case 'fcp_get_security_headers_results': {
3156
- const { website_id, scan_id } = args;
3157
- const result = await client.getSecurityHeadersResults(website_id, scan_id);
3158
- return {
3159
- content: [
3160
- {
3161
- type: 'text',
3162
- text: JSON.stringify(result, null, 2),
3163
- },
3164
- ],
3165
- };
3166
- }
3167
- // Site/Website CRUD Handlers
3168
- case 'fcp_list_sites': {
3169
- const filters = args;
3170
- const result = await client.listSites(filters);
3171
- return {
3172
- content: [
3173
- {
3174
- type: 'text',
3175
- text: JSON.stringify(result, null, 2),
3176
- },
3177
- ],
3178
- };
3179
- }
3180
- case 'fcp_search_sites': {
3181
- const { query, limit } = args;
3182
- const result = await client.searchSites(query, limit);
3183
- return {
3184
- content: [
3185
- {
3186
- type: 'text',
3187
- text: JSON.stringify(result, null, 2),
3188
- },
3189
- ],
3190
- };
3191
- }
3192
- case 'fcp_get_site': {
3193
- const { website_id } = args;
3194
- const result = await client.getSite(website_id);
3195
- return {
3196
- content: [
3197
- {
3198
- type: 'text',
3199
- text: JSON.stringify(result, null, 2),
3200
- },
3201
- ],
3202
- };
3203
- }
3204
- case 'fcp_get_local_setup_guide': {
3205
- const { website_id } = args;
3206
- const result = await client.getLocalSetupGuide(website_id);
3207
- // Return the guide as readable text, with structured data as JSON
3208
- const guide = result.guide || '';
3209
- const siteInfo = result.site || {};
3210
- const ddevInfo = result.ddev || {};
3211
- return {
3212
- content: [
3213
- {
3214
- type: 'text',
3215
- text: guide + '\n\n---\n\n**Structured Data:**\n```json\n' + JSON.stringify({ site: siteInfo, ddev: ddevInfo }, null, 2) + '\n```',
3216
- },
3217
- ],
3218
- };
3219
- }
3220
- case 'fcp_create_site': {
3221
- const { account_id, domain, cms, url_full, git_provider, git_link, hosting_provider, fru_hosted, k8s_cluster, k8s_namespace, staging, } = args;
3222
- const result = await client.createSite({ account_id, domain, cms, url_full, git_provider, git_link, hosting_provider, fru_hosted, k8s_cluster, k8s_namespace }, staging);
3223
- return {
3224
- content: [
3225
- {
3226
- type: 'text',
3227
- text: JSON.stringify(result, null, 2),
3228
- },
3229
- ],
3230
- };
3231
- }
3232
- case 'fcp_update_site': {
3233
- const { website_id, ...updates } = args;
3234
- const result = await client.updateSite(website_id, updates);
3235
- return {
3236
- content: [
3237
- {
3238
- type: 'text',
3239
- text: JSON.stringify(result, null, 2),
3240
- },
3241
- ],
3242
- };
3243
- }
3244
- case 'fcp_delete_site': {
3245
- const { website_id, reason, forward_url } = args;
3246
- const result = await client.deleteSite(website_id, { reason, forward_url });
3247
- return {
3248
- content: [
3249
- {
3250
- type: 'text',
3251
- text: JSON.stringify(result, null, 2),
3252
- },
3253
- ],
3254
- };
3255
- }
3256
- // Launch CRUD Handlers
3257
- case 'fcp_create_launch': {
3258
- const { name, platform, launch_type, target_launch_date, ...rest } = args;
3259
- const result = await client.createLaunch({
3260
- name,
3261
- platform,
3262
- launch_type,
3263
- target_launch_date,
3264
- ...rest,
3265
- });
3266
- return {
3267
- content: [
3268
- {
3269
- type: 'text',
3270
- text: JSON.stringify({
3271
- success: true,
3272
- message: `Launch created: "${name}" (ID: ${result.launch.id})`,
3273
- launch: result.launch,
3274
- }, null, 2),
3275
- },
3276
- ],
3277
- };
3278
- }
3279
- case 'fcp_update_launch': {
3280
- const { launch_id, ...updates } = args;
3281
- const result = await client.updateLaunch(launch_id, updates);
3282
- return {
3283
- content: [
3284
- {
3285
- type: 'text',
3286
- text: JSON.stringify({
3287
- success: true,
3288
- message: `Launch ${launch_id} updated`,
3289
- launch: result.launch || result,
3290
- }, null, 2),
3291
- },
3292
- ],
3293
- };
3294
- }
3295
- case 'fcp_delete_launch': {
3296
- const { launch_id } = args;
3297
- await client.deleteLaunch(launch_id);
3298
- return {
3299
- content: [
3300
- {
3301
- type: 'text',
3302
- text: JSON.stringify({
3303
- success: true,
3304
- message: `Launch ${launch_id} deleted`,
3305
- }, null, 2),
3306
- },
3307
- ],
3308
- };
3309
- }
3310
- // Validation / Success Factor Handlers
3311
- case 'fcp_get_success_factors': {
3312
- const { launch_id, item_id } = args;
3313
- const result = await client.getSuccessFactors(launch_id, item_id);
3314
- return {
3315
- content: [
3316
- {
3317
- type: 'text',
3318
- text: JSON.stringify({
3319
- checklist_item_id: item_id,
3320
- total: result.factors?.length || 0,
3321
- factors: result.factors || [],
3322
- }, null, 2),
3323
- },
3324
- ],
3325
- };
3326
- }
3327
- case 'fcp_validate_checklist_item': {
3328
- const { launch_id, item_id } = args;
3329
- const result = await client.validateChecklistItem(launch_id, item_id);
3330
- return {
3331
- content: [
3332
- {
3333
- type: 'text',
3334
- text: JSON.stringify(result, null, 2),
3335
- },
3336
- ],
3337
- };
3338
- }
3339
- case 'fcp_get_unroo_section': {
3340
- const { repo_url, project_key } = args;
3341
- // Use provided repo_url or detect from current directory
3342
- const repoUrlToUse = repo_url || detectGitRemote();
3343
- if (!repoUrlToUse && !project_key) {
3344
- return {
3345
- content: [
3346
- {
3347
- type: 'text',
3348
- text: JSON.stringify({
3349
- error: 'Could not detect git remote and no project_key provided. Either run this from a git repository or provide a project_key.',
3350
- }, null, 2),
3351
- },
3352
- ],
3353
- isError: true,
3354
- };
3355
- }
3356
- // Build query params
3357
- const sectionParams = new URLSearchParams();
3358
- if (repoUrlToUse)
3359
- sectionParams.set('repo_url', repoUrlToUse);
3360
- if (project_key)
3361
- sectionParams.set('project_key', project_key);
3362
- // Call FCP API to get the section
3363
- const sectionUrl = `${FCP_API_URL}/api/mcp/claude-md-section?${sectionParams}`;
3364
- const sectionHeaders = {
3365
- 'Content-Type': 'application/json',
3366
- };
3367
- if (FCP_API_TOKEN && FCP_API_TOKEN !== 'dev_bypass') {
3368
- sectionHeaders['X-API-Key'] = FCP_API_TOKEN;
3369
- }
3370
- else if (FCP_API_TOKEN === 'dev_bypass') {
3371
- sectionHeaders['X-Dev-Bypass'] = 'true';
3372
- }
3373
- const response = await fetch(sectionUrl, { headers: sectionHeaders });
3374
- const data = await response.json();
3375
- if (!response.ok) {
3376
- return {
3377
- content: [
3378
- {
3379
- type: 'text',
3380
- text: JSON.stringify({ error: data.error || 'Failed to get section' }, null, 2),
3381
- },
3382
- ],
3383
- isError: true,
3384
- };
3385
- }
3386
- // Return the markdown content with metadata
3387
- return {
3388
- content: [
3389
- {
3390
- type: 'text',
3391
- text: `Project Key: ${data.project_key}\nSource: ${data.source}\n${data.warning ? `Warning: ${data.warning}\n` : ''}\n---\n\n${data.content}`,
3392
- },
3393
- ],
3394
- };
3395
- }
3396
- // Unroo Task Management Handlers
3397
- case 'unroo_list_projects': {
3398
- const result = await unrooClient.listProjects();
3399
- return {
3400
- content: [
3401
- {
3402
- type: 'text',
3403
- text: JSON.stringify(result, null, 2),
3404
- },
3405
- ],
3406
- };
3407
- }
3408
- case 'unroo_list_tasks': {
3409
- const filters = args;
3410
- const result = await unrooClient.listTasks(filters);
3411
- return {
3412
- content: [
3413
- {
3414
- type: 'text',
3415
- text: JSON.stringify({
3416
- total: result.summary?.total || result.tasks.length,
3417
- by_status: result.summary?.by_status || {},
3418
- tasks: result.tasks,
3419
- }, null, 2),
3420
- },
3421
- ],
3422
- };
3423
- }
3424
- case 'unroo_create_task': {
3425
- const taskInput = args;
3426
- const result = await unrooClient.createTask({
3427
- ...taskInput,
3428
- external_source_type: 'claude_code',
3429
- });
3430
- return {
3431
- content: [
3432
- {
3433
- type: 'text',
3434
- text: JSON.stringify({
3435
- success: true,
3436
- message: `Task created: ${result.task.id}`,
3437
- task: result.task,
3438
- }, null, 2),
3439
- },
3440
- ],
3441
- };
3442
- }
3443
- case 'unroo_get_task': {
3444
- const { task_id } = args;
3445
- const result = await unrooClient.getTask(task_id);
3446
- return {
3447
- content: [
3448
- {
3449
- type: 'text',
3450
- text: JSON.stringify({
3451
- success: true,
3452
- task: result.task,
3453
- }, null, 2),
3454
- },
3455
- ],
3456
- };
3457
- }
3458
- case 'unroo_update_task': {
3459
- const { task_id, ...updates } = args;
3460
- const result = await unrooClient.updateTask(task_id, updates);
3461
- return {
3462
- content: [
3463
- {
3464
- type: 'text',
3465
- text: JSON.stringify({
3466
- success: true,
3467
- message: `Task ${task_id} updated`,
3468
- task: result.task,
3469
- }, null, 2),
3470
- },
3471
- ],
3472
- };
3473
- }
3474
- case 'unroo_add_comment': {
3475
- const { task_id, comment_text, is_internal, mentions } = args;
3476
- const commentResult = await unrooClient.addComment(task_id, {
3477
- comment_text,
3478
- is_internal,
3479
- mentions,
3480
- });
3481
- return {
3482
- content: [
3483
- {
3484
- type: 'text',
3485
- text: JSON.stringify({
3486
- success: true,
3487
- message: `Comment added to task ${task_id}${is_internal ? ' (internal)' : ''}`,
3488
- comment: commentResult.comment,
3489
- }, null, 2),
3490
- },
3491
- ],
3492
- };
3493
- }
3494
- case 'unroo_list_comments': {
3495
- const { task_id } = args;
3496
- const commentsResult = await unrooClient.listComments(task_id);
3497
- return {
3498
- content: [
3499
- {
3500
- type: 'text',
3501
- text: JSON.stringify({
3502
- total: commentsResult.total,
3503
- comments: commentsResult.comments,
3504
- }, null, 2),
3505
- },
3506
- ],
3507
- };
3508
- }
3509
- case 'unroo_get_my_tasks': {
3510
- const { status, limit } = args;
3511
- // Note: The API key determines the user, tasks are filtered server-side
3512
- const result = await unrooClient.listTasks({
3513
- status,
3514
- limit: limit || 50,
3515
- });
3516
- return {
3517
- content: [
3518
- {
3519
- type: 'text',
3520
- text: JSON.stringify({
3521
- total: result.tasks.length,
3522
- tasks: result.tasks,
3523
- }, null, 2),
3524
- },
3525
- ],
3526
- };
3527
- }
3528
- case 'unroo_start_session': {
3529
- const sessionInput = args;
3530
- // Update session tracker with task context
3531
- if (sessionInput.task_id) {
3532
- sessionTracker.setCurrentTask(sessionInput.task_id);
3533
- }
3534
- const result = await unrooClient.startSession({
3535
- ...sessionInput,
3536
- source: 'claude-code-mcp',
3537
- });
3538
- return {
3539
- content: [
3540
- {
3541
- type: 'text',
3542
- text: JSON.stringify({
3543
- success: true,
3544
- message: 'Work session started',
3545
- session: result.session,
3546
- auto_tracking: sessionTracker.getStats(),
3547
- }, null, 2),
3548
- },
3549
- ],
3550
- };
3551
- }
3552
- case 'unroo_end_session': {
3553
- // End the auto-tracked session (clears heartbeat interval, logs activity)
3554
- await sessionTracker.endSession();
3555
- // End session via API to get the final session state
3556
- // Note: If sessionTracker already ended it, Unroo returns the existing completed session
3557
- // (protected against duplicate work log creation server-side)
3558
- const result = await unrooClient.endSession({
3559
- machine_id: INSTANCE_ID,
3560
- repo_name: currentProject ? `${currentProject.github.owner}/${currentProject.github.repo}` : undefined,
3561
- });
3562
- return {
3563
- content: [
3564
- {
3565
- type: 'text',
3566
- text: JSON.stringify({
3567
- success: true,
3568
- message: `Session ended. Duration: ${result.session.duration_minutes} minutes`,
3569
- session: result.session,
3570
- }, null, 2),
3571
- },
3572
- ],
3573
- };
3574
- }
3575
- case 'unroo_create_follow_up': {
3576
- const { parent_task_id, title, description, priority } = args;
3577
- // Get parent task to inherit project_key
3578
- const parentResult = await unrooClient.getTask(parent_task_id);
3579
- if (!parentResult.task) {
3580
- throw new Error(`Parent task not found: ${parent_task_id}`);
3581
- }
3582
- const parentTask = parentResult.task;
3583
- const result = await unrooClient.createTask({
3584
- title: `[Follow-up] ${title}`,
3585
- description: description || `Follow-up from task: ${parentTask.title}`,
3586
- project_key: parentTask.project_key || 'FCP',
3587
- priority: priority || parentTask.priority || 'medium',
3588
- parent_issue_id: parent_task_id,
3589
- external_source_type: 'claude_code_followup',
3590
- labels: ['follow-up'],
3591
- });
3592
- return {
3593
- content: [
3594
- {
3595
- type: 'text',
3596
- text: JSON.stringify({
3597
- success: true,
3598
- message: `Follow-up task created: ${result.task.id}`,
3599
- parent_task_id,
3600
- task: result.task,
3601
- }, null, 2),
3602
- },
3603
- ],
3604
- };
3605
- }
3606
- // Parking Lot / Backlog Handlers
3607
- case 'unroo_log_future_work': {
3608
- const input = args;
3609
- const result = await unrooClient.logFutureWork({
3610
- ...input,
3611
- discovered_by: 'claude-code-mcp',
3612
- });
3613
- return {
3614
- content: [
3615
- {
3616
- type: 'text',
3617
- text: JSON.stringify({
3618
- success: true,
3619
- message: result.message,
3620
- id: result.id,
3621
- destination: result.destination,
3622
- }, null, 2),
3623
- },
3624
- ],
3625
- };
3626
- }
3627
- case 'unroo_get_parking_lot': {
3628
- const { project_key, status, limit } = args;
3629
- const result = await unrooClient.getFutureWork({
3630
- project_key,
3631
- status: status || 'pending',
3632
- destination: 'parking_lot',
3633
- limit: limit || 100,
3634
- });
3635
- return {
3636
- content: [
3637
- {
3638
- type: 'text',
3639
- text: JSON.stringify({
3640
- success: true,
3641
- total: result.items.length,
3642
- stats: result.stats,
3643
- items: result.items,
3644
- }, null, 2),
3645
- },
3646
- ],
3647
- };
3648
- }
3649
- case 'unroo_get_backlog': {
3650
- const { project_key, priority, limit } = args;
3651
- const result = await unrooClient.getBacklog({
3652
- project_key,
3653
- priority,
3654
- limit: limit || 100,
3655
- });
3656
- return {
3657
- content: [
3658
- {
3659
- type: 'text',
3660
- text: JSON.stringify({
3661
- success: true,
3662
- total: result.total,
3663
- items: result.items,
3664
- }, null, 2),
3665
- },
3666
- ],
3667
- };
3668
- }
3669
- case 'unroo_convert_to_backlog': {
3670
- const { id, priority, notes } = args;
3671
- const result = await unrooClient.updateFutureWork(id, {
3672
- convert_to_backlog: true,
3673
- priority,
3674
- notes,
3675
- });
3676
- return {
3677
- content: [
3678
- {
3679
- type: 'text',
3680
- text: JSON.stringify({
3681
- success: true,
3682
- message: `Parking lot item ${id} converted to backlog`,
3683
- item: result.item,
3684
- }, null, 2),
3685
- },
3686
- ],
3687
- };
3688
- }
3689
- // FileSync Handlers
3690
- case 'fcp_filesync_list_configs': {
3691
- const filters = args;
3692
- const result = await client.listFileSyncConfigs(filters);
3693
- // If search is provided, filter client-side (API doesn't have text search)
3694
- let configs = result.data || [];
3695
- if (filters.search) {
3696
- const term = filters.search.toLowerCase();
3697
- configs = configs.filter((c) => (c.prod_namespace || '').toLowerCase().includes(term) ||
3698
- (c.staging_namespace || '').toLowerCase().includes(term) ||
3699
- (c.prod_cluster || '').toLowerCase().includes(term) ||
3700
- (c.staging_cluster || '').toLowerCase().includes(term) ||
3701
- (c.prod_pvc_name || '').toLowerCase().includes(term) ||
3702
- (c.staging_pvc_name || '').toLowerCase().includes(term));
3703
- }
3704
- return {
3705
- content: [
3706
- {
3707
- type: 'text',
3708
- text: JSON.stringify({
3709
- total: configs.length,
3710
- configs: configs.map((c) => ({
3711
- id: c.id,
3712
- prod_namespace: c.prod_namespace,
3713
- staging_namespace: c.staging_namespace,
3714
- prod_cluster: c.prod_cluster,
3715
- staging_cluster: c.staging_cluster,
3716
- sync_direction: c.sync_direction,
3717
- sync_method: c.sync_method,
3718
- enabled: c.enabled,
3719
- schedule_enabled: c.schedule_enabled,
3720
- schedule_cron: c.schedule_cron,
3721
- last_sync_at: c.last_sync_at,
3722
- last_sync_status: c.last_sync_status,
3723
- })),
3724
- }, null, 2),
3725
- },
3726
- ],
3727
- };
3728
- }
3729
- case 'fcp_filesync_get_config': {
3730
- const { config_id } = args;
3731
- // Get config from list (filtered by looking through all)
3732
- const configsResult = await client.listFileSyncConfigs();
3733
- const config = (configsResult.data || []).find((c) => c.id === config_id);
3734
- if (!config) {
3735
- throw new Error(`Config not found: ${config_id}`);
3736
- }
3737
- // Get recent jobs for this config
3738
- const jobsResult = await client.getFileSyncJobs({ config_id, limit: 5 });
3739
- return {
3740
- content: [
3741
- {
3742
- type: 'text',
3743
- text: JSON.stringify({
3744
- config,
3745
- recent_jobs: (jobsResult.data || []).map((j) => ({
3746
- id: j.id,
3747
- job_id: j.job_id,
3748
- status: j.status,
3749
- trigger_type: j.trigger_type,
3750
- progress_percent: j.progress_percent,
3751
- current_step: j.current_step,
3752
- started_at: j.started_at,
3753
- completed_at: j.completed_at,
3754
- duration_seconds: j.duration_seconds,
3755
- files_transferred: j.files_transferred,
3756
- bytes_transferred: j.bytes_transferred,
3757
- error_message: j.error_message,
3758
- })),
3759
- }, null, 2),
3760
- },
3761
- ],
3762
- };
3763
- }
3764
- case 'fcp_filesync_start_sync': {
3765
- const { config_id, confirmation_token } = args;
3766
- console.error(`[MCP] Starting file sync for config ${config_id}`);
3767
- const result = await client.startFileSync({
3768
- config_id,
3769
- trigger_type: 'api',
3770
- triggered_by: 'claude-code-mcp',
3771
- confirmation_token,
3772
- });
3773
- return {
3774
- content: [
3775
- {
3776
- type: 'text',
3777
- text: JSON.stringify({
3778
- success: result.success,
3779
- message: result.message,
3780
- job: result.data ? {
3781
- id: result.data.id,
3782
- job_id: result.data.job_id,
3783
- status: result.data.status,
3784
- current_step: result.data.current_step,
3785
- } : null,
3786
- }, null, 2),
3787
- },
3788
- ],
3789
- };
3790
- }
3791
- case 'fcp_filesync_get_job_status': {
3792
- const { config_id, status, limit } = args;
3793
- const result = await client.getFileSyncJobs({
3794
- config_id,
3795
- status,
3796
- limit: limit || 5,
3797
- });
3798
- return {
3799
- content: [
3800
- {
3801
- type: 'text',
3802
- text: JSON.stringify({
3803
- total: (result.data || []).length,
3804
- jobs: (result.data || []).map((j) => ({
3805
- id: j.id,
3806
- job_id: j.job_id,
3807
- status: j.status,
3808
- trigger_type: j.trigger_type,
3809
- progress_percent: j.progress_percent,
3810
- current_step: j.current_step,
3811
- started_at: j.started_at,
3812
- completed_at: j.completed_at,
3813
- duration_seconds: j.duration_seconds,
3814
- files_transferred: j.files_transferred,
3815
- files_deleted: j.files_deleted,
3816
- bytes_transferred: j.bytes_transferred,
3817
- error_message: j.error_message,
3818
- rsync_stats: j.rsync_stats,
3819
- })),
3820
- }, null, 2),
3821
- },
3822
- ],
3823
- };
3824
- }
3825
- case 'fcp_filesync_cancel_sync': {
3826
- const { job_id } = args;
3827
- console.error(`[MCP] Cancelling file sync job ${job_id}`);
3828
- const result = await client.cancelFileSync(job_id);
3829
- return {
3830
- content: [
3831
- {
3832
- type: 'text',
3833
- text: JSON.stringify({
3834
- success: result.success,
3835
- message: result.message,
3836
- }, null, 2),
3837
- },
3838
- ],
3839
- };
3840
- }
3841
- case 'fcp_filesync_get_confirmation': {
3842
- const { config_id } = args;
3843
- console.error(`[MCP] Getting confirmation token for config ${config_id}`);
3844
- const result = await client.getFileSyncConfirmation(config_id);
3845
- return {
3846
- content: [
3847
- {
3848
- type: 'text',
3849
- text: JSON.stringify({
3850
- success: result.success,
3851
- confirmation_token: result.data.token,
3852
- config_id: result.data.config_id,
3853
- expires_at: result.data.expires_at,
3854
- ttl_seconds: result.data.ttl_seconds,
3855
- warnings: result.data.warnings,
3856
- }, null, 2),
3857
- },
3858
- ],
3859
- };
3860
- }
3861
- // Fruition Shield Handlers
3862
- case 'fcp_shield_list_domains': {
3863
- const { status, account_id } = args;
3864
- const result = await client.shieldListDomains({ status, account_id });
3865
- return {
3866
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3867
- };
3868
- }
3869
- case 'fcp_shield_add_domain': {
3870
- const { domain, origin_host, origin_port, origin_protocol, website_id, account_id, cache_profile, notes } = args;
3871
- const result = await client.shieldAddDomain({
3872
- domain, origin_host, origin_port, origin_protocol, website_id, account_id, cache_profile, notes,
3873
- });
3874
- return {
3875
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3876
- };
3877
- }
3878
- case 'fcp_shield_get_domain': {
3879
- const { domain_id } = args;
3880
- const result = await client.shieldGetDomain(domain_id);
3881
- return {
3882
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3883
- };
3884
- }
3885
- case 'fcp_shield_update_domain': {
3886
- const { domain_id, ...updates } = args;
3887
- const result = await client.shieldUpdateDomain(domain_id, updates);
3888
- return {
3889
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3890
- };
3891
- }
3892
- case 'fcp_shield_remove_domain': {
3893
- const { domain_id } = args;
3894
- const result = await client.shieldRemoveDomain(domain_id);
3895
- return {
3896
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3897
- };
3898
- }
3899
- case 'fcp_shield_get_metrics': {
3900
- const result = await client.shieldGetMetrics();
3901
- return {
3902
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3903
- };
3904
- }
3905
- // ============================================================================
3906
- // Backup Management Handlers
3907
- // ============================================================================
3908
- case 'fcp_backup_list_sites': {
3909
- const result = await client.backupListSites();
3910
- return {
3911
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3912
- };
3913
- }
3914
- case 'fcp_backup_get_config': {
3915
- const result = await client.backupGetConfig();
3916
- return {
3917
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3918
- };
3919
- }
3920
- case 'fcp_backup_list_eligible': {
3921
- const result = await client.backupListEligible();
3922
- return {
3923
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3924
- };
3925
- }
3926
- case 'fcp_backup_enable': {
3927
- const { siteId } = args;
3928
- const result = await client.backupEnable(siteId);
3929
- return {
3930
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3931
- };
3932
- }
3933
- case 'fcp_backup_trigger': {
3934
- const { websiteId, triggerType } = args;
3935
- const result = await client.backupTrigger(websiteId, triggerType);
3936
- return {
3937
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3938
- };
3939
- }
3940
- case 'fcp_backup_check_trigger': {
3941
- const { websiteId } = args;
3942
- const result = await client.backupCheckTrigger(websiteId);
3943
- return {
3944
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3945
- };
3946
- }
3947
- case 'fcp_backup_list_backups': {
3948
- const { siteId } = args;
3949
- const result = await client.backupListBackups(`site-${siteId}`);
3950
- return {
3951
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3952
- };
3953
- }
3954
- case 'fcp_backup_get_status': {
3955
- const { backupId, websiteId } = args;
3956
- const result = await client.backupGetStatus({ backupId, websiteId });
3957
- return {
3958
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3959
- };
3960
- }
3961
- case 'fcp_backup_download': {
3962
- const { backupId } = args;
3963
- const result = await client.backupDownload(backupId);
3964
- return {
3965
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3966
- };
3967
- }
3968
- case 'fcp_backup_download_prepared': {
3969
- const { siteId, backupId, downloadType, format, sanitize } = args;
3970
- const result = await client.backupDownloadPrepared({ siteId, backupId, downloadType, format, sanitize });
3971
- return {
3972
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3973
- };
3974
- }
3975
- case 'fcp_backup_sanitize_status': {
3976
- const { jobId } = args;
3977
- const result = await client.backupSanitizeStatus(jobId);
3978
- return {
3979
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3980
- };
3981
- }
3982
- case 'fcp_backup_list_pairings': {
3983
- const { siteId } = args;
3984
- const result = await client.backupListPairings(siteId);
3985
- return {
3986
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3987
- };
3988
- }
3989
- case 'fcp_backup_update_pairing': {
3990
- const { siteId, pairingConfig } = args;
3991
- const result = await client.backupUpdatePairing(siteId, pairingConfig);
3992
- return {
3993
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3994
- };
3995
- }
3996
- case 'fcp_backup_delete_pairing': {
3997
- const { siteId } = args;
3998
- const result = await client.backupDeletePairing(siteId);
3999
- return {
4000
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4001
- };
4002
- }
4003
- // Kinsta backup tools
4004
- case 'fcp_kinsta_backup_list_sites': {
4005
- const result = await client.kinstaBackupListSites();
4006
- return {
4007
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4008
- };
4009
- }
4010
- case 'fcp_kinsta_backup_list': {
4011
- const { siteId } = args;
4012
- const result = await client.kinstaBackupListBackups(siteId);
4013
- return {
4014
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4015
- };
4016
- }
4017
- case 'fcp_kinsta_backup_download': {
4018
- const { s3Key } = args;
4019
- const result = await client.kinstaBackupDownload(s3Key);
4020
- return {
4021
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4022
- };
4023
- }
4024
- // Clone to Staging tools
4025
- case 'fcp_clone_to_staging': {
4026
- const { productionSiteId, stagingSiteId, includeFiles, includeDatabase, runSearchReplace, backupId } = args;
4027
- // For MCP callers, auto-generate confirmation token (bypasses type-to-confirm UI)
4028
- const confirmResult = await client.cloneToStagingConfirm(productionSiteId, stagingSiteId);
4029
- const token = confirmResult.token;
4030
- const result = await client.cloneToStaging({
4031
- productionSiteId,
4032
- stagingSiteId,
4033
- includeFiles: includeFiles !== false,
4034
- includeDatabase: includeDatabase !== false,
4035
- runSearchReplace: runSearchReplace !== false,
4036
- confirmationToken: token,
4037
- backupId,
4038
- });
4039
- return {
4040
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4041
- };
4042
- }
4043
- case 'fcp_clone_confirm': {
4044
- const { productionSiteId, stagingSiteId } = args;
4045
- const result = await client.cloneToStagingConfirm(productionSiteId, stagingSiteId);
4046
- return {
4047
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4048
- };
4049
- }
4050
- case 'fcp_clone_status': {
4051
- const { cloneId } = args;
4052
- const result = await client.getCloneStatus(cloneId);
4053
- return {
4054
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4055
- };
4056
- }
4057
- case 'fcp_clone_list': {
4058
- const { productionSiteId, stagingSiteId, limit } = args;
4059
- const result = await client.listCloneOperations({ productionSiteId, stagingSiteId, limit });
4060
- return {
4061
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4062
- };
4063
- }
4064
- case 'fcp_get_dev_environment_info': {
4065
- const { siteId } = args;
4066
- const result = await client.getDevEnvironmentInfo(siteId);
4067
- return {
4068
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
4069
- };
4070
- }
4071
- default:
4072
- throw new Error(`Unknown tool: ${name}`);
4073
- }
4074
- }
4075
- catch (error) {
4076
- const message = error instanceof Error ? error.message : String(error);
4077
- return {
4078
- content: [
4079
- {
4080
- type: 'text',
4081
- text: JSON.stringify({ error: message }, null, 2),
4082
- },
4083
- ],
4084
- isError: true,
4085
- };
4086
- }
4087
- });
4088
- // Resource handlers
4089
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
4090
- return {
4091
- resources: [
4092
- {
4093
- uri: 'fcp://launches',
4094
- name: 'FCP Launches',
4095
- description: 'List of all launches in FCP',
4096
- mimeType: 'application/json',
4097
- },
4098
- ],
4099
- };
4100
- });
4101
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
4102
- const { uri } = request.params;
4103
- if (uri === 'fcp://launches') {
4104
- const result = await client.listLaunches({ limit: 50 });
4105
- return {
4106
- contents: [
4107
- {
4108
- uri,
4109
- mimeType: 'application/json',
4110
- text: JSON.stringify(result, null, 2),
4111
- },
4112
- ],
4113
- };
4114
- }
4115
- // Handle fcp://launches/{id}
4116
- const launchMatch = uri.match(/^fcp:\/\/launches\/(\d+)$/);
4117
- if (launchMatch) {
4118
- const launchId = parseInt(launchMatch[1], 10);
4119
- const result = await client.getLaunch(launchId);
4120
- return {
4121
- contents: [
4122
- {
4123
- uri,
4124
- mimeType: 'application/json',
4125
- text: JSON.stringify(result, null, 2),
4126
- },
4127
- ],
4128
- };
4129
- }
4130
- throw new Error(`Unknown resource: ${uri}`);
4131
- });
4132
- // Version comparison helper
4133
- function compareVersions(v1, v2) {
4134
- const parts1 = v1.split('.').map(Number);
4135
- const parts2 = v2.split('.').map(Number);
4136
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
4137
- const p1 = parts1[i] || 0;
4138
- const p2 = parts2[i] || 0;
4139
- if (p1 > p2)
4140
- return 1;
4141
- if (p1 < p2)
4142
- return -1;
4143
- }
4144
- return 0;
4145
- }
4146
- // Check for updates on startup
4147
- async function checkForUpdates() {
4148
- if (!FCP_API_URL)
4149
- return;
4150
- try {
4151
- const response = await fetch(`${FCP_API_URL}/api/mcp/version`, {
4152
- headers: FCP_API_TOKEN ? { 'X-API-Key': FCP_API_TOKEN } : {},
4153
- });
4154
- if (!response.ok) {
4155
- // Silently ignore - version endpoint may not exist on older FCP versions
4156
- return;
4157
- }
4158
- const data = await response.json();
4159
- const latestVersion = data.version;
4160
- const minVersion = data.minVersion;
4161
- if (compareVersions(MCP_SERVER_VERSION, minVersion) < 0) {
4162
- console.error(`\n⚠️ [MCP Server] INCOMPATIBLE VERSION`);
4163
- console.error(` Your version: ${MCP_SERVER_VERSION}`);
4164
- console.error(` Minimum required: ${minVersion}`);
4165
- console.error(` Please update: ${data.updateUrl}\n`);
4166
- }
4167
- else if (compareVersions(MCP_SERVER_VERSION, latestVersion) < 0) {
4168
- console.error(`\n📦 [MCP Server] Update available: ${MCP_SERVER_VERSION} → ${latestVersion}`);
4169
- console.error(` Update: ${data.updateUrl}\n`);
4170
- }
4171
- else {
4172
- console.error(`[MCP Server] Version ${MCP_SERVER_VERSION} (up to date)`);
4173
- }
4174
- }
4175
- catch {
4176
- // Silently ignore network errors - don't block startup
4177
- }
4178
- }
4179
- // Start server
4180
- async function main() {
4181
- const transport = new StdioServerTransport();
4182
- await server.connect(transport);
4183
- console.error(`FCP MCP Server v${MCP_SERVER_VERSION} running on stdio`);
4184
- console.error(` FCP API: ${FCP_API_URL}`);
4185
- if (USE_FCP_UNROO_PROXY) {
4186
- console.error(' Unroo: via FCP proxy (unified key mode)');
4187
- }
4188
- else {
4189
- console.error(` Unroo: direct (${UNROO_API_URL})`);
4190
- }
4191
- // Auto-detect project from git remote (non-blocking)
4192
- initializeProjectDetection();
4193
- // Check for updates (non-blocking)
4194
- checkForUpdates();
4195
- // Sync ~/.claude/skills with the shared Unroo KG (non-blocking, opt-in).
4196
- // First run on a fresh workstation is dry-run only; user opts in via
4197
- // `fcp-mcp-server sync-skills --enable`.
4198
- // Passing the FCP key lets skills-sync route through FCP's Unroo proxy when
4199
- // no personal UNROO_API_KEY is set — so the single `claude mcp add` key
4200
- // covers skill sync too.
4201
- runBackgroundSync({
4202
- unrooApiKey: process.env.UNROO_API_KEY ?? "",
4203
- userEmail: FCP_USER_EMAIL,
4204
- unrooApiUrl: process.env.UNROO_API_URL,
4205
- fcpApiUrl: FCP_API_URL,
4206
- fcpApiToken: FCP_API_TOKEN,
4207
- });
4208
- // Handle graceful shutdown — idempotent, safe to call from any signal/event
4209
- let shuttingDown = false;
4210
- const shutdown = async (reason) => {
4211
- if (shuttingDown)
4212
- return;
4213
- shuttingDown = true;
4214
- console.error(`Shutting down MCP server (${reason})...`);
4215
- try {
4216
- await sessionTracker.endSession();
4217
- }
4218
- catch (err) {
4219
- console.error('Error ending session during shutdown:', err);
4220
- }
4221
- process.exit(0);
4222
- };
4223
- process.on('SIGINT', () => void shutdown('SIGINT'));
4224
- process.on('SIGTERM', () => void shutdown('SIGTERM'));
4225
- process.on('SIGHUP', () => void shutdown('SIGHUP'));
4226
- // Stdin closure means the parent (Claude Code) closed the pipe — exit.
4227
- // Without this, the server lingers as a zombie when the parent restarts
4228
- // or crashes, and accumulates over a long workstation session.
4229
- process.stdin.on('end', () => void shutdown('stdin end'));
4230
- process.stdin.on('close', () => void shutdown('stdin close'));
4231
- process.stdin.on('error', (err) => {
4232
- console.error('stdin error:', err);
4233
- void shutdown('stdin error');
4234
- });
4235
- // Orphan watchdog: if our parent process dies and we get reparented to
4236
- // init (PID 1), no signal fires and stdin may stay technically open. Poll
4237
- // for ppid change and shut down if we're orphaned.
4238
- const initialPpid = process.ppid;
4239
- setInterval(() => {
4240
- const currentPpid = process.ppid;
4241
- if (currentPpid === 1 && initialPpid !== 1) {
4242
- void shutdown(`orphaned (ppid ${initialPpid} -> 1)`);
4243
- }
4244
- }, 30_000).unref();
4245
- }
4246
- // Subcommand: `fcp-mcp-server sync-skills [...]` runs the skills sync CLI
4247
- // instead of starting the MCP server. This lets the same binary serve both
4248
- // the long-running stdio MCP role and the one-shot sync-skills tool.
4249
- if (process.argv[2] === 'sync-skills') {
4250
- runSkillsCli(process.argv.slice(3))
4251
- .then((code) => process.exit(code))
4252
- .catch((err) => {
4253
- console.error('[skills-sync] fatal:', err);
4254
- process.exit(1);
4255
- });
4256
- }
4257
- else {
4258
- main().catch((error) => {
4259
- console.error('Fatal error:', error);
4260
- process.exit(1);
4261
- });
4262
- }