@fruition/fcp-mcp-server 1.27.0 → 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 +9 -0
- package/dist/index.d.ts +397 -0
- package/dist/index.js +166 -6
- package/dist/skills-sync.d.ts +2 -1
- package/package.json +1 -1
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',
|
|
@@ -203,6 +207,9 @@ const TOOL_PERMISSIONS = {
|
|
|
203
207
|
// Resolved role for the current API key. Cached only on successful lookup so
|
|
204
208
|
// transient FCP outages don't pin us to 'none' for the rest of the session.
|
|
205
209
|
let cachedUserRole;
|
|
210
|
+
// The API key owner's email, as reported by /api/mcp/me. Used as the fallback
|
|
211
|
+
// identity for "my"-scoped queries when FCP_USER_EMAIL is not configured.
|
|
212
|
+
let cachedUserEmail;
|
|
206
213
|
async function fetchUserRole() {
|
|
207
214
|
if (cachedUserRole !== undefined)
|
|
208
215
|
return cachedUserRole;
|
|
@@ -227,6 +234,8 @@ async function fetchUserRole() {
|
|
|
227
234
|
const data = await res.json();
|
|
228
235
|
const role = data?.role ?? 'none';
|
|
229
236
|
cachedUserRole = role;
|
|
237
|
+
if (data?.email)
|
|
238
|
+
cachedUserEmail = data.email;
|
|
230
239
|
console.error(`[MCP Server] Resolved user role: ${role} (${data?.email ?? 'unknown'})`);
|
|
231
240
|
return role;
|
|
232
241
|
}
|
|
@@ -235,6 +244,17 @@ async function fetchUserRole() {
|
|
|
235
244
|
return 'none';
|
|
236
245
|
}
|
|
237
246
|
}
|
|
247
|
+
// Resolve the acting user's email for "my"-scoped queries (e.g. unroo_get_my_tasks).
|
|
248
|
+
// Precedence: FCP_USER_EMAIL (the per-developer identity) -> the API key owner's
|
|
249
|
+
// email from /api/mcp/me (the shared key's creator, honoring the "based on API
|
|
250
|
+
// key" contract). Returns '' if neither resolves, so callers can surface a clear
|
|
251
|
+
// "set FCP_USER_EMAIL" message rather than silently returning the org-wide list.
|
|
252
|
+
async function resolveActingEmail() {
|
|
253
|
+
if (FCP_USER_EMAIL)
|
|
254
|
+
return FCP_USER_EMAIL;
|
|
255
|
+
await fetchUserRole(); // populates cachedUserEmail as a side effect
|
|
256
|
+
return cachedUserEmail ?? '';
|
|
257
|
+
}
|
|
238
258
|
function isRoleAtLeast(actual, required) {
|
|
239
259
|
if (actual === 'none')
|
|
240
260
|
return required === 'none';
|
|
@@ -330,7 +350,7 @@ async function initializeProjectDetection() {
|
|
|
330
350
|
}
|
|
331
351
|
}
|
|
332
352
|
// API Client
|
|
333
|
-
class FCPClient {
|
|
353
|
+
export class FCPClient {
|
|
334
354
|
baseUrl;
|
|
335
355
|
token;
|
|
336
356
|
defaultTimeout = 15000; // 15 seconds
|
|
@@ -779,6 +799,36 @@ class FCPClient {
|
|
|
779
799
|
return this.fetchText(`/api/infrastructure/trusted-ips/export${qs ? `?${qs}` : ''}`);
|
|
780
800
|
}
|
|
781
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
|
+
// ============================================================================
|
|
782
832
|
// Backup Management Methods
|
|
783
833
|
// ============================================================================
|
|
784
834
|
async backupListSites() {
|
|
@@ -2051,17 +2101,17 @@ const TOOLS = [
|
|
|
2051
2101
|
},
|
|
2052
2102
|
{
|
|
2053
2103
|
name: 'unroo_get_my_tasks',
|
|
2054
|
-
description: 'Get tasks assigned to
|
|
2104
|
+
description: 'Get tasks assigned to you. "You" resolves from FCP_USER_EMAIL (your Fruition email), falling back to the FCP API key owner if FCP_USER_EMAIL is unset. Returns an error asking you to set FCP_USER_EMAIL if neither can be resolved.',
|
|
2055
2105
|
inputSchema: {
|
|
2056
2106
|
type: 'object',
|
|
2057
2107
|
properties: {
|
|
2058
2108
|
status: {
|
|
2059
2109
|
type: 'string',
|
|
2060
|
-
description: 'Filter by status (comma-separated for multiple)',
|
|
2110
|
+
description: 'Filter by status: To Do, In Progress, Done, Blocked (case-sensitive, comma-separated for multiple)',
|
|
2061
2111
|
},
|
|
2062
2112
|
limit: {
|
|
2063
2113
|
type: 'number',
|
|
2064
|
-
description: 'Maximum number of tasks to return',
|
|
2114
|
+
description: 'Maximum number of tasks to return (default 50)',
|
|
2065
2115
|
},
|
|
2066
2116
|
},
|
|
2067
2117
|
},
|
|
@@ -3059,6 +3109,55 @@ const TOOLS = [
|
|
|
3059
3109
|
},
|
|
3060
3110
|
},
|
|
3061
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
|
+
},
|
|
3062
3161
|
// ============================================================================
|
|
3063
3162
|
// Backup Management Tools
|
|
3064
3163
|
// ============================================================================
|
|
@@ -4425,9 +4524,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4425
4524
|
}
|
|
4426
4525
|
case 'unroo_get_my_tasks': {
|
|
4427
4526
|
const { status, limit } = args;
|
|
4428
|
-
//
|
|
4527
|
+
// "My tasks" must be scoped by an explicit assignee_email filter. There is
|
|
4528
|
+
// NO server-side auto-scoping: GET /api/external/fcp/tasks returns every
|
|
4529
|
+
// task unless assignee_email is supplied, and the shared FCP_API_TOKEN
|
|
4530
|
+
// resolves to the key creator (not the developer running Claude Code), so
|
|
4531
|
+
// the key alone cannot identify "me". Resolve the acting user's email and
|
|
4532
|
+
// pass it as the filter.
|
|
4533
|
+
const assignee_email = await resolveActingEmail();
|
|
4534
|
+
if (!assignee_email) {
|
|
4535
|
+
return {
|
|
4536
|
+
content: [
|
|
4537
|
+
{
|
|
4538
|
+
type: 'text',
|
|
4539
|
+
text: JSON.stringify({
|
|
4540
|
+
total: 0,
|
|
4541
|
+
tasks: [],
|
|
4542
|
+
error: 'Could not determine your identity. Set FCP_USER_EMAIL (your Fruition email) in your MCP server env config so "my tasks" can be scoped to you.',
|
|
4543
|
+
}, null, 2),
|
|
4544
|
+
},
|
|
4545
|
+
],
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
4429
4548
|
const result = await unrooClient.listTasks({
|
|
4430
4549
|
status,
|
|
4550
|
+
assignee_email,
|
|
4431
4551
|
limit: limit || 50,
|
|
4432
4552
|
});
|
|
4433
4553
|
return {
|
|
@@ -4436,6 +4556,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4436
4556
|
type: 'text',
|
|
4437
4557
|
text: JSON.stringify({
|
|
4438
4558
|
total: result.tasks.length,
|
|
4559
|
+
assignee_email,
|
|
4439
4560
|
tasks: result.tasks,
|
|
4440
4561
|
}, null, 2),
|
|
4441
4562
|
},
|
|
@@ -4886,6 +5007,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4886
5007
|
};
|
|
4887
5008
|
}
|
|
4888
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
|
+
// ============================================================================
|
|
4889
5049
|
// Backup Management Handlers
|
|
4890
5050
|
// ============================================================================
|
|
4891
5051
|
case 'fcp_backup_list_sites': {
|
|
@@ -5543,7 +5703,7 @@ if (process.argv[2] === 'sync-skills') {
|
|
|
5543
5703
|
process.exit(1);
|
|
5544
5704
|
});
|
|
5545
5705
|
}
|
|
5546
|
-
else {
|
|
5706
|
+
else if (!process.env.VITEST) {
|
|
5547
5707
|
main().catch((error) => {
|
|
5548
5708
|
console.error('Fatal error:', error);
|
|
5549
5709
|
process.exit(1);
|
package/dist/skills-sync.d.ts
CHANGED
|
@@ -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:
|
|
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.
|
|
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",
|