@fruition/fcp-mcp-server 1.3.0 → 1.5.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 +14 -1
- package/dist/index.js +455 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ MCP (Model Context Protocol) server that gives Claude Code direct access to the
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
### Tools
|
|
7
|
+
### FCP Launch Management Tools
|
|
8
8
|
|
|
9
9
|
| Tool | Description |
|
|
10
10
|
|------|-------------|
|
|
@@ -16,6 +16,19 @@ 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
|
+
### Unroo Task Management Tools
|
|
20
|
+
|
|
21
|
+
| Tool | Description |
|
|
22
|
+
|------|-------------|
|
|
23
|
+
| `unroo_list_projects` | List all Unroo projects mapped to FCP clients |
|
|
24
|
+
| `unroo_list_tasks` | Query tasks with filters (status, project, assignee) |
|
|
25
|
+
| `unroo_create_task` | Create new tasks for discovered issues or follow-ups |
|
|
26
|
+
| `unroo_update_task` | Update task status, hours logged, priority |
|
|
27
|
+
| `unroo_get_my_tasks` | Get tasks assigned to current user |
|
|
28
|
+
| `unroo_start_session` | Start work session for time tracking |
|
|
29
|
+
| `unroo_end_session` | End work session and log time |
|
|
30
|
+
| `unroo_create_follow_up` | Create follow-up task linked to a parent |
|
|
31
|
+
|
|
19
32
|
### Resources
|
|
20
33
|
|
|
21
34
|
- `fcp://launches` - List of all launches
|
package/dist/index.js
CHANGED
|
@@ -33,20 +33,104 @@ catch {
|
|
|
33
33
|
console.error('[MCP Server] Warning: Could not read package.json version');
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
import { execSync } from 'child_process';
|
|
36
37
|
// Configuration
|
|
37
38
|
const FCP_API_URL = process.env.FCP_API_URL || 'https://fcp.fru.io';
|
|
38
39
|
const FCP_API_TOKEN = process.env.FCP_API_TOKEN || '';
|
|
40
|
+
// Unroo API can be called directly (legacy) or proxied through FCP (recommended)
|
|
41
|
+
// When UNROO_API_KEY is not set, Unroo calls go through FCP's proxy at /api/mcp/unroo/*
|
|
39
42
|
const UNROO_API_URL = process.env.UNROO_API_URL || 'https://chat.frugpt.com';
|
|
40
43
|
const UNROO_API_KEY = process.env.UNROO_API_KEY || '';
|
|
44
|
+
// If no Unroo key, use FCP as proxy (unified key setup)
|
|
45
|
+
const USE_FCP_UNROO_PROXY = !UNROO_API_KEY;
|
|
46
|
+
// Helper to check if Unroo functionality is available (either mode)
|
|
47
|
+
const UNROO_AVAILABLE = UNROO_API_KEY || (USE_FCP_UNROO_PROXY && FCP_API_TOKEN);
|
|
48
|
+
let currentProject = null;
|
|
49
|
+
/**
|
|
50
|
+
* Detect the current git remote URL
|
|
51
|
+
*/
|
|
52
|
+
function detectGitRemote() {
|
|
53
|
+
try {
|
|
54
|
+
const remote = execSync('git config --get remote.origin.url', {
|
|
55
|
+
encoding: 'utf-8',
|
|
56
|
+
timeout: 5000,
|
|
57
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
58
|
+
}).trim();
|
|
59
|
+
return remote || null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Not a git repo or no remote configured
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolve project from git remote URL via FCP API
|
|
68
|
+
*/
|
|
69
|
+
async function resolveProjectFromRepo(repoUrl) {
|
|
70
|
+
if (!FCP_API_URL) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const url = `${FCP_API_URL}/api/mcp/resolve-project?repo_url=${encodeURIComponent(repoUrl)}`;
|
|
75
|
+
const headers = {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
};
|
|
78
|
+
if (FCP_API_TOKEN && FCP_API_TOKEN !== 'dev_bypass') {
|
|
79
|
+
headers['X-API-Key'] = FCP_API_TOKEN;
|
|
80
|
+
}
|
|
81
|
+
else if (FCP_API_TOKEN === 'dev_bypass') {
|
|
82
|
+
headers['X-Dev-Bypass'] = 'true';
|
|
83
|
+
}
|
|
84
|
+
const response = await fetch(url, { headers });
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
if (data.found && data.project) {
|
|
90
|
+
return {
|
|
91
|
+
project_key: data.project.project_key,
|
|
92
|
+
website_id: data.project.website_id,
|
|
93
|
+
domain: data.project.domain,
|
|
94
|
+
account_name: data.project.account_name || 'Unknown',
|
|
95
|
+
github: data.project.github,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.error('[MCP Server] Error resolving project:', error);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Initialize project detection on startup
|
|
107
|
+
*/
|
|
108
|
+
async function initializeProjectDetection() {
|
|
109
|
+
const remoteUrl = detectGitRemote();
|
|
110
|
+
if (!remoteUrl) {
|
|
111
|
+
console.error('[MCP Server] No git remote detected - project will be tracked as general');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
console.error(`[MCP Server] Detected git remote: ${remoteUrl}`);
|
|
115
|
+
const project = await resolveProjectFromRepo(remoteUrl);
|
|
116
|
+
if (project) {
|
|
117
|
+
currentProject = project;
|
|
118
|
+
console.error(`[MCP Server] Resolved project: ${project.domain} (${project.project_key})`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
console.error('[MCP Server] Could not resolve project from repo - check FCP website github config');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
41
124
|
// API Client
|
|
42
125
|
class FCPClient {
|
|
43
126
|
baseUrl;
|
|
44
127
|
token;
|
|
128
|
+
defaultTimeout = 15000; // 15 seconds
|
|
45
129
|
constructor(baseUrl, token) {
|
|
46
130
|
this.baseUrl = baseUrl;
|
|
47
131
|
this.token = token;
|
|
48
132
|
}
|
|
49
|
-
async fetch(path, options = {}) {
|
|
133
|
+
async fetch(path, options = {}, timeoutMs) {
|
|
50
134
|
const url = `${this.baseUrl}${path}`;
|
|
51
135
|
const headers = {
|
|
52
136
|
'Content-Type': 'application/json',
|
|
@@ -65,6 +149,7 @@ class FCPClient {
|
|
|
65
149
|
const response = await fetch(url, {
|
|
66
150
|
...options,
|
|
67
151
|
headers,
|
|
152
|
+
signal: AbortSignal.timeout(timeoutMs || this.defaultTimeout),
|
|
68
153
|
});
|
|
69
154
|
if (!response.ok) {
|
|
70
155
|
const error = await response.text();
|
|
@@ -105,27 +190,61 @@ class FCPClient {
|
|
|
105
190
|
}
|
|
106
191
|
}
|
|
107
192
|
// Unroo API Client
|
|
193
|
+
// Supports two modes:
|
|
194
|
+
// 1. Direct: Uses UNROO_API_KEY to call Unroo directly (legacy)
|
|
195
|
+
// 2. Proxy: Routes through FCP at /api/mcp/unroo/* using FCP_API_TOKEN (recommended)
|
|
108
196
|
class UnrooClient {
|
|
109
197
|
baseUrl;
|
|
110
198
|
apiKey;
|
|
111
|
-
|
|
199
|
+
useProxy;
|
|
200
|
+
fcpUrl;
|
|
201
|
+
fcpToken;
|
|
202
|
+
defaultTimeout = 15000; // 15 seconds
|
|
203
|
+
constructor(baseUrl, apiKey, useProxy = false, fcpUrl = '', fcpToken = '') {
|
|
112
204
|
this.baseUrl = baseUrl;
|
|
113
205
|
this.apiKey = apiKey;
|
|
206
|
+
this.useProxy = useProxy;
|
|
207
|
+
this.fcpUrl = fcpUrl;
|
|
208
|
+
this.fcpToken = fcpToken;
|
|
209
|
+
if (this.useProxy) {
|
|
210
|
+
console.error('[UnrooClient] Using FCP proxy mode (unified key)');
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
console.error('[UnrooClient] Using direct Unroo API mode');
|
|
214
|
+
}
|
|
114
215
|
}
|
|
115
|
-
async fetch(path, options = {}) {
|
|
116
|
-
|
|
216
|
+
async fetch(path, options = {}, timeoutMs) {
|
|
217
|
+
let url;
|
|
117
218
|
const headers = {
|
|
118
219
|
'Content-Type': 'application/json',
|
|
119
|
-
'X-API-Key': this.apiKey,
|
|
120
220
|
...(options.headers || {}),
|
|
121
221
|
};
|
|
222
|
+
if (this.useProxy) {
|
|
223
|
+
// Route through FCP proxy: /api/external/fcp/X -> /api/mcp/unroo/X
|
|
224
|
+
const proxyPath = path.replace('/api/external/fcp/', '/api/mcp/unroo/');
|
|
225
|
+
url = `${this.fcpUrl}${proxyPath}`;
|
|
226
|
+
// Use FCP API token
|
|
227
|
+
if (this.fcpToken && this.fcpToken !== 'dev_bypass') {
|
|
228
|
+
headers['X-API-Key'] = this.fcpToken;
|
|
229
|
+
}
|
|
230
|
+
else if (this.fcpToken === 'dev_bypass') {
|
|
231
|
+
headers['X-Dev-Bypass'] = 'true';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Direct Unroo API call
|
|
236
|
+
url = `${this.baseUrl}${path}`;
|
|
237
|
+
headers['X-API-Key'] = this.apiKey;
|
|
238
|
+
}
|
|
122
239
|
const response = await fetch(url, {
|
|
123
240
|
...options,
|
|
124
241
|
headers,
|
|
242
|
+
signal: AbortSignal.timeout(timeoutMs || this.defaultTimeout),
|
|
125
243
|
});
|
|
126
244
|
if (!response.ok) {
|
|
127
245
|
const error = await response.text();
|
|
128
|
-
|
|
246
|
+
const source = this.useProxy ? 'FCP Proxy' : 'Unroo API';
|
|
247
|
+
throw new Error(`${source} error (${response.status}): ${error}`);
|
|
129
248
|
}
|
|
130
249
|
return response.json();
|
|
131
250
|
}
|
|
@@ -180,6 +299,64 @@ class UnrooClient {
|
|
|
180
299
|
body: JSON.stringify({ action: 'heartbeat', ...input }),
|
|
181
300
|
});
|
|
182
301
|
}
|
|
302
|
+
async logActivity(taskId, input) {
|
|
303
|
+
return this.fetch(`/api/external/fcp/tasks/${encodeURIComponent(taskId)}/activity`, {
|
|
304
|
+
method: 'POST',
|
|
305
|
+
body: JSON.stringify({
|
|
306
|
+
activity_type: input.activity_type,
|
|
307
|
+
source: input.source || 'claude_code',
|
|
308
|
+
field_changed: input.field_changed,
|
|
309
|
+
old_value: input.old_value,
|
|
310
|
+
new_value: input.new_value,
|
|
311
|
+
metadata: input.metadata,
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// ============================================================================
|
|
316
|
+
// Parking Lot / Backlog APIs
|
|
317
|
+
// ============================================================================
|
|
318
|
+
async logFutureWork(input) {
|
|
319
|
+
return this.fetch('/api/external/fcp/future-work', {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
body: JSON.stringify(input),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
async getFutureWork(filters) {
|
|
325
|
+
const params = new URLSearchParams();
|
|
326
|
+
if (filters.project_key)
|
|
327
|
+
params.set('project_key', filters.project_key);
|
|
328
|
+
if (filters.account_id)
|
|
329
|
+
params.set('account_id', filters.account_id);
|
|
330
|
+
if (filters.status)
|
|
331
|
+
params.set('status', filters.status);
|
|
332
|
+
if (filters.destination)
|
|
333
|
+
params.set('destination', filters.destination);
|
|
334
|
+
if (filters.limit)
|
|
335
|
+
params.set('limit', filters.limit.toString());
|
|
336
|
+
if (filters.offset)
|
|
337
|
+
params.set('offset', filters.offset.toString());
|
|
338
|
+
const query = params.toString();
|
|
339
|
+
return this.fetch(`/api/external/fcp/future-work${query ? `?${query}` : ''}`);
|
|
340
|
+
}
|
|
341
|
+
async updateFutureWork(id, updates) {
|
|
342
|
+
return this.fetch(`/api/external/fcp/future-work/${encodeURIComponent(id)}`, {
|
|
343
|
+
method: 'PUT',
|
|
344
|
+
body: JSON.stringify(updates),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
async getBacklog(filters) {
|
|
348
|
+
const params = new URLSearchParams();
|
|
349
|
+
if (filters.project_key)
|
|
350
|
+
params.set('project_key', filters.project_key);
|
|
351
|
+
if (filters.priority)
|
|
352
|
+
params.set('priority', filters.priority);
|
|
353
|
+
if (filters.limit)
|
|
354
|
+
params.set('limit', filters.limit.toString());
|
|
355
|
+
if (filters.offset)
|
|
356
|
+
params.set('offset', filters.offset.toString());
|
|
357
|
+
const query = params.toString();
|
|
358
|
+
return this.fetch(`/api/external/fcp/backlog${query ? `?${query}` : ''}`);
|
|
359
|
+
}
|
|
183
360
|
}
|
|
184
361
|
// ============================================================================
|
|
185
362
|
// Auto Session Tracking
|
|
@@ -192,6 +369,8 @@ class SessionTracker {
|
|
|
192
369
|
activities = [];
|
|
193
370
|
heartbeatInterval = null;
|
|
194
371
|
currentTaskId = null;
|
|
372
|
+
consecutiveHeartbeatFailures = 0;
|
|
373
|
+
static MAX_HEARTBEAT_FAILURES = 3;
|
|
195
374
|
constructor(unrooClient) {
|
|
196
375
|
this.unrooClient = unrooClient;
|
|
197
376
|
}
|
|
@@ -210,7 +389,8 @@ class SessionTracker {
|
|
|
210
389
|
this.activities = this.activities.slice(-50);
|
|
211
390
|
}
|
|
212
391
|
// Auto-start session on first tool call
|
|
213
|
-
|
|
392
|
+
// Session tracking works with either direct Unroo key OR FCP proxy mode
|
|
393
|
+
if (!this.sessionActive && UNROO_AVAILABLE) {
|
|
214
394
|
await this.startSession();
|
|
215
395
|
}
|
|
216
396
|
// Send heartbeat every 30 tool calls or every 5 minutes
|
|
@@ -230,22 +410,39 @@ class SessionTracker {
|
|
|
230
410
|
* Start a new session
|
|
231
411
|
*/
|
|
232
412
|
async startSession() {
|
|
233
|
-
if (!
|
|
413
|
+
if (!UNROO_AVAILABLE) {
|
|
234
414
|
return;
|
|
235
415
|
}
|
|
236
416
|
try {
|
|
237
|
-
|
|
417
|
+
// Use auto-detected project if available
|
|
418
|
+
const sessionInput = {
|
|
238
419
|
task_id: this.currentTaskId || undefined,
|
|
239
420
|
source: 'claude-code-mcp',
|
|
240
421
|
machine_id: process.env.HOSTNAME || 'unknown',
|
|
241
|
-
}
|
|
422
|
+
};
|
|
423
|
+
// Add auto-detected project info
|
|
424
|
+
if (currentProject) {
|
|
425
|
+
sessionInput.project_key = currentProject.project_key;
|
|
426
|
+
sessionInput.repo_name = `${currentProject.github.owner}/${currentProject.github.repo}`;
|
|
427
|
+
}
|
|
428
|
+
await this.unrooClient.startSession(sessionInput);
|
|
242
429
|
this.sessionActive = true;
|
|
243
430
|
this.lastHeartbeat = new Date();
|
|
244
431
|
// Set up periodic heartbeat (every 5 minutes)
|
|
245
432
|
this.heartbeatInterval = setInterval(() => {
|
|
246
|
-
this.sendHeartbeat().catch(() => {
|
|
433
|
+
this.sendHeartbeat().catch((err) => {
|
|
434
|
+
this.consecutiveHeartbeatFailures++;
|
|
435
|
+
if (this.consecutiveHeartbeatFailures >= SessionTracker.MAX_HEARTBEAT_FAILURES) {
|
|
436
|
+
console.error(`[SessionTracker] Heartbeat failed ${this.consecutiveHeartbeatFailures}x - session tracking may not work`);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
247
439
|
}, 5 * 60 * 1000);
|
|
248
|
-
|
|
440
|
+
if (currentProject) {
|
|
441
|
+
console.error(`[SessionTracker] Session started for project: ${currentProject.domain}`);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
console.error('[SessionTracker] Session started (no project detected)');
|
|
445
|
+
}
|
|
249
446
|
}
|
|
250
447
|
catch (error) {
|
|
251
448
|
console.error('[SessionTracker] Failed to start session:', error);
|
|
@@ -255,7 +452,7 @@ class SessionTracker {
|
|
|
255
452
|
* Send a heartbeat to keep session alive
|
|
256
453
|
*/
|
|
257
454
|
async sendHeartbeat() {
|
|
258
|
-
if (!this.sessionActive || !
|
|
455
|
+
if (!this.sessionActive || !UNROO_AVAILABLE) {
|
|
259
456
|
return;
|
|
260
457
|
}
|
|
261
458
|
try {
|
|
@@ -265,17 +462,20 @@ class SessionTracker {
|
|
|
265
462
|
});
|
|
266
463
|
this.lastHeartbeat = new Date();
|
|
267
464
|
this.toolCallCount = 0;
|
|
465
|
+
this.consecutiveHeartbeatFailures = 0; // Reset on success
|
|
268
466
|
console.error('[SessionTracker] Heartbeat sent');
|
|
269
467
|
}
|
|
270
468
|
catch (error) {
|
|
271
|
-
|
|
469
|
+
this.consecutiveHeartbeatFailures++;
|
|
470
|
+
console.error(`[SessionTracker] Failed to send heartbeat (attempt ${this.consecutiveHeartbeatFailures}):`, error);
|
|
471
|
+
throw error; // Re-throw for interval handler
|
|
272
472
|
}
|
|
273
473
|
}
|
|
274
474
|
/**
|
|
275
|
-
* End the current session
|
|
475
|
+
* End the current session and log activity summary to task
|
|
276
476
|
*/
|
|
277
477
|
async endSession() {
|
|
278
|
-
if (!this.sessionActive || !
|
|
478
|
+
if (!this.sessionActive || !UNROO_AVAILABLE) {
|
|
279
479
|
return;
|
|
280
480
|
}
|
|
281
481
|
if (this.heartbeatInterval) {
|
|
@@ -283,9 +483,34 @@ class SessionTracker {
|
|
|
283
483
|
this.heartbeatInterval = null;
|
|
284
484
|
}
|
|
285
485
|
try {
|
|
286
|
-
|
|
486
|
+
// End the session first
|
|
487
|
+
const result = await this.unrooClient.endSession();
|
|
287
488
|
this.sessionActive = false;
|
|
288
489
|
console.error('[SessionTracker] Session ended');
|
|
490
|
+
// Log activity summary to the task if we have a current task
|
|
491
|
+
if (this.currentTaskId && result.session) {
|
|
492
|
+
try {
|
|
493
|
+
const session = result.session;
|
|
494
|
+
const uniqueTools = [...new Set(this.activities.map(a => a.tool))];
|
|
495
|
+
await this.unrooClient.logActivity(this.currentTaskId, {
|
|
496
|
+
activity_type: 'claude_code_session',
|
|
497
|
+
source: 'claude_code',
|
|
498
|
+
metadata: {
|
|
499
|
+
session_id: session.id,
|
|
500
|
+
duration_seconds: session.duration_minutes ? session.duration_minutes * 60 : 0,
|
|
501
|
+
total_tool_calls: session.total_tool_calls || this.toolCallCount,
|
|
502
|
+
tools_used: uniqueTools.slice(0, 20), // Limit to 20 tools
|
|
503
|
+
started_at: session.started_at,
|
|
504
|
+
ended_at: session.ended_at || new Date().toISOString(),
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
console.error(`[SessionTracker] Logged session activity to task ${this.currentTaskId}`);
|
|
508
|
+
}
|
|
509
|
+
catch (activityError) {
|
|
510
|
+
console.error('[SessionTracker] Failed to log activity to task:', activityError);
|
|
511
|
+
// Don't throw - session end was successful
|
|
512
|
+
}
|
|
513
|
+
}
|
|
289
514
|
}
|
|
290
515
|
catch (error) {
|
|
291
516
|
console.error('[SessionTracker] Failed to end session:', error);
|
|
@@ -314,7 +539,7 @@ const server = new Server({
|
|
|
314
539
|
},
|
|
315
540
|
});
|
|
316
541
|
const client = new FCPClient(FCP_API_URL, FCP_API_TOKEN);
|
|
317
|
-
const unrooClient = new UnrooClient(UNROO_API_URL, UNROO_API_KEY);
|
|
542
|
+
const unrooClient = new UnrooClient(UNROO_API_URL, UNROO_API_KEY, USE_FCP_UNROO_PROXY, FCP_API_URL, FCP_API_TOKEN);
|
|
318
543
|
const sessionTracker = new SessionTracker(unrooClient);
|
|
319
544
|
// Tool definitions
|
|
320
545
|
const TOOLS = [
|
|
@@ -662,6 +887,126 @@ const TOOLS = [
|
|
|
662
887
|
required: ['parent_task_id', 'title'],
|
|
663
888
|
},
|
|
664
889
|
},
|
|
890
|
+
// Parking Lot / Backlog Tools
|
|
891
|
+
{
|
|
892
|
+
name: 'unroo_log_future_work',
|
|
893
|
+
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.',
|
|
894
|
+
inputSchema: {
|
|
895
|
+
type: 'object',
|
|
896
|
+
properties: {
|
|
897
|
+
title: {
|
|
898
|
+
type: 'string',
|
|
899
|
+
description: 'Title of the future work item (required)',
|
|
900
|
+
},
|
|
901
|
+
project_key: {
|
|
902
|
+
type: 'string',
|
|
903
|
+
description: 'JIRA project key or FCP-SITE-{id} (required)',
|
|
904
|
+
},
|
|
905
|
+
description: {
|
|
906
|
+
type: 'string',
|
|
907
|
+
description: 'Detailed description of the work needed',
|
|
908
|
+
},
|
|
909
|
+
priority: {
|
|
910
|
+
type: 'string',
|
|
911
|
+
enum: ['Urgent', 'High', 'Medium', 'Low'],
|
|
912
|
+
description: 'Priority level (default: Medium)',
|
|
913
|
+
},
|
|
914
|
+
task_type: {
|
|
915
|
+
type: 'string',
|
|
916
|
+
enum: ['bug', 'tech_debt', 'feature', 'documentation', 'security', 'performance'],
|
|
917
|
+
description: 'Type of work item',
|
|
918
|
+
},
|
|
919
|
+
estimated_hours: {
|
|
920
|
+
type: 'number',
|
|
921
|
+
description: 'Estimated hours to complete',
|
|
922
|
+
},
|
|
923
|
+
launch_id: {
|
|
924
|
+
type: 'number',
|
|
925
|
+
description: 'FCP launch ID if related to a launch',
|
|
926
|
+
},
|
|
927
|
+
checklist_item_id: {
|
|
928
|
+
type: 'number',
|
|
929
|
+
description: 'FCP checklist item ID if discovered during checklist work',
|
|
930
|
+
},
|
|
931
|
+
notes: {
|
|
932
|
+
type: 'string',
|
|
933
|
+
description: 'Additional notes or context',
|
|
934
|
+
},
|
|
935
|
+
destination: {
|
|
936
|
+
type: 'string',
|
|
937
|
+
enum: ['parking_lot', 'backlog'],
|
|
938
|
+
description: 'Where to put the item: parking_lot (needs review) or backlog (ready for sprint). Default: parking_lot',
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
required: ['title', 'project_key'],
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
name: 'unroo_get_parking_lot',
|
|
946
|
+
description: 'Get items from the parking lot - discovered work that needs review before being added to backlog.',
|
|
947
|
+
inputSchema: {
|
|
948
|
+
type: 'object',
|
|
949
|
+
properties: {
|
|
950
|
+
project_key: {
|
|
951
|
+
type: 'string',
|
|
952
|
+
description: 'Filter by JIRA project key or FCP-SITE-{id}',
|
|
953
|
+
},
|
|
954
|
+
status: {
|
|
955
|
+
type: 'string',
|
|
956
|
+
description: 'Filter by status: pending, approved, rejected, converted (default: pending)',
|
|
957
|
+
},
|
|
958
|
+
limit: {
|
|
959
|
+
type: 'number',
|
|
960
|
+
description: 'Maximum number of items to return (default: 100)',
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
name: 'unroo_get_backlog',
|
|
967
|
+
description: 'Get backlog items - tasks ready to be scheduled into sprints.',
|
|
968
|
+
inputSchema: {
|
|
969
|
+
type: 'object',
|
|
970
|
+
properties: {
|
|
971
|
+
project_key: {
|
|
972
|
+
type: 'string',
|
|
973
|
+
description: 'Filter by JIRA project key or FCP-SITE-{id}',
|
|
974
|
+
},
|
|
975
|
+
priority: {
|
|
976
|
+
type: 'string',
|
|
977
|
+
enum: ['Urgent', 'High', 'Medium', 'Low'],
|
|
978
|
+
description: 'Filter by priority',
|
|
979
|
+
},
|
|
980
|
+
limit: {
|
|
981
|
+
type: 'number',
|
|
982
|
+
description: 'Maximum number of items to return (default: 100)',
|
|
983
|
+
},
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
name: 'unroo_convert_to_backlog',
|
|
989
|
+
description: 'Convert a parking lot item to backlog. Use after reviewing a discovered item and deciding it should be done.',
|
|
990
|
+
inputSchema: {
|
|
991
|
+
type: 'object',
|
|
992
|
+
properties: {
|
|
993
|
+
id: {
|
|
994
|
+
type: 'string',
|
|
995
|
+
description: 'The ID of the parking lot item to convert (required)',
|
|
996
|
+
},
|
|
997
|
+
priority: {
|
|
998
|
+
type: 'string',
|
|
999
|
+
enum: ['Urgent', 'High', 'Medium', 'Low'],
|
|
1000
|
+
description: 'Priority for the backlog item (optional, keeps original if not specified)',
|
|
1001
|
+
},
|
|
1002
|
+
notes: {
|
|
1003
|
+
type: 'string',
|
|
1004
|
+
description: 'Notes about the conversion decision',
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
1007
|
+
required: ['id'],
|
|
1008
|
+
},
|
|
1009
|
+
},
|
|
665
1010
|
];
|
|
666
1011
|
// Register tool handlers
|
|
667
1012
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -952,6 +1297,89 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
952
1297
|
],
|
|
953
1298
|
};
|
|
954
1299
|
}
|
|
1300
|
+
// Parking Lot / Backlog Handlers
|
|
1301
|
+
case 'unroo_log_future_work': {
|
|
1302
|
+
const input = args;
|
|
1303
|
+
const result = await unrooClient.logFutureWork({
|
|
1304
|
+
...input,
|
|
1305
|
+
discovered_by: 'claude-code-mcp',
|
|
1306
|
+
});
|
|
1307
|
+
return {
|
|
1308
|
+
content: [
|
|
1309
|
+
{
|
|
1310
|
+
type: 'text',
|
|
1311
|
+
text: JSON.stringify({
|
|
1312
|
+
success: true,
|
|
1313
|
+
message: result.message,
|
|
1314
|
+
id: result.id,
|
|
1315
|
+
destination: result.destination,
|
|
1316
|
+
}, null, 2),
|
|
1317
|
+
},
|
|
1318
|
+
],
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
case 'unroo_get_parking_lot': {
|
|
1322
|
+
const { project_key, status, limit } = args;
|
|
1323
|
+
const result = await unrooClient.getFutureWork({
|
|
1324
|
+
project_key,
|
|
1325
|
+
status: status || 'pending',
|
|
1326
|
+
destination: 'parking_lot',
|
|
1327
|
+
limit: limit || 100,
|
|
1328
|
+
});
|
|
1329
|
+
return {
|
|
1330
|
+
content: [
|
|
1331
|
+
{
|
|
1332
|
+
type: 'text',
|
|
1333
|
+
text: JSON.stringify({
|
|
1334
|
+
success: true,
|
|
1335
|
+
total: result.items.length,
|
|
1336
|
+
stats: result.stats,
|
|
1337
|
+
items: result.items,
|
|
1338
|
+
}, null, 2),
|
|
1339
|
+
},
|
|
1340
|
+
],
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
case 'unroo_get_backlog': {
|
|
1344
|
+
const { project_key, priority, limit } = args;
|
|
1345
|
+
const result = await unrooClient.getBacklog({
|
|
1346
|
+
project_key,
|
|
1347
|
+
priority,
|
|
1348
|
+
limit: limit || 100,
|
|
1349
|
+
});
|
|
1350
|
+
return {
|
|
1351
|
+
content: [
|
|
1352
|
+
{
|
|
1353
|
+
type: 'text',
|
|
1354
|
+
text: JSON.stringify({
|
|
1355
|
+
success: true,
|
|
1356
|
+
total: result.total,
|
|
1357
|
+
items: result.items,
|
|
1358
|
+
}, null, 2),
|
|
1359
|
+
},
|
|
1360
|
+
],
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
case 'unroo_convert_to_backlog': {
|
|
1364
|
+
const { id, priority, notes } = args;
|
|
1365
|
+
const result = await unrooClient.updateFutureWork(id, {
|
|
1366
|
+
convert_to_backlog: true,
|
|
1367
|
+
priority,
|
|
1368
|
+
notes,
|
|
1369
|
+
});
|
|
1370
|
+
return {
|
|
1371
|
+
content: [
|
|
1372
|
+
{
|
|
1373
|
+
type: 'text',
|
|
1374
|
+
text: JSON.stringify({
|
|
1375
|
+
success: true,
|
|
1376
|
+
message: `Parking lot item ${id} converted to backlog`,
|
|
1377
|
+
item: result.item,
|
|
1378
|
+
}, null, 2),
|
|
1379
|
+
},
|
|
1380
|
+
],
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
955
1383
|
default:
|
|
956
1384
|
throw new Error(`Unknown tool: ${name}`);
|
|
957
1385
|
}
|
|
@@ -1065,6 +1493,15 @@ async function main() {
|
|
|
1065
1493
|
const transport = new StdioServerTransport();
|
|
1066
1494
|
await server.connect(transport);
|
|
1067
1495
|
console.error(`FCP MCP Server v${MCP_SERVER_VERSION} running on stdio`);
|
|
1496
|
+
console.error(` FCP API: ${FCP_API_URL}`);
|
|
1497
|
+
if (USE_FCP_UNROO_PROXY) {
|
|
1498
|
+
console.error(' Unroo: via FCP proxy (unified key mode)');
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
console.error(` Unroo: direct (${UNROO_API_URL})`);
|
|
1502
|
+
}
|
|
1503
|
+
// Auto-detect project from git remote (non-blocking)
|
|
1504
|
+
initializeProjectDetection();
|
|
1068
1505
|
// Check for updates (non-blocking)
|
|
1069
1506
|
checkForUpdates();
|
|
1070
1507
|
// Handle graceful shutdown
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fruition/fcp-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|