@fruition/fcp-mcp-server 1.27.1 → 1.28.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/README.md CHANGED
@@ -16,6 +16,15 @@ MCP (Model Context Protocol) server that gives Claude Code direct access to the
16
16
  | `fcp_add_progress_note` | Add progress notes to document work done |
17
17
  | `fcp_get_claude_md` | Generate CLAUDE.md content for a launch |
18
18
 
19
+ ### Tasker Freeze Management Tools
20
+
21
+ | Tool | Description |
22
+ |------|-------------|
23
+ | `fcp_list_freezes` | List active + scheduled auto-merge freezes and the available repos |
24
+ | `fcp_freeze_repo` | Schedule a freeze (by days or explicit window) to hold a repo's deploys |
25
+ | `fcp_update_freeze` | Edit an existing freeze's window or reason |
26
+ | `fcp_unfreeze` | Remove a freeze by id, or clear all freezes for a repo |
27
+
19
28
  ### Unroo Task Management Tools
20
29
 
21
30
  | Tool | Description |
package/dist/index.d.ts CHANGED
@@ -7,4 +7,401 @@
7
7
  * - Update checklist item status
8
8
  * - Get project context for migrations
9
9
  */
10
+ interface Launch {
11
+ id: number;
12
+ name: string;
13
+ description: string | null;
14
+ platform: string;
15
+ launch_type: string;
16
+ status: string;
17
+ priority: string;
18
+ target_launch_date: string;
19
+ domain: string | null;
20
+ legacy_domain: string | null;
21
+ legacy_access_info: Record<string, any>;
22
+ [key: string]: any;
23
+ }
24
+ interface ChecklistItem {
25
+ id: number;
26
+ title: string;
27
+ description: string | null;
28
+ status: string;
29
+ category: string;
30
+ is_blocker: boolean;
31
+ claude_instructions: string | null;
32
+ [key: string]: any;
33
+ }
34
+ interface LaunchDetail {
35
+ launch: Launch;
36
+ checklist: ChecklistItem[];
37
+ [key: string]: any;
38
+ }
39
+ export declare class FCPClient {
40
+ private baseUrl;
41
+ private token;
42
+ private defaultTimeout;
43
+ constructor(baseUrl: string, token: string);
44
+ private fetch;
45
+ listLaunches(filters?: {
46
+ status?: string;
47
+ platform?: string;
48
+ upcoming?: number;
49
+ limit?: number;
50
+ }): Promise<{
51
+ launches: Launch[];
52
+ }>;
53
+ getLaunch(id: number): Promise<LaunchDetail>;
54
+ createChecklistItem(launchId: number, input: {
55
+ title: string;
56
+ category: string;
57
+ description?: string;
58
+ role?: string;
59
+ environment?: string;
60
+ assigned_to_id?: number;
61
+ due_date?: string;
62
+ sort_order?: number;
63
+ depends_on_item_id?: number;
64
+ is_required?: boolean;
65
+ is_blocking?: boolean;
66
+ external_link?: string;
67
+ }): Promise<ChecklistItem>;
68
+ updateChecklistItem(launchId: number, itemId: number, updates: {
69
+ status?: string;
70
+ notes?: string;
71
+ title?: string;
72
+ description?: string | null;
73
+ category?: string;
74
+ role?: string;
75
+ environment?: string;
76
+ assigned_to_id?: number | null;
77
+ due_date?: string | null;
78
+ sort_order?: number;
79
+ depends_on_item_id?: number | null;
80
+ is_required?: boolean;
81
+ is_blocking?: boolean;
82
+ external_link?: string | null;
83
+ evidence_url?: string | null;
84
+ jira_ticket_key?: string | null;
85
+ completion_notes?: string | null;
86
+ }): Promise<ChecklistItem>;
87
+ deleteChecklistItem(launchId: number, itemId: number): Promise<{
88
+ success: boolean;
89
+ }>;
90
+ private buildQuery;
91
+ listAlerts(q?: {
92
+ status?: string;
93
+ source?: string;
94
+ severity?: string;
95
+ cluster?: string;
96
+ namespace?: string;
97
+ domain?: string;
98
+ websiteId?: number;
99
+ environment?: string;
100
+ from?: string;
101
+ to?: string;
102
+ page?: number;
103
+ limit?: number;
104
+ }): Promise<any>;
105
+ getAlert(id: number): Promise<any>;
106
+ createAlert(input: {
107
+ alertName: string;
108
+ severity?: string;
109
+ summary?: string;
110
+ cluster?: string;
111
+ namespace?: string;
112
+ domain?: string;
113
+ websiteId?: number;
114
+ }): Promise<any>;
115
+ updateAlert(id: number, updates: {
116
+ status?: string;
117
+ acknowledgedBy?: string;
118
+ silencedUntil?: string;
119
+ silencedBy?: string;
120
+ adminNotes?: string;
121
+ websiteId?: number;
122
+ unrooTaskId?: number;
123
+ }): Promise<any>;
124
+ getAlertStats(): Promise<any>;
125
+ listAlertRules(accountId?: number): Promise<any>;
126
+ getAlertRule(ruleId: number): Promise<any>;
127
+ createAlertRule(input: Record<string, any>): Promise<any>;
128
+ updateAlertRule(ruleId: number, updates: Record<string, any>): Promise<any>;
129
+ deleteAlertRule(ruleId: number): Promise<any>;
130
+ listOutages(q?: {
131
+ status?: string;
132
+ accountId?: number;
133
+ limit?: number;
134
+ }): Promise<any>;
135
+ getOutage(outageId: number): Promise<any>;
136
+ createOutage(input: Record<string, any>): Promise<any>;
137
+ updateOutage(outageId: number, updates: Record<string, any>): Promise<any>;
138
+ resolveOutage(outageId: number): Promise<any>;
139
+ bulkNotifyClients(input: {
140
+ outageIds: number[];
141
+ notificationType: string;
142
+ subject: string;
143
+ message: string;
144
+ channels?: string[];
145
+ sentBy?: string;
146
+ }): Promise<any>;
147
+ getBulkNotifyHistory(limit?: number): Promise<any>;
148
+ createLaunch(input: {
149
+ name: string;
150
+ platform: string;
151
+ launch_type: string;
152
+ target_launch_date: string;
153
+ website_id?: number;
154
+ account_id?: number;
155
+ description?: string;
156
+ soft_launch_date?: string;
157
+ kickoff_date?: string;
158
+ priority?: string;
159
+ jira_project_key?: string;
160
+ unroo_project_id?: number;
161
+ webops_lead_id?: number;
162
+ devops_lead_id?: number;
163
+ dev_lead_id?: number;
164
+ seo_lead_id?: number;
165
+ client_contact?: string;
166
+ client_contact_email?: string;
167
+ use_default_checklist?: boolean;
168
+ }): Promise<{
169
+ launch: Launch;
170
+ }>;
171
+ updateLaunch(id: number, updates: {
172
+ website_id?: number | null;
173
+ account_id?: number | null;
174
+ name?: string;
175
+ description?: string;
176
+ platform?: string;
177
+ launch_type?: string;
178
+ target_launch_date?: string;
179
+ soft_launch_date?: string | null;
180
+ actual_launch_date?: string | null;
181
+ kickoff_date?: string | null;
182
+ status?: string;
183
+ priority?: string;
184
+ jira_project_key?: string | null;
185
+ unroo_project_id?: number | null;
186
+ webops_lead_id?: number | null;
187
+ devops_lead_id?: number | null;
188
+ dev_lead_id?: number | null;
189
+ seo_lead_id?: number | null;
190
+ client_contact?: string | null;
191
+ client_contact_email?: string | null;
192
+ }): Promise<any>;
193
+ deleteLaunch(id: number): Promise<{
194
+ success: boolean;
195
+ }>;
196
+ getSuccessFactors(launchId: number, itemId: number): Promise<{
197
+ factors: any[];
198
+ }>;
199
+ validateChecklistItem(launchId: number, itemId: number): Promise<any>;
200
+ getClaudeMd(launchId: number): Promise<{
201
+ content: string;
202
+ }>;
203
+ addNote(launchId: number, content: string): Promise<any>;
204
+ listFileSyncConfigs(filters?: {
205
+ enabled?: boolean;
206
+ search?: string;
207
+ sync_direction?: string;
208
+ }): Promise<{
209
+ success: boolean;
210
+ data: any[];
211
+ count: number;
212
+ }>;
213
+ getFileSyncJobs(filters?: {
214
+ config_id?: number;
215
+ status?: string;
216
+ limit?: number;
217
+ }): Promise<{
218
+ success: boolean;
219
+ data: any[];
220
+ count: number;
221
+ }>;
222
+ startFileSync(input: {
223
+ config_id: number;
224
+ trigger_type?: string;
225
+ triggered_by?: string;
226
+ confirmation_token?: string;
227
+ }): Promise<{
228
+ success: boolean;
229
+ data: any;
230
+ message: string;
231
+ }>;
232
+ cancelFileSync(jobId: string): Promise<{
233
+ success: boolean;
234
+ message: string;
235
+ }>;
236
+ getFileSyncConfirmation(configId: number): Promise<{
237
+ success: boolean;
238
+ data: {
239
+ token: string;
240
+ config_id: number;
241
+ expires_at: string;
242
+ ttl_seconds: number;
243
+ warnings: string[];
244
+ };
245
+ }>;
246
+ triggerNucleiScan(websiteId: number, options?: {
247
+ url?: string;
248
+ severity?: string;
249
+ templates?: string;
250
+ }): Promise<any>;
251
+ getNucleiResults(websiteId: number, options?: {
252
+ scan_id?: string;
253
+ limit?: number;
254
+ status?: string;
255
+ }): Promise<any>;
256
+ scanSecurityHeaders(websiteId: number): Promise<any>;
257
+ getSecurityHeadersResults(websiteId: number, scanId?: string): Promise<any>;
258
+ listSites(filters?: {
259
+ account_id?: number;
260
+ cms?: string;
261
+ environment?: string;
262
+ fru_hosted?: boolean;
263
+ retired?: string;
264
+ limit?: number;
265
+ offset?: number;
266
+ }): Promise<any>;
267
+ searchSites(query: string, limit?: number): Promise<any>;
268
+ getSite(siteId: number): Promise<any>;
269
+ createSite(production: {
270
+ account_id: number;
271
+ domain: string;
272
+ cms: string;
273
+ url_full?: string;
274
+ git_provider?: string;
275
+ git_link?: string;
276
+ hosting_provider?: string;
277
+ fru_hosted?: boolean;
278
+ k8s_cluster?: string;
279
+ k8s_namespace?: string;
280
+ }, staging?: Array<{
281
+ domain: string;
282
+ k8s_namespace: string;
283
+ }>): Promise<any>;
284
+ updateSite(siteId: number, updates: Record<string, any>): Promise<any>;
285
+ deleteSite(siteId: number, options?: {
286
+ reason?: string;
287
+ forward_url?: string;
288
+ }): Promise<any>;
289
+ getLocalSetupGuide(siteId: number): Promise<any>;
290
+ listClients(filters?: {
291
+ q?: string;
292
+ webops_lead?: string;
293
+ contract_type?: string;
294
+ tier?: string;
295
+ marketing?: boolean;
296
+ status?: string;
297
+ limit?: number;
298
+ }): Promise<any>;
299
+ getClient(accountId: number): Promise<any>;
300
+ updateClientProfile(accountId: number, updates: Record<string, any>): Promise<any>;
301
+ shieldListDomains(filters?: {
302
+ status?: string;
303
+ account_id?: number;
304
+ }): Promise<any>;
305
+ shieldAddDomain(input: {
306
+ domain: string;
307
+ origin_type?: 'custom' | 's3';
308
+ origin_host?: string;
309
+ origin_port?: number;
310
+ origin_protocol?: string;
311
+ s3_bucket?: string;
312
+ s3_region?: string;
313
+ s3_origin_path?: string;
314
+ website_id?: number;
315
+ account_id?: number;
316
+ cache_profile?: string;
317
+ waf_profile?: string;
318
+ geo_block_countries?: string[];
319
+ bot_control_enabled?: boolean;
320
+ notes?: string;
321
+ }): Promise<any>;
322
+ shieldGetDomain(domainId: number): Promise<any>;
323
+ shieldUpdateDomain(domainId: number, updates: Record<string, any>): Promise<any>;
324
+ shieldRemoveDomain(domainId: number): Promise<any>;
325
+ shieldGetMetrics(): Promise<any>;
326
+ shieldVerifyDomain(domainId: number): Promise<any>;
327
+ private fetchText;
328
+ trustedIpListRanges(filters?: {
329
+ group?: string;
330
+ include_pending?: boolean;
331
+ include_expired?: boolean;
332
+ }): Promise<any>;
333
+ trustedIpAddRange(input: {
334
+ group_id: number;
335
+ cidr: string;
336
+ label?: string;
337
+ purpose?: string;
338
+ ticket_ref?: string;
339
+ expires_at?: string;
340
+ override_reason?: string;
341
+ }): Promise<any>;
342
+ trustedIpUpdateRange(id: number, updates: Record<string, unknown>): Promise<any>;
343
+ trustedIpRemoveRange(id: number): Promise<any>;
344
+ trustedIpExport(opts?: {
345
+ format?: string;
346
+ group?: string;
347
+ purpose?: string;
348
+ separator?: string;
349
+ }): Promise<string>;
350
+ listFreezes(): Promise<any>;
351
+ freezeRepo(input: {
352
+ repo: string;
353
+ reason: string;
354
+ days?: number;
355
+ starts_at?: string;
356
+ ends_at?: string;
357
+ }): Promise<any>;
358
+ updateFreeze(id: number, updates: Record<string, unknown>): Promise<any>;
359
+ unfreeze(input: {
360
+ id?: number;
361
+ repo?: string;
362
+ }): Promise<any>;
363
+ backupListSites(): Promise<any>;
364
+ backupGetConfig(): Promise<any>;
365
+ backupListEligible(): Promise<any>;
366
+ backupEnable(siteId: number): Promise<any>;
367
+ backupTrigger(websiteId: number, triggerType?: string): Promise<any>;
368
+ backupCheckTrigger(websiteId: number): Promise<any>;
369
+ backupListBackups(siteId: string): Promise<any>;
370
+ backupGetStatus(options: {
371
+ backupId?: string;
372
+ websiteId?: number;
373
+ }): Promise<any>;
374
+ backupDownload(backupId: string): Promise<any>;
375
+ backupDownloadPrepared(input: {
376
+ siteId: string;
377
+ backupId: string;
378
+ downloadType: string;
379
+ format?: string;
380
+ sanitize?: boolean;
381
+ }): Promise<any>;
382
+ backupSanitizeStatus(jobId: string): Promise<any>;
383
+ backupListPairings(siteId?: number): Promise<any>;
384
+ backupUpdatePairing(siteId: number, pairingConfig: Record<string, any>): Promise<any>;
385
+ backupDeletePairing(siteId: number): Promise<any>;
386
+ kinstaBackupListSites(): Promise<any>;
387
+ kinstaBackupListBackups(siteId: string): Promise<any>;
388
+ kinstaBackupDownload(s3Key: string): Promise<any>;
389
+ cloneToStagingConfirm(productionSiteId: number, stagingSiteId: number): Promise<any>;
390
+ cloneToStaging(params: {
391
+ productionSiteId: number;
392
+ stagingSiteId: number;
393
+ includeFiles?: boolean;
394
+ includeDatabase?: boolean;
395
+ runSearchReplace?: boolean;
396
+ confirmationToken: string;
397
+ backupId?: string;
398
+ }): Promise<any>;
399
+ getCloneStatus(cloneId: string): Promise<any>;
400
+ listCloneOperations(params?: {
401
+ productionSiteId?: number;
402
+ stagingSiteId?: number;
403
+ limit?: number;
404
+ }): Promise<any>;
405
+ getDevEnvironmentInfo(siteId: number): Promise<any>;
406
+ }
10
407
  export {};
package/dist/index.js CHANGED
@@ -98,6 +98,7 @@ const TOOL_PERMISSIONS = {
98
98
  fcp_trusted_ip_remove_range: 'super_admin',
99
99
  fcp_backup_delete_pairing: 'super_admin',
100
100
  fcp_filesync_cancel_sync: 'super_admin',
101
+ fcp_unfreeze: 'super_admin', // lifting a freeze releases a held deploy
101
102
  // --- admin+: mutating ops with real-world side effects ---
102
103
  fcp_create_launch: 'admin',
103
104
  fcp_update_launch: 'admin',
@@ -108,6 +109,8 @@ const TOOL_PERMISSIONS = {
108
109
  fcp_shield_update_domain: 'admin',
109
110
  fcp_trusted_ip_add_range: 'admin',
110
111
  fcp_trusted_ip_update_range: 'admin',
112
+ fcp_freeze_repo: 'admin',
113
+ fcp_update_freeze: 'admin',
111
114
  fcp_backup_enable: 'admin',
112
115
  fcp_backup_trigger: 'admin',
113
116
  fcp_backup_check_trigger: 'admin',
@@ -157,6 +160,7 @@ const TOOL_PERMISSIONS = {
157
160
  fcp_shield_get_metrics: 'viewer',
158
161
  fcp_trusted_ip_list_ranges: 'viewer',
159
162
  fcp_trusted_ip_export: 'viewer',
163
+ fcp_list_freezes: 'viewer',
160
164
  fcp_backup_list_sites: 'viewer',
161
165
  fcp_backup_get_config: 'viewer',
162
166
  fcp_backup_list_eligible: 'viewer',
@@ -346,7 +350,7 @@ async function initializeProjectDetection() {
346
350
  }
347
351
  }
348
352
  // API Client
349
- class FCPClient {
353
+ export class FCPClient {
350
354
  baseUrl;
351
355
  token;
352
356
  defaultTimeout = 15000; // 15 seconds
@@ -795,6 +799,36 @@ class FCPClient {
795
799
  return this.fetchText(`/api/infrastructure/trusted-ips/export${qs ? `?${qs}` : ''}`);
796
800
  }
797
801
  // ============================================================================
802
+ // Tasker Freeze Management Methods
803
+ // ============================================================================
804
+ async listFreezes() {
805
+ return this.fetch('/api/ticket-workflow/freeze');
806
+ }
807
+ async freezeRepo(input) {
808
+ return this.fetch('/api/ticket-workflow/freeze', {
809
+ method: 'POST',
810
+ body: JSON.stringify(input),
811
+ });
812
+ }
813
+ async updateFreeze(id, updates) {
814
+ return this.fetch('/api/ticket-workflow/freeze', {
815
+ method: 'PATCH',
816
+ body: JSON.stringify({ id, ...updates }),
817
+ });
818
+ }
819
+ async unfreeze(input) {
820
+ const hasId = input.id !== undefined && input.id !== null;
821
+ const hasRepo = typeof input.repo === 'string' && input.repo.length > 0;
822
+ if (hasId === hasRepo) {
823
+ throw new Error('unfreeze requires exactly one of "id" or "repo"');
824
+ }
825
+ const body = hasId ? { id: input.id } : { repo: input.repo };
826
+ return this.fetch('/api/ticket-workflow/freeze', {
827
+ method: 'DELETE',
828
+ body: JSON.stringify(body),
829
+ });
830
+ }
831
+ // ============================================================================
798
832
  // Backup Management Methods
799
833
  // ============================================================================
800
834
  async backupListSites() {
@@ -3075,6 +3109,55 @@ const TOOLS = [
3075
3109
  },
3076
3110
  },
3077
3111
  },
3112
+ {
3113
+ name: 'fcp_list_freezes',
3114
+ description: 'List Tasker auto-merge freezes. Returns frozen_repos (active now), scheduled_repos (future windows), and available_repos (every repo you may freeze, with project keys + domains). A frozen repo has its release/security PRs held from the 6-7 AM MT deployment window. Call this before fcp_freeze_repo to pick a valid repo and avoid duplicate freezes.',
3115
+ inputSchema: {
3116
+ type: 'object',
3117
+ properties: {},
3118
+ },
3119
+ },
3120
+ {
3121
+ name: 'fcp_freeze_repo',
3122
+ description: 'Schedule a Tasker auto-merge freeze for a repo (blocks release-PR auto-promotion + security auto-merge while active). Provide either days (1-90) or an explicit ends_at; starts_at defaults to now and cannot be backdated >5 min. The repo must exist in github_repo_mappings (else 404 \u2014 use fcp_list_freezes available_repos). Appends a new window each call.',
3123
+ inputSchema: {
3124
+ type: 'object',
3125
+ properties: {
3126
+ repo: { type: 'string', description: 'owner/repo, e.g. "fruition/metroairport"' },
3127
+ reason: { type: 'string', description: 'Why the repo is being frozen (required)' },
3128
+ days: { type: 'number', description: 'Freeze length in days (1-90); alternative to ends_at' },
3129
+ starts_at: { type: 'string', description: 'ISO-8601 start (optional; defaults to now, no backdating >5 min)' },
3130
+ ends_at: { type: 'string', description: 'ISO-8601 end (alternative to days)' },
3131
+ },
3132
+ required: ['repo', 'reason'],
3133
+ },
3134
+ },
3135
+ {
3136
+ name: 'fcp_update_freeze',
3137
+ description: 'Edit an existing freeze by id (from fcp_list_freezes). Change the window (starts_at / ends_at, or days as a shorthand for ends_at) and/or the reason. Window rules match fcp_freeze_repo (<=90 days, no backdating >5 min).',
3138
+ inputSchema: {
3139
+ type: 'object',
3140
+ properties: {
3141
+ id: { type: 'number', description: 'Freeze id to edit' },
3142
+ starts_at: { type: 'string', description: 'New ISO-8601 start' },
3143
+ ends_at: { type: 'string', description: 'New ISO-8601 end' },
3144
+ days: { type: 'number', description: 'New length in days (recomputes ends_at from starts_at)' },
3145
+ reason: { type: 'string', description: 'New reason' },
3146
+ },
3147
+ required: ['id'],
3148
+ },
3149
+ },
3150
+ {
3151
+ name: 'fcp_unfreeze',
3152
+ description: 'Remove a Tasker freeze. Pass exactly one of: id (remove that single freeze) or repo (remove ALL active + scheduled freezes for that owner/repo). Lifting a freeze lets a held deploy through, so this is a super_admin action.',
3153
+ inputSchema: {
3154
+ type: 'object',
3155
+ properties: {
3156
+ id: { type: 'number', description: 'Freeze id to remove (single)' },
3157
+ repo: { type: 'string', description: 'owner/repo to clear all active+scheduled freezes for' },
3158
+ },
3159
+ },
3160
+ },
3078
3161
  // ============================================================================
3079
3162
  // Backup Management Tools
3080
3163
  // ============================================================================
@@ -4924,6 +5007,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4924
5007
  };
4925
5008
  }
4926
5009
  // ============================================================================
5010
+ // Tasker Freeze Management Handlers
5011
+ // ============================================================================
5012
+ case 'fcp_list_freezes': {
5013
+ const result = await client.listFreezes();
5014
+ return {
5015
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
5016
+ };
5017
+ }
5018
+ case 'fcp_freeze_repo': {
5019
+ const { repo, reason, days, starts_at, ends_at } = args;
5020
+ const result = await client.freezeRepo({ repo, reason, days, starts_at, ends_at });
5021
+ return {
5022
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
5023
+ };
5024
+ }
5025
+ case 'fcp_update_freeze': {
5026
+ const { id, starts_at, ends_at, days, reason } = args;
5027
+ const updates = {};
5028
+ if (starts_at !== undefined)
5029
+ updates.starts_at = starts_at;
5030
+ if (ends_at !== undefined)
5031
+ updates.ends_at = ends_at;
5032
+ if (days !== undefined)
5033
+ updates.days = days;
5034
+ if (reason !== undefined)
5035
+ updates.reason = reason;
5036
+ const result = await client.updateFreeze(id, updates);
5037
+ return {
5038
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
5039
+ };
5040
+ }
5041
+ case 'fcp_unfreeze': {
5042
+ const { id, repo } = args;
5043
+ const result = await client.unfreeze({ id, repo });
5044
+ return {
5045
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
5046
+ };
5047
+ }
5048
+ // ============================================================================
4927
5049
  // Backup Management Handlers
4928
5050
  // ============================================================================
4929
5051
  case 'fcp_backup_list_sites': {
@@ -5581,7 +5703,7 @@ if (process.argv[2] === 'sync-skills') {
5581
5703
  process.exit(1);
5582
5704
  });
5583
5705
  }
5584
- else {
5706
+ else if (!process.env.VITEST) {
5585
5707
  main().catch((error) => {
5586
5708
  console.error('Fatal error:', error);
5587
5709
  process.exit(1);
@@ -22,6 +22,7 @@
22
22
  * - `private: true` in frontmatter also opts out.
23
23
  * - Network failures and missing auth never block startup — we log + continue.
24
24
  */
25
+ import { basename } from 'path';
25
26
  export interface SkillFrontmatter {
26
27
  name?: string;
27
28
  description?: string;
@@ -151,5 +152,5 @@ export declare const __test__: {
151
152
  detectSecret: typeof detectSecret;
152
153
  pushSkipReason: typeof pushSkipReason;
153
154
  hashBody: typeof hashBody;
154
- basename: (path: string, suffix?: string) => string;
155
+ basename: typeof basename;
155
156
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fruition/fcp-mcp-server",
3
- "version": "1.27.1",
3
+ "version": "1.28.0",
4
4
  "description": "MCP Server for FCP Launch Coordination System - enables Claude Code to interact with FCP launches and track development time",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",