@fruition/fcp-mcp-server 1.18.0 → 1.19.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/bin/unroo-heartbeat.sh +12 -4
- package/dist/index.js +151 -0
- package/dist/skills-sync-cli.js +12 -4
- package/dist/skills-sync.d.ts +2 -0
- package/dist/skills-sync.js +102 -29
- package/package.json +2 -2
package/bin/unroo-heartbeat.sh
CHANGED
|
@@ -57,12 +57,20 @@ if [ -f ".unroo" ]; then
|
|
|
57
57
|
PROJECT_SLUG=$(grep -E "^project_slug=" .unroo 2>/dev/null | cut -d= -f2)
|
|
58
58
|
fi
|
|
59
59
|
|
|
60
|
-
# Get machine ID (for multi-machine tracking)
|
|
60
|
+
# Get machine ID (for multi-machine tracking) — portable across Linux and macOS
|
|
61
61
|
MACHINE_ID=""
|
|
62
62
|
if [ -f /etc/machine-id ]; then
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
# Linux (systemd)
|
|
64
|
+
MACHINE_ID=$(head -c 8 /etc/machine-id 2>/dev/null)
|
|
65
|
+
elif [ -r /var/lib/dbus/machine-id ]; then
|
|
66
|
+
# Linux (dbus, no systemd)
|
|
67
|
+
MACHINE_ID=$(head -c 8 /var/lib/dbus/machine-id 2>/dev/null)
|
|
68
|
+
elif command -v ioreg >/dev/null 2>&1; then
|
|
69
|
+
# macOS — IOPlatformUUID is stable per machine
|
|
70
|
+
MACHINE_ID=$(ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null \
|
|
71
|
+
| awk -F'"' '/IOPlatformUUID/{print $4}' | head -c 8)
|
|
72
|
+
elif command -v hostid >/dev/null 2>&1; then
|
|
73
|
+
MACHINE_ID=$(hostid 2>/dev/null)
|
|
66
74
|
fi
|
|
67
75
|
|
|
68
76
|
# Build JSON payload
|
package/dist/index.js
CHANGED
|
@@ -77,6 +77,148 @@ const UNROO_AVAILABLE = UNROO_API_KEY || (USE_FCP_UNROO_PROXY && FCP_API_TOKEN);
|
|
|
77
77
|
// - Multiple processes on same machine (PID)
|
|
78
78
|
// - Process restarts (timestamp)
|
|
79
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
|
+
}
|
|
80
222
|
let currentProject = null;
|
|
81
223
|
/**
|
|
82
224
|
* Detect the current git remote URL
|
|
@@ -2805,6 +2947,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2805
2947
|
// Track tool call for session management
|
|
2806
2948
|
await sessionTracker.trackToolCall(name, `Called ${name}`);
|
|
2807
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);
|
|
2808
2954
|
switch (name) {
|
|
2809
2955
|
case 'fcp_list_launches': {
|
|
2810
2956
|
const result = await client.listLaunches(args);
|
|
@@ -4049,10 +4195,15 @@ async function main() {
|
|
|
4049
4195
|
// Sync ~/.claude/skills with the shared Unroo KG (non-blocking, opt-in).
|
|
4050
4196
|
// First run on a fresh workstation is dry-run only; user opts in via
|
|
4051
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.
|
|
4052
4201
|
runBackgroundSync({
|
|
4053
4202
|
unrooApiKey: process.env.UNROO_API_KEY ?? "",
|
|
4054
4203
|
userEmail: FCP_USER_EMAIL,
|
|
4055
4204
|
unrooApiUrl: process.env.UNROO_API_URL,
|
|
4205
|
+
fcpApiUrl: FCP_API_URL,
|
|
4206
|
+
fcpApiToken: FCP_API_TOKEN,
|
|
4056
4207
|
});
|
|
4057
4208
|
// Handle graceful shutdown — idempotent, safe to call from any signal/event
|
|
4058
4209
|
let shuttingDown = false;
|
package/dist/skills-sync-cli.js
CHANGED
|
@@ -47,10 +47,15 @@ Options:
|
|
|
47
47
|
--enable Persist the one-time opt-in flag and run a real sync
|
|
48
48
|
--help, -h Show this help
|
|
49
49
|
|
|
50
|
-
Environment:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
Environment (proxy mode — default, recommended):
|
|
51
|
+
FCP_API_TOKEN FCP API key. With this set and no UNROO_API_KEY, sync routes
|
|
52
|
+
through FCP's Unroo proxy — no personal Unroo key needed.
|
|
53
|
+
FCP_API_URL FCP base URL (default https://fcp.fru.io).
|
|
54
|
+
|
|
55
|
+
Environment (direct mode — optional, legacy):
|
|
56
|
+
UNROO_API_KEY Personal Unroo key. If set, sync talks to Unroo directly.
|
|
57
|
+
FCP_USER_EMAIL Your Fruition team email; required for direct-mode attribution.
|
|
58
|
+
UNROO_API_URL Unroo base URL override (default https://app.unroo.io).
|
|
54
59
|
|
|
55
60
|
State file: ${defaultStateFile(defaultSkillsDir())}
|
|
56
61
|
`.trim();
|
|
@@ -95,6 +100,9 @@ export async function runSkillsCli(argv) {
|
|
|
95
100
|
unrooApiKey: process.env.UNROO_API_KEY ?? '',
|
|
96
101
|
userEmail: process.env.FCP_USER_EMAIL ?? '',
|
|
97
102
|
unrooApiUrl: process.env.UNROO_API_URL,
|
|
103
|
+
// FCP key enables proxy mode when no personal UNROO_API_KEY is set.
|
|
104
|
+
fcpApiUrl: process.env.FCP_API_URL,
|
|
105
|
+
fcpApiToken: process.env.FCP_API_TOKEN,
|
|
98
106
|
// --enable forces a real run; otherwise let lib decide based on opt-in flag
|
|
99
107
|
dryRun: args.enable ? false : args.dryRun,
|
|
100
108
|
};
|
package/dist/skills-sync.d.ts
CHANGED
package/dist/skills-sync.js
CHANGED
|
@@ -31,8 +31,18 @@ import { createHash } from 'crypto';
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
const STATE_VERSION = 1;
|
|
33
33
|
const DEFAULT_UNROO_URL = 'https://app.unroo.io';
|
|
34
|
+
const DEFAULT_FCP_URL = 'https://fcp.fru.io';
|
|
35
|
+
// Direct mode: skills-sync talks straight to Unroo's knowledge-graph endpoints
|
|
36
|
+
// with a personal UNROO_API_KEY.
|
|
34
37
|
const SEARCH_PATH = '/api/external/fcp/knowledge/search';
|
|
35
38
|
const CAPTURE_PATH = '/api/external/fcp/knowledge/capture-skill';
|
|
39
|
+
// Proxy mode: skills-sync talks to FCP, which forwards to the same Unroo
|
|
40
|
+
// endpoints using FCP's own service key. This lets a single FCP API key (the
|
|
41
|
+
// one from `claude mcp add`) cover skill sync — no personal Unroo key to
|
|
42
|
+
// obtain, export, or rotate. Mirrors USE_FCP_UNROO_PROXY in the MCP server.
|
|
43
|
+
// See app/api/mcp/unroo/[...path]/route.ts.
|
|
44
|
+
const PROXY_SEARCH_PATH = '/api/mcp/unroo/knowledge/search';
|
|
45
|
+
const PROXY_CAPTURE_PATH = '/api/mcp/unroo/knowledge/capture-skill';
|
|
36
46
|
// Refuse to upload bodies containing strings that look like a baked-in secret.
|
|
37
47
|
// We capture the leading "label" and the candidate "value" separately so we
|
|
38
48
|
// can reject obvious placeholder shapes (env-var names, template variables,
|
|
@@ -294,21 +304,20 @@ export function detectSecret(body) {
|
|
|
294
304
|
export function hashBody(body) {
|
|
295
305
|
return createHash('sha256').update(body, 'utf-8').digest('hex');
|
|
296
306
|
}
|
|
297
|
-
async function postCapture(
|
|
298
|
-
const res = await fetchImpl(
|
|
307
|
+
async function postCapture(ctx, payload) {
|
|
308
|
+
const res = await ctx.fetchImpl(ctx.captureUrl, {
|
|
299
309
|
method: 'POST',
|
|
300
310
|
headers: {
|
|
301
311
|
'Content-Type': 'application/json',
|
|
302
312
|
Accept: 'application/json',
|
|
303
|
-
|
|
304
|
-
'X-FCP-User-Email': email,
|
|
313
|
+
...ctx.authHeaders,
|
|
305
314
|
},
|
|
306
315
|
body: JSON.stringify(payload),
|
|
307
316
|
});
|
|
308
317
|
const body = await res.text();
|
|
309
318
|
return { ok: res.ok, status: res.status, body };
|
|
310
319
|
}
|
|
311
|
-
async function getSearch(
|
|
320
|
+
async function getSearch(ctx) {
|
|
312
321
|
const all = [];
|
|
313
322
|
let cursor = null;
|
|
314
323
|
// Defensive cap: 50 pages × 100 = 5000 skills, far more than the team will
|
|
@@ -317,11 +326,10 @@ async function getSearch(baseUrl, apiKey, email, fetchImpl) {
|
|
|
317
326
|
const qs = new URLSearchParams({ node_type: 'skill', limit: '100' });
|
|
318
327
|
if (cursor)
|
|
319
328
|
qs.set('cursor', cursor);
|
|
320
|
-
const res = await fetchImpl(`${
|
|
329
|
+
const res = await ctx.fetchImpl(`${ctx.searchUrl}?${qs.toString()}`, {
|
|
321
330
|
headers: {
|
|
322
331
|
Accept: 'application/json',
|
|
323
|
-
|
|
324
|
-
'X-FCP-User-Email': email,
|
|
332
|
+
...ctx.authHeaders,
|
|
325
333
|
},
|
|
326
334
|
});
|
|
327
335
|
if (!res.ok) {
|
|
@@ -384,7 +392,7 @@ async function pushOnce(ctx, state, result) {
|
|
|
384
392
|
continue;
|
|
385
393
|
}
|
|
386
394
|
try {
|
|
387
|
-
const res = await postCapture(ctx
|
|
395
|
+
const res = await postCapture(ctx, payload);
|
|
388
396
|
if (!res.ok) {
|
|
389
397
|
ctx.log(`[skills-sync] push ${skill.slug} failed (HTTP ${res.status}): ${res.body.slice(0, 200)}`);
|
|
390
398
|
continue;
|
|
@@ -403,7 +411,7 @@ async function pushOnce(ctx, state, result) {
|
|
|
403
411
|
async function pullOnce(ctx, state, result) {
|
|
404
412
|
let remoteSkills;
|
|
405
413
|
try {
|
|
406
|
-
remoteSkills = await getSearch(ctx
|
|
414
|
+
remoteSkills = await getSearch(ctx);
|
|
407
415
|
}
|
|
408
416
|
catch (err) {
|
|
409
417
|
ctx.log(`[skills-sync] pull failed: ${err.message}`);
|
|
@@ -475,18 +483,80 @@ async function pullOnce(ctx, state, result) {
|
|
|
475
483
|
result.pulled.push(slug);
|
|
476
484
|
}
|
|
477
485
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
486
|
+
/**
|
|
487
|
+
* Decide how skills-sync reaches the knowledge graph.
|
|
488
|
+
*
|
|
489
|
+
* Direct mode: a personal UNROO_API_KEY talks straight to Unroo. Used when the
|
|
490
|
+
* caller explicitly supplies an Unroo key (legacy / power-user setup).
|
|
491
|
+
*
|
|
492
|
+
* Proxy mode: the FCP API key talks to FCP, which forwards to the same Unroo
|
|
493
|
+
* endpoints with its own service key. This is the default — it means a
|
|
494
|
+
* developer only needs the one FCP key from `claude mcp add`, with no second
|
|
495
|
+
* key to obtain, export, or rotate. Mirrors USE_FCP_UNROO_PROXY in index.ts.
|
|
496
|
+
*/
|
|
497
|
+
function resolveTransport(opts) {
|
|
498
|
+
const email = opts.userEmail ?? '';
|
|
499
|
+
const unrooKey = opts.unrooApiKey ?? '';
|
|
500
|
+
const fcpToken = opts.fcpApiToken ?? '';
|
|
501
|
+
// Direct mode wins when a personal Unroo key is explicitly provided.
|
|
502
|
+
if (unrooKey) {
|
|
503
|
+
if (!email) {
|
|
504
|
+
return {
|
|
505
|
+
configured: false,
|
|
506
|
+
reason: 'UNROO_API_KEY is set but FCP_USER_EMAIL is not — cannot attribute sync',
|
|
507
|
+
mode: 'direct', searchUrl: '', captureUrl: '', authHeaders: {},
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
const base = opts.unrooApiUrl ?? DEFAULT_UNROO_URL;
|
|
511
|
+
return {
|
|
512
|
+
configured: true,
|
|
513
|
+
mode: 'direct',
|
|
514
|
+
searchUrl: `${base}${SEARCH_PATH}`,
|
|
515
|
+
captureUrl: `${base}${CAPTURE_PATH}`,
|
|
516
|
+
authHeaders: { 'X-API-Key': unrooKey, 'X-FCP-User-Email': email },
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
// Proxy mode: the FCP key carries the sync.
|
|
520
|
+
if (fcpToken) {
|
|
521
|
+
if (fcpToken === 'dev_bypass') {
|
|
522
|
+
return {
|
|
523
|
+
configured: false,
|
|
524
|
+
reason: 'FCP_API_TOKEN=dev_bypass (local dev) — skill sync skipped',
|
|
525
|
+
mode: 'proxy', searchUrl: '', captureUrl: '', authHeaders: {},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const base = opts.fcpApiUrl ?? DEFAULT_FCP_URL;
|
|
529
|
+
// The FCP proxy reads X-Acting-User-Email for attribution; it honors the
|
|
530
|
+
// override only when it matches the key owner (or an Auth0 session),
|
|
531
|
+
// otherwise it falls back to the key owner — itself a real Fruition user,
|
|
532
|
+
// so the knowledge graph's Fruition-org gate passes either way.
|
|
533
|
+
const authHeaders = { 'X-API-Key': fcpToken };
|
|
534
|
+
if (email)
|
|
535
|
+
authHeaders['X-Acting-User-Email'] = email;
|
|
536
|
+
return {
|
|
537
|
+
configured: true,
|
|
538
|
+
mode: 'proxy',
|
|
539
|
+
searchUrl: `${base}${PROXY_SEARCH_PATH}`,
|
|
540
|
+
captureUrl: `${base}${PROXY_CAPTURE_PATH}`,
|
|
541
|
+
authHeaders,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
configured: false,
|
|
546
|
+
reason: 'neither UNROO_API_KEY nor FCP_API_TOKEN is set — cannot reach the knowledge graph',
|
|
547
|
+
mode: 'proxy', searchUrl: '', captureUrl: '', authHeaders: {},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function makeCtx(opts, dryRunOverride, transport) {
|
|
482
551
|
const skillsDir = opts.skillsDir ?? defaultSkillsDir();
|
|
483
552
|
const stateFile = opts.stateFile ?? defaultStateFile(skillsDir);
|
|
484
553
|
return {
|
|
485
554
|
skillsDir,
|
|
486
555
|
stateFile,
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
556
|
+
mode: transport.mode,
|
|
557
|
+
searchUrl: transport.searchUrl,
|
|
558
|
+
captureUrl: transport.captureUrl,
|
|
559
|
+
authHeaders: transport.authHeaders,
|
|
490
560
|
fetchImpl: opts.fetchImpl ?? fetch,
|
|
491
561
|
log: opts.log ?? ((m) => console.error(m)),
|
|
492
562
|
dryRun: dryRunOverride,
|
|
@@ -509,12 +579,13 @@ export async function pushSkills(opts = {}) {
|
|
|
509
579
|
// Fresh workstation -> force dry-run unless explicitly opted in OR caller
|
|
510
580
|
// passed dryRun:false meaning "I know what I'm doing".
|
|
511
581
|
const dryRun = opts.dryRun !== undefined ? opts.dryRun : state.optedIn !== true;
|
|
512
|
-
const
|
|
582
|
+
const transport = resolveTransport(opts);
|
|
583
|
+
const ctx = makeCtx(opts, dryRun, transport);
|
|
513
584
|
const result = emptyResult(dryRun);
|
|
514
|
-
if (!
|
|
515
|
-
ctx.log(
|
|
585
|
+
if (!transport.configured) {
|
|
586
|
+
ctx.log(`[skills-sync] skipping push: ${transport.reason}`);
|
|
516
587
|
result.ok = false;
|
|
517
|
-
result.reason = '
|
|
588
|
+
result.reason = transport.reason ?? 'transport not configured';
|
|
518
589
|
return result;
|
|
519
590
|
}
|
|
520
591
|
await pushOnce(ctx, state, result);
|
|
@@ -525,12 +596,13 @@ export async function pushSkills(opts = {}) {
|
|
|
525
596
|
export async function pullSkills(opts = {}) {
|
|
526
597
|
const state = loadState(opts.stateFile ?? defaultStateFile(opts.skillsDir ?? defaultSkillsDir()));
|
|
527
598
|
const dryRun = opts.dryRun !== undefined ? opts.dryRun : state.optedIn !== true;
|
|
528
|
-
const
|
|
599
|
+
const transport = resolveTransport(opts);
|
|
600
|
+
const ctx = makeCtx(opts, dryRun, transport);
|
|
529
601
|
const result = emptyResult(dryRun);
|
|
530
|
-
if (!
|
|
531
|
-
ctx.log(
|
|
602
|
+
if (!transport.configured) {
|
|
603
|
+
ctx.log(`[skills-sync] skipping pull: ${transport.reason}`);
|
|
532
604
|
result.ok = false;
|
|
533
|
-
result.reason = '
|
|
605
|
+
result.reason = transport.reason ?? 'transport not configured';
|
|
534
606
|
return result;
|
|
535
607
|
}
|
|
536
608
|
await pullOnce(ctx, state, result);
|
|
@@ -541,12 +613,13 @@ export async function pullSkills(opts = {}) {
|
|
|
541
613
|
export async function syncSkills(opts = {}) {
|
|
542
614
|
const state = loadState(opts.stateFile ?? defaultStateFile(opts.skillsDir ?? defaultSkillsDir()));
|
|
543
615
|
const dryRun = opts.dryRun !== undefined ? opts.dryRun : state.optedIn !== true;
|
|
544
|
-
const
|
|
616
|
+
const transport = resolveTransport(opts);
|
|
617
|
+
const ctx = makeCtx(opts, dryRun, transport);
|
|
545
618
|
const result = emptyResult(dryRun);
|
|
546
|
-
if (!
|
|
547
|
-
ctx.log(
|
|
619
|
+
if (!transport.configured) {
|
|
620
|
+
ctx.log(`[skills-sync] skipping sync: ${transport.reason}`);
|
|
548
621
|
result.ok = false;
|
|
549
|
-
result.reason = '
|
|
622
|
+
result.reason = transport.reason ?? 'transport not configured';
|
|
550
623
|
return result;
|
|
551
624
|
}
|
|
552
625
|
// Pull first so a brand-new workstation gets the team library before
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fruition/fcp-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@types/node": "^
|
|
42
|
+
"@types/node": "^25.6.2",
|
|
43
43
|
"tsx": "^4.7.0",
|
|
44
44
|
"typescript": "^5.3.0"
|
|
45
45
|
}
|