@fruition/fcp-mcp-server 1.4.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.
Files changed (3) hide show
  1. package/README.md +14 -1
  2. package/dist/index.js +321 -13
  3. 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
@@ -37,8 +37,14 @@ import { execSync } from 'child_process';
37
37
  // Configuration
38
38
  const FCP_API_URL = process.env.FCP_API_URL || 'https://fcp.fru.io';
39
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/*
40
42
  const UNROO_API_URL = process.env.UNROO_API_URL || 'https://chat.frugpt.com';
41
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);
42
48
  let currentProject = null;
43
49
  /**
44
50
  * Detect the current git remote URL
@@ -119,11 +125,12 @@ async function initializeProjectDetection() {
119
125
  class FCPClient {
120
126
  baseUrl;
121
127
  token;
128
+ defaultTimeout = 15000; // 15 seconds
122
129
  constructor(baseUrl, token) {
123
130
  this.baseUrl = baseUrl;
124
131
  this.token = token;
125
132
  }
126
- async fetch(path, options = {}) {
133
+ async fetch(path, options = {}, timeoutMs) {
127
134
  const url = `${this.baseUrl}${path}`;
128
135
  const headers = {
129
136
  'Content-Type': 'application/json',
@@ -142,6 +149,7 @@ class FCPClient {
142
149
  const response = await fetch(url, {
143
150
  ...options,
144
151
  headers,
152
+ signal: AbortSignal.timeout(timeoutMs || this.defaultTimeout),
145
153
  });
146
154
  if (!response.ok) {
147
155
  const error = await response.text();
@@ -182,27 +190,61 @@ class FCPClient {
182
190
  }
183
191
  }
184
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)
185
196
  class UnrooClient {
186
197
  baseUrl;
187
198
  apiKey;
188
- constructor(baseUrl, apiKey) {
199
+ useProxy;
200
+ fcpUrl;
201
+ fcpToken;
202
+ defaultTimeout = 15000; // 15 seconds
203
+ constructor(baseUrl, apiKey, useProxy = false, fcpUrl = '', fcpToken = '') {
189
204
  this.baseUrl = baseUrl;
190
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
+ }
191
215
  }
192
- async fetch(path, options = {}) {
193
- const url = `${this.baseUrl}${path}`;
216
+ async fetch(path, options = {}, timeoutMs) {
217
+ let url;
194
218
  const headers = {
195
219
  'Content-Type': 'application/json',
196
- 'X-API-Key': this.apiKey,
197
220
  ...(options.headers || {}),
198
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
+ }
199
239
  const response = await fetch(url, {
200
240
  ...options,
201
241
  headers,
242
+ signal: AbortSignal.timeout(timeoutMs || this.defaultTimeout),
202
243
  });
203
244
  if (!response.ok) {
204
245
  const error = await response.text();
205
- throw new Error(`Unroo API error (${response.status}): ${error}`);
246
+ const source = this.useProxy ? 'FCP Proxy' : 'Unroo API';
247
+ throw new Error(`${source} error (${response.status}): ${error}`);
206
248
  }
207
249
  return response.json();
208
250
  }
@@ -270,6 +312,51 @@ class UnrooClient {
270
312
  }),
271
313
  });
272
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
+ }
273
360
  }
274
361
  // ============================================================================
275
362
  // Auto Session Tracking
@@ -282,6 +369,8 @@ class SessionTracker {
282
369
  activities = [];
283
370
  heartbeatInterval = null;
284
371
  currentTaskId = null;
372
+ consecutiveHeartbeatFailures = 0;
373
+ static MAX_HEARTBEAT_FAILURES = 3;
285
374
  constructor(unrooClient) {
286
375
  this.unrooClient = unrooClient;
287
376
  }
@@ -300,7 +389,8 @@ class SessionTracker {
300
389
  this.activities = this.activities.slice(-50);
301
390
  }
302
391
  // Auto-start session on first tool call
303
- if (!this.sessionActive && UNROO_API_KEY) {
392
+ // Session tracking works with either direct Unroo key OR FCP proxy mode
393
+ if (!this.sessionActive && UNROO_AVAILABLE) {
304
394
  await this.startSession();
305
395
  }
306
396
  // Send heartbeat every 30 tool calls or every 5 minutes
@@ -320,7 +410,7 @@ class SessionTracker {
320
410
  * Start a new session
321
411
  */
322
412
  async startSession() {
323
- if (!UNROO_API_KEY) {
413
+ if (!UNROO_AVAILABLE) {
324
414
  return;
325
415
  }
326
416
  try {
@@ -340,7 +430,12 @@ class SessionTracker {
340
430
  this.lastHeartbeat = new Date();
341
431
  // Set up periodic heartbeat (every 5 minutes)
342
432
  this.heartbeatInterval = setInterval(() => {
343
- 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
+ });
344
439
  }, 5 * 60 * 1000);
345
440
  if (currentProject) {
346
441
  console.error(`[SessionTracker] Session started for project: ${currentProject.domain}`);
@@ -357,7 +452,7 @@ class SessionTracker {
357
452
  * Send a heartbeat to keep session alive
358
453
  */
359
454
  async sendHeartbeat() {
360
- if (!this.sessionActive || !UNROO_API_KEY) {
455
+ if (!this.sessionActive || !UNROO_AVAILABLE) {
361
456
  return;
362
457
  }
363
458
  try {
@@ -367,17 +462,20 @@ class SessionTracker {
367
462
  });
368
463
  this.lastHeartbeat = new Date();
369
464
  this.toolCallCount = 0;
465
+ this.consecutiveHeartbeatFailures = 0; // Reset on success
370
466
  console.error('[SessionTracker] Heartbeat sent');
371
467
  }
372
468
  catch (error) {
373
- console.error('[SessionTracker] Failed to send heartbeat:', error);
469
+ this.consecutiveHeartbeatFailures++;
470
+ console.error(`[SessionTracker] Failed to send heartbeat (attempt ${this.consecutiveHeartbeatFailures}):`, error);
471
+ throw error; // Re-throw for interval handler
374
472
  }
375
473
  }
376
474
  /**
377
475
  * End the current session and log activity summary to task
378
476
  */
379
477
  async endSession() {
380
- if (!this.sessionActive || !UNROO_API_KEY) {
478
+ if (!this.sessionActive || !UNROO_AVAILABLE) {
381
479
  return;
382
480
  }
383
481
  if (this.heartbeatInterval) {
@@ -441,7 +539,7 @@ const server = new Server({
441
539
  },
442
540
  });
443
541
  const client = new FCPClient(FCP_API_URL, FCP_API_TOKEN);
444
- 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);
445
543
  const sessionTracker = new SessionTracker(unrooClient);
446
544
  // Tool definitions
447
545
  const TOOLS = [
@@ -789,6 +887,126 @@ const TOOLS = [
789
887
  required: ['parent_task_id', 'title'],
790
888
  },
791
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
+ },
792
1010
  ];
793
1011
  // Register tool handlers
794
1012
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -1079,6 +1297,89 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1079
1297
  ],
1080
1298
  };
1081
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
+ }
1082
1383
  default:
1083
1384
  throw new Error(`Unknown tool: ${name}`);
1084
1385
  }
@@ -1192,6 +1493,13 @@ async function main() {
1192
1493
  const transport = new StdioServerTransport();
1193
1494
  await server.connect(transport);
1194
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
+ }
1195
1503
  // Auto-detect project from git remote (non-blocking)
1196
1504
  initializeProjectDetection();
1197
1505
  // Check for updates (non-blocking)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fruition/fcp-mcp-server",
3
- "version": "1.4.0",
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",