@fruition/fcp-mcp-server 1.0.0 → 1.1.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/dist/index.js +577 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13,6 +13,8 @@ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSche
|
|
|
13
13
|
// Configuration
|
|
14
14
|
const FCP_API_URL = process.env.FCP_API_URL || 'https://fcp.fru.io';
|
|
15
15
|
const FCP_API_TOKEN = process.env.FCP_API_TOKEN || '';
|
|
16
|
+
const UNROO_API_URL = process.env.UNROO_API_URL || 'https://frugpt.fruition.net';
|
|
17
|
+
const UNROO_API_KEY = process.env.UNROO_API_KEY || '';
|
|
16
18
|
// API Client
|
|
17
19
|
class FCPClient {
|
|
18
20
|
baseUrl;
|
|
@@ -79,6 +81,205 @@ class FCPClient {
|
|
|
79
81
|
});
|
|
80
82
|
}
|
|
81
83
|
}
|
|
84
|
+
// Unroo API Client
|
|
85
|
+
class UnrooClient {
|
|
86
|
+
baseUrl;
|
|
87
|
+
apiKey;
|
|
88
|
+
constructor(baseUrl, apiKey) {
|
|
89
|
+
this.baseUrl = baseUrl;
|
|
90
|
+
this.apiKey = apiKey;
|
|
91
|
+
}
|
|
92
|
+
async fetch(path, options = {}) {
|
|
93
|
+
const url = `${this.baseUrl}${path}`;
|
|
94
|
+
const headers = {
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
'X-API-Key': this.apiKey,
|
|
97
|
+
...(options.headers || {}),
|
|
98
|
+
};
|
|
99
|
+
const response = await fetch(url, {
|
|
100
|
+
...options,
|
|
101
|
+
headers,
|
|
102
|
+
});
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const error = await response.text();
|
|
105
|
+
throw new Error(`Unroo API error (${response.status}): ${error}`);
|
|
106
|
+
}
|
|
107
|
+
return response.json();
|
|
108
|
+
}
|
|
109
|
+
async listProjects() {
|
|
110
|
+
return this.fetch('/api/external/fcp/projects');
|
|
111
|
+
}
|
|
112
|
+
async listTasks(filters) {
|
|
113
|
+
const params = new URLSearchParams();
|
|
114
|
+
if (filters.project_key)
|
|
115
|
+
params.set('project_key', filters.project_key);
|
|
116
|
+
if (filters.status)
|
|
117
|
+
params.set('status', filters.status);
|
|
118
|
+
if (filters.assignee_email)
|
|
119
|
+
params.set('assignee_email', filters.assignee_email);
|
|
120
|
+
if (filters.external_source_type)
|
|
121
|
+
params.set('external_source_type', filters.external_source_type);
|
|
122
|
+
if (filters.limit)
|
|
123
|
+
params.set('limit', filters.limit.toString());
|
|
124
|
+
const query = params.toString();
|
|
125
|
+
return this.fetch(`/api/external/fcp/tasks${query ? `?${query}` : ''}`);
|
|
126
|
+
}
|
|
127
|
+
async createTask(task) {
|
|
128
|
+
return this.fetch('/api/external/fcp/tasks', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
body: JSON.stringify(task),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async getTask(taskId) {
|
|
134
|
+
return this.fetch(`/api/external/fcp/tasks/${encodeURIComponent(taskId)}`);
|
|
135
|
+
}
|
|
136
|
+
async updateTask(taskId, updates) {
|
|
137
|
+
return this.fetch(`/api/external/fcp/tasks/${encodeURIComponent(taskId)}`, {
|
|
138
|
+
method: 'PUT',
|
|
139
|
+
body: JSON.stringify(updates),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async startSession(input) {
|
|
143
|
+
return this.fetch('/api/external/fcp/sessions', {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
body: JSON.stringify({ action: 'start', ...input }),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async endSession() {
|
|
149
|
+
return this.fetch('/api/external/fcp/sessions', {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
body: JSON.stringify({ action: 'end' }),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async sessionHeartbeat(input) {
|
|
155
|
+
return this.fetch('/api/external/fcp/sessions', {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
body: JSON.stringify({ action: 'heartbeat', ...input }),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// Auto Session Tracking
|
|
163
|
+
// ============================================================================
|
|
164
|
+
class SessionTracker {
|
|
165
|
+
unrooClient;
|
|
166
|
+
toolCallCount = 0;
|
|
167
|
+
sessionActive = false;
|
|
168
|
+
lastHeartbeat = null;
|
|
169
|
+
activities = [];
|
|
170
|
+
heartbeatInterval = null;
|
|
171
|
+
currentTaskId = null;
|
|
172
|
+
constructor(unrooClient) {
|
|
173
|
+
this.unrooClient = unrooClient;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Track a tool call - auto-starts session if needed
|
|
177
|
+
*/
|
|
178
|
+
async trackToolCall(toolName, description) {
|
|
179
|
+
this.toolCallCount++;
|
|
180
|
+
this.activities.push({
|
|
181
|
+
timestamp: new Date().toISOString(),
|
|
182
|
+
tool: toolName,
|
|
183
|
+
description: description || toolName,
|
|
184
|
+
});
|
|
185
|
+
// Keep last 50 activities
|
|
186
|
+
if (this.activities.length > 50) {
|
|
187
|
+
this.activities = this.activities.slice(-50);
|
|
188
|
+
}
|
|
189
|
+
// Auto-start session on first tool call
|
|
190
|
+
if (!this.sessionActive && UNROO_API_KEY) {
|
|
191
|
+
await this.startSession();
|
|
192
|
+
}
|
|
193
|
+
// Send heartbeat every 30 tool calls or every 5 minutes
|
|
194
|
+
const shouldHeartbeat = this.toolCallCount % 30 === 0 ||
|
|
195
|
+
(this.lastHeartbeat && Date.now() - this.lastHeartbeat.getTime() > 5 * 60 * 1000);
|
|
196
|
+
if (shouldHeartbeat && this.sessionActive) {
|
|
197
|
+
await this.sendHeartbeat();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Set the current task being worked on
|
|
202
|
+
*/
|
|
203
|
+
setCurrentTask(taskId) {
|
|
204
|
+
this.currentTaskId = taskId;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Start a new session
|
|
208
|
+
*/
|
|
209
|
+
async startSession() {
|
|
210
|
+
if (!UNROO_API_KEY) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
await this.unrooClient.startSession({
|
|
215
|
+
task_id: this.currentTaskId || undefined,
|
|
216
|
+
source: 'claude-code-mcp',
|
|
217
|
+
machine_id: process.env.HOSTNAME || 'unknown',
|
|
218
|
+
});
|
|
219
|
+
this.sessionActive = true;
|
|
220
|
+
this.lastHeartbeat = new Date();
|
|
221
|
+
// Set up periodic heartbeat (every 5 minutes)
|
|
222
|
+
this.heartbeatInterval = setInterval(() => {
|
|
223
|
+
this.sendHeartbeat().catch(() => { });
|
|
224
|
+
}, 5 * 60 * 1000);
|
|
225
|
+
console.error('[SessionTracker] Session started');
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
console.error('[SessionTracker] Failed to start session:', error);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Send a heartbeat to keep session alive
|
|
233
|
+
*/
|
|
234
|
+
async sendHeartbeat() {
|
|
235
|
+
if (!this.sessionActive || !UNROO_API_KEY) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
await this.unrooClient.sessionHeartbeat({
|
|
240
|
+
tool_calls_delta: this.toolCallCount,
|
|
241
|
+
activity: this.activities.slice(-10), // Send last 10 activities
|
|
242
|
+
});
|
|
243
|
+
this.lastHeartbeat = new Date();
|
|
244
|
+
this.toolCallCount = 0;
|
|
245
|
+
console.error('[SessionTracker] Heartbeat sent');
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
console.error('[SessionTracker] Failed to send heartbeat:', error);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* End the current session
|
|
253
|
+
*/
|
|
254
|
+
async endSession() {
|
|
255
|
+
if (!this.sessionActive || !UNROO_API_KEY) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (this.heartbeatInterval) {
|
|
259
|
+
clearInterval(this.heartbeatInterval);
|
|
260
|
+
this.heartbeatInterval = null;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
await this.unrooClient.endSession();
|
|
264
|
+
this.sessionActive = false;
|
|
265
|
+
console.error('[SessionTracker] Session ended');
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
console.error('[SessionTracker] Failed to end session:', error);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get session stats
|
|
273
|
+
*/
|
|
274
|
+
getStats() {
|
|
275
|
+
return {
|
|
276
|
+
active: this.sessionActive,
|
|
277
|
+
toolCalls: this.toolCallCount,
|
|
278
|
+
lastHeartbeat: this.lastHeartbeat?.toISOString() || null,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Session tracker instance created after clients below
|
|
82
283
|
// Create server
|
|
83
284
|
const server = new Server({
|
|
84
285
|
name: 'fcp-mcp-server',
|
|
@@ -90,6 +291,8 @@ const server = new Server({
|
|
|
90
291
|
},
|
|
91
292
|
});
|
|
92
293
|
const client = new FCPClient(FCP_API_URL, FCP_API_TOKEN);
|
|
294
|
+
const unrooClient = new UnrooClient(UNROO_API_URL, UNROO_API_KEY);
|
|
295
|
+
const sessionTracker = new SessionTracker(unrooClient);
|
|
93
296
|
// Tool definitions
|
|
94
297
|
const TOOLS = [
|
|
95
298
|
{
|
|
@@ -226,6 +429,216 @@ const TOOLS = [
|
|
|
226
429
|
required: ['launch_id'],
|
|
227
430
|
},
|
|
228
431
|
},
|
|
432
|
+
// Unroo Task Management Tools
|
|
433
|
+
{
|
|
434
|
+
name: 'unroo_list_projects',
|
|
435
|
+
description: 'List all Unroo projects mapped to FCP clients. Returns project mappings with organization and JIRA key info.',
|
|
436
|
+
inputSchema: {
|
|
437
|
+
type: 'object',
|
|
438
|
+
properties: {},
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: 'unroo_list_tasks',
|
|
443
|
+
description: 'List tasks from Unroo with optional filters. Use to find tasks for a project, by status, or by assignee.',
|
|
444
|
+
inputSchema: {
|
|
445
|
+
type: 'object',
|
|
446
|
+
properties: {
|
|
447
|
+
project_key: {
|
|
448
|
+
type: 'string',
|
|
449
|
+
description: 'Filter by JIRA project key or FCP-SITE-{id}',
|
|
450
|
+
},
|
|
451
|
+
status: {
|
|
452
|
+
type: 'string',
|
|
453
|
+
description: 'Filter by status: To Do, In Progress, Done, Blocked (comma-separated for multiple)',
|
|
454
|
+
},
|
|
455
|
+
assignee_email: {
|
|
456
|
+
type: 'string',
|
|
457
|
+
description: 'Filter by assignee email',
|
|
458
|
+
},
|
|
459
|
+
external_source_type: {
|
|
460
|
+
type: 'string',
|
|
461
|
+
description: 'Filter by source: fcp, fcp_checklist, fcp_launch',
|
|
462
|
+
},
|
|
463
|
+
limit: {
|
|
464
|
+
type: 'number',
|
|
465
|
+
description: 'Maximum number of tasks to return (default 100)',
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: 'unroo_create_task',
|
|
472
|
+
description: 'Create a new task in Unroo. Requires title and project_key. Use for follow-up tasks, discovered issues, or new work items.',
|
|
473
|
+
inputSchema: {
|
|
474
|
+
type: 'object',
|
|
475
|
+
properties: {
|
|
476
|
+
title: {
|
|
477
|
+
type: 'string',
|
|
478
|
+
description: 'Task title (required)',
|
|
479
|
+
},
|
|
480
|
+
description: {
|
|
481
|
+
type: 'string',
|
|
482
|
+
description: 'Detailed description of the task',
|
|
483
|
+
},
|
|
484
|
+
project_key: {
|
|
485
|
+
type: 'string',
|
|
486
|
+
description: 'JIRA project key or FCP-SITE-{id} (required)',
|
|
487
|
+
},
|
|
488
|
+
priority: {
|
|
489
|
+
type: 'string',
|
|
490
|
+
enum: ['urgent', 'high', 'medium', 'low'],
|
|
491
|
+
description: 'Task priority (default: medium)',
|
|
492
|
+
},
|
|
493
|
+
status: {
|
|
494
|
+
type: 'string',
|
|
495
|
+
description: 'Initial status (default: To Do)',
|
|
496
|
+
},
|
|
497
|
+
assignee_email: {
|
|
498
|
+
type: 'string',
|
|
499
|
+
description: 'Email of person to assign the task to',
|
|
500
|
+
},
|
|
501
|
+
due_date: {
|
|
502
|
+
type: 'string',
|
|
503
|
+
description: 'Due date in ISO format',
|
|
504
|
+
},
|
|
505
|
+
hours_estimated: {
|
|
506
|
+
type: 'number',
|
|
507
|
+
description: 'Estimated hours to complete',
|
|
508
|
+
},
|
|
509
|
+
labels: {
|
|
510
|
+
type: 'array',
|
|
511
|
+
items: { type: 'string' },
|
|
512
|
+
description: 'Labels/tags for the task',
|
|
513
|
+
},
|
|
514
|
+
parent_issue_id: {
|
|
515
|
+
type: 'string',
|
|
516
|
+
description: 'Parent task ID if this is a subtask',
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
required: ['title', 'project_key'],
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
name: 'unroo_update_task',
|
|
524
|
+
description: 'Update an existing task in Unroo. Use to change status, log hours, update priority, or reassign.',
|
|
525
|
+
inputSchema: {
|
|
526
|
+
type: 'object',
|
|
527
|
+
properties: {
|
|
528
|
+
task_id: {
|
|
529
|
+
type: 'string',
|
|
530
|
+
description: 'The ID of the task to update (required)',
|
|
531
|
+
},
|
|
532
|
+
title: {
|
|
533
|
+
type: 'string',
|
|
534
|
+
description: 'Updated title',
|
|
535
|
+
},
|
|
536
|
+
description: {
|
|
537
|
+
type: 'string',
|
|
538
|
+
description: 'Updated description',
|
|
539
|
+
},
|
|
540
|
+
status: {
|
|
541
|
+
type: 'string',
|
|
542
|
+
description: 'New status: To Do, In Progress, Done, Blocked',
|
|
543
|
+
},
|
|
544
|
+
priority: {
|
|
545
|
+
type: 'string',
|
|
546
|
+
enum: ['urgent', 'high', 'medium', 'low'],
|
|
547
|
+
description: 'Updated priority',
|
|
548
|
+
},
|
|
549
|
+
assignee_email: {
|
|
550
|
+
type: 'string',
|
|
551
|
+
description: 'New assignee email (null to unassign)',
|
|
552
|
+
},
|
|
553
|
+
hours_logged: {
|
|
554
|
+
type: 'number',
|
|
555
|
+
description: 'Total hours logged on the task',
|
|
556
|
+
},
|
|
557
|
+
resolution: {
|
|
558
|
+
type: 'string',
|
|
559
|
+
description: 'Resolution when marking as Done',
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
required: ['task_id'],
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
name: 'unroo_get_my_tasks',
|
|
567
|
+
description: 'Get tasks assigned to the current user (based on API key). Useful for finding your work items.',
|
|
568
|
+
inputSchema: {
|
|
569
|
+
type: 'object',
|
|
570
|
+
properties: {
|
|
571
|
+
status: {
|
|
572
|
+
type: 'string',
|
|
573
|
+
description: 'Filter by status (comma-separated for multiple)',
|
|
574
|
+
},
|
|
575
|
+
limit: {
|
|
576
|
+
type: 'number',
|
|
577
|
+
description: 'Maximum number of tasks to return',
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: 'unroo_start_session',
|
|
584
|
+
description: 'Start a work session for tracking time and activity. Call this when beginning work on a task.',
|
|
585
|
+
inputSchema: {
|
|
586
|
+
type: 'object',
|
|
587
|
+
properties: {
|
|
588
|
+
task_id: {
|
|
589
|
+
type: 'string',
|
|
590
|
+
description: 'The task you are working on',
|
|
591
|
+
},
|
|
592
|
+
task_jira_key: {
|
|
593
|
+
type: 'string',
|
|
594
|
+
description: 'JIRA issue key if applicable',
|
|
595
|
+
},
|
|
596
|
+
project_key: {
|
|
597
|
+
type: 'string',
|
|
598
|
+
description: 'Project being worked on',
|
|
599
|
+
},
|
|
600
|
+
repo_name: {
|
|
601
|
+
type: 'string',
|
|
602
|
+
description: 'Repository name if working on code',
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
name: 'unroo_end_session',
|
|
609
|
+
description: 'End the current work session and log time. Call this when finishing work.',
|
|
610
|
+
inputSchema: {
|
|
611
|
+
type: 'object',
|
|
612
|
+
properties: {},
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: 'unroo_create_follow_up',
|
|
617
|
+
description: 'Create a follow-up task linked to a parent task. Use for discovered issues, tech debt, or next steps.',
|
|
618
|
+
inputSchema: {
|
|
619
|
+
type: 'object',
|
|
620
|
+
properties: {
|
|
621
|
+
parent_task_id: {
|
|
622
|
+
type: 'string',
|
|
623
|
+
description: 'The parent task this follows up on (required)',
|
|
624
|
+
},
|
|
625
|
+
title: {
|
|
626
|
+
type: 'string',
|
|
627
|
+
description: 'Follow-up task title (required)',
|
|
628
|
+
},
|
|
629
|
+
description: {
|
|
630
|
+
type: 'string',
|
|
631
|
+
description: 'Description of the follow-up work',
|
|
632
|
+
},
|
|
633
|
+
priority: {
|
|
634
|
+
type: 'string',
|
|
635
|
+
enum: ['urgent', 'high', 'medium', 'low'],
|
|
636
|
+
description: 'Priority (default: same as parent or medium)',
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
required: ['parent_task_id', 'title'],
|
|
640
|
+
},
|
|
641
|
+
},
|
|
229
642
|
];
|
|
230
643
|
// Register tool handlers
|
|
231
644
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -233,6 +646,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
233
646
|
});
|
|
234
647
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
235
648
|
const { name, arguments: args } = request.params;
|
|
649
|
+
// Track tool call for session management
|
|
650
|
+
await sessionTracker.trackToolCall(name, `Called ${name}`);
|
|
236
651
|
try {
|
|
237
652
|
switch (name) {
|
|
238
653
|
case 'fcp_list_launches': {
|
|
@@ -360,6 +775,160 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
360
775
|
],
|
|
361
776
|
};
|
|
362
777
|
}
|
|
778
|
+
// Unroo Task Management Handlers
|
|
779
|
+
case 'unroo_list_projects': {
|
|
780
|
+
const result = await unrooClient.listProjects();
|
|
781
|
+
return {
|
|
782
|
+
content: [
|
|
783
|
+
{
|
|
784
|
+
type: 'text',
|
|
785
|
+
text: JSON.stringify(result, null, 2),
|
|
786
|
+
},
|
|
787
|
+
],
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
case 'unroo_list_tasks': {
|
|
791
|
+
const filters = args;
|
|
792
|
+
const result = await unrooClient.listTasks(filters);
|
|
793
|
+
return {
|
|
794
|
+
content: [
|
|
795
|
+
{
|
|
796
|
+
type: 'text',
|
|
797
|
+
text: JSON.stringify({
|
|
798
|
+
total: result.summary?.total || result.tasks.length,
|
|
799
|
+
by_status: result.summary?.by_status || {},
|
|
800
|
+
tasks: result.tasks,
|
|
801
|
+
}, null, 2),
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
case 'unroo_create_task': {
|
|
807
|
+
const taskInput = args;
|
|
808
|
+
const result = await unrooClient.createTask({
|
|
809
|
+
...taskInput,
|
|
810
|
+
external_source_type: 'claude_code',
|
|
811
|
+
});
|
|
812
|
+
return {
|
|
813
|
+
content: [
|
|
814
|
+
{
|
|
815
|
+
type: 'text',
|
|
816
|
+
text: JSON.stringify({
|
|
817
|
+
success: true,
|
|
818
|
+
message: `Task created: ${result.task.id}`,
|
|
819
|
+
task: result.task,
|
|
820
|
+
}, null, 2),
|
|
821
|
+
},
|
|
822
|
+
],
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
case 'unroo_update_task': {
|
|
826
|
+
const { task_id, ...updates } = args;
|
|
827
|
+
const result = await unrooClient.updateTask(task_id, updates);
|
|
828
|
+
return {
|
|
829
|
+
content: [
|
|
830
|
+
{
|
|
831
|
+
type: 'text',
|
|
832
|
+
text: JSON.stringify({
|
|
833
|
+
success: true,
|
|
834
|
+
message: `Task ${task_id} updated`,
|
|
835
|
+
task: result.task,
|
|
836
|
+
}, null, 2),
|
|
837
|
+
},
|
|
838
|
+
],
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
case 'unroo_get_my_tasks': {
|
|
842
|
+
const { status, limit } = args;
|
|
843
|
+
// Note: The API key determines the user, tasks are filtered server-side
|
|
844
|
+
const result = await unrooClient.listTasks({
|
|
845
|
+
status,
|
|
846
|
+
limit: limit || 50,
|
|
847
|
+
});
|
|
848
|
+
return {
|
|
849
|
+
content: [
|
|
850
|
+
{
|
|
851
|
+
type: 'text',
|
|
852
|
+
text: JSON.stringify({
|
|
853
|
+
total: result.tasks.length,
|
|
854
|
+
tasks: result.tasks,
|
|
855
|
+
}, null, 2),
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
case 'unroo_start_session': {
|
|
861
|
+
const sessionInput = args;
|
|
862
|
+
// Update session tracker with task context
|
|
863
|
+
if (sessionInput.task_id) {
|
|
864
|
+
sessionTracker.setCurrentTask(sessionInput.task_id);
|
|
865
|
+
}
|
|
866
|
+
const result = await unrooClient.startSession({
|
|
867
|
+
...sessionInput,
|
|
868
|
+
source: 'claude-code-mcp',
|
|
869
|
+
});
|
|
870
|
+
return {
|
|
871
|
+
content: [
|
|
872
|
+
{
|
|
873
|
+
type: 'text',
|
|
874
|
+
text: JSON.stringify({
|
|
875
|
+
success: true,
|
|
876
|
+
message: 'Work session started',
|
|
877
|
+
session: result.session,
|
|
878
|
+
auto_tracking: sessionTracker.getStats(),
|
|
879
|
+
}, null, 2),
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
case 'unroo_end_session': {
|
|
885
|
+
// End both explicit and auto-tracked sessions
|
|
886
|
+
await sessionTracker.endSession();
|
|
887
|
+
const result = await unrooClient.endSession();
|
|
888
|
+
return {
|
|
889
|
+
content: [
|
|
890
|
+
{
|
|
891
|
+
type: 'text',
|
|
892
|
+
text: JSON.stringify({
|
|
893
|
+
success: true,
|
|
894
|
+
message: `Session ended. Duration: ${result.session.duration_minutes} minutes`,
|
|
895
|
+
session: result.session,
|
|
896
|
+
}, null, 2),
|
|
897
|
+
},
|
|
898
|
+
],
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
case 'unroo_create_follow_up': {
|
|
902
|
+
const { parent_task_id, title, description, priority } = args;
|
|
903
|
+
// Get parent task to inherit project_key
|
|
904
|
+
const parentResult = await unrooClient.getTask(parent_task_id);
|
|
905
|
+
if (!parentResult.task) {
|
|
906
|
+
throw new Error(`Parent task not found: ${parent_task_id}`);
|
|
907
|
+
}
|
|
908
|
+
const parentTask = parentResult.task;
|
|
909
|
+
const result = await unrooClient.createTask({
|
|
910
|
+
title: `[Follow-up] ${title}`,
|
|
911
|
+
description: description || `Follow-up from task: ${parentTask.title}`,
|
|
912
|
+
project_key: parentTask.project_key || 'FCP',
|
|
913
|
+
priority: priority || parentTask.priority || 'medium',
|
|
914
|
+
parent_issue_id: parent_task_id,
|
|
915
|
+
external_source_type: 'claude_code_followup',
|
|
916
|
+
labels: ['follow-up'],
|
|
917
|
+
});
|
|
918
|
+
return {
|
|
919
|
+
content: [
|
|
920
|
+
{
|
|
921
|
+
type: 'text',
|
|
922
|
+
text: JSON.stringify({
|
|
923
|
+
success: true,
|
|
924
|
+
message: `Follow-up task created: ${result.task.id}`,
|
|
925
|
+
parent_task_id,
|
|
926
|
+
task: result.task,
|
|
927
|
+
}, null, 2),
|
|
928
|
+
},
|
|
929
|
+
],
|
|
930
|
+
};
|
|
931
|
+
}
|
|
363
932
|
default:
|
|
364
933
|
throw new Error(`Unknown tool: ${name}`);
|
|
365
934
|
}
|
|
@@ -426,6 +995,14 @@ async function main() {
|
|
|
426
995
|
const transport = new StdioServerTransport();
|
|
427
996
|
await server.connect(transport);
|
|
428
997
|
console.error('FCP MCP Server running on stdio');
|
|
998
|
+
// Handle graceful shutdown
|
|
999
|
+
const shutdown = async () => {
|
|
1000
|
+
console.error('Shutting down MCP server...');
|
|
1001
|
+
await sessionTracker.endSession();
|
|
1002
|
+
process.exit(0);
|
|
1003
|
+
};
|
|
1004
|
+
process.on('SIGINT', shutdown);
|
|
1005
|
+
process.on('SIGTERM', shutdown);
|
|
429
1006
|
}
|
|
430
1007
|
main().catch((error) => {
|
|
431
1008
|
console.error('Fatal error:', error);
|
package/package.json
CHANGED