@fruition/fcp-mcp-server 1.0.1 → 1.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # FCP MCP Server
2
2
 
3
- MCP (Model Context Protocol) server that gives Claude Code direct access to the FCP Launch Coordination System.
3
+ MCP (Model Context Protocol) server that gives Claude Code direct access to the FCP Launch Coordination System. Also includes automatic time tracking integration with Unroo.
4
4
 
5
5
  ## Features
6
6
 
@@ -149,3 +149,76 @@ To get an API token for the MCP server:
149
149
  ### Tools not appearing
150
150
  - Restart Claude Code after updating settings
151
151
  - Check Claude Code logs for MCP server errors
152
+
153
+ ---
154
+
155
+ ## Time Tracking with Unroo
156
+
157
+ This package also includes `unroo-heartbeat`, a script that automatically tracks your Claude Code development time in Unroo.
158
+
159
+ ### Quick Setup
160
+
161
+ After installing the package globally (`npm install -g @fruition/fcp-mcp-server`), configure time tracking:
162
+
163
+ #### 1. Set your Unroo API key
164
+
165
+ Add to `~/.bashrc` or `~/.zshrc`:
166
+
167
+ ```bash
168
+ export UNROO_API_KEY="mcp_XXXXXX_your_key_here"
169
+ ```
170
+
171
+ Get your API key from Unroo: Settings → API Keys → Create with "Time Tracking" enabled.
172
+
173
+ #### 2. Configure Claude Code hooks
174
+
175
+ Add to `~/.claude/settings.json`:
176
+
177
+ ```json
178
+ {
179
+ "hooks": {
180
+ "PostToolUse": [
181
+ {
182
+ "matcher": "*",
183
+ "hooks": [
184
+ {
185
+ "type": "command",
186
+ "command": "unroo-heartbeat"
187
+ }
188
+ ]
189
+ }
190
+ ],
191
+ "Stop": [
192
+ {
193
+ "hooks": [
194
+ {
195
+ "type": "command",
196
+ "command": "unroo-heartbeat session_end"
197
+ }
198
+ ]
199
+ }
200
+ ]
201
+ }
202
+ }
203
+ ```
204
+
205
+ #### 3. Restart Claude Code
206
+
207
+ Time tracking is now active. Sessions are automatically mapped to the correct Unroo project based on:
208
+ 1. `.unroo` file in project root (if present)
209
+ 2. Saved repo→project mappings (learned from previous sessions)
210
+ 3. Smart matching by repo name to Jira project key
211
+
212
+ ### Project Configuration (Optional)
213
+
214
+ For repos that don't auto-match, create a `.unroo` file:
215
+
216
+ ```bash
217
+ echo "project_slug=YOUR_JIRA_KEY" > .unroo
218
+ ```
219
+
220
+ The system learns from this and saves the mapping permanently, so you only need to do this once per repo.
221
+
222
+ ### Viewing Time
223
+
224
+ Log in to [Unroo](https://chat.frugpt.com) and check My Day or Timesheet to see your tracked sessions.
@@ -0,0 +1,90 @@
1
+ #!/bin/bash
2
+ # ============================================================================
3
+ # UNROO CLAUDE CODE HEARTBEAT SCRIPT
4
+ # ============================================================================
5
+ # Sends heartbeat signals to UnRoo time tracker API
6
+ # Called by Claude Code hooks on tool use events
7
+ #
8
+ # Usage:
9
+ # unroo-heartbeat.sh [event_type]
10
+ #
11
+ # Event types:
12
+ # tool_end (default) - After tool completes
13
+ # session_end - When Claude Code session ends
14
+ # heartbeat - Periodic heartbeat from watcher
15
+ #
16
+ # Environment variables:
17
+ # UNROO_API_KEY - Required: Your UnRoo API key with time tracking enabled
18
+ # UNROO_API_ENDPOINT - Optional: API base URL (default: https://chat.frugpt.com)
19
+ # TOOL_NAME - Optional: Name of the tool that was used
20
+ # TOOL_DESCRIPTION - Optional: Description of what the tool did
21
+ #
22
+ # Project Configuration:
23
+ # Create a .unroo file in your project root with:
24
+ # project_slug=YOUR_PROJECT_SLUG
25
+ # This maps time to the correct Unroo project (optional - auto-matching works for most repos).
26
+ #
27
+ # Installation:
28
+ # npm install -g @fruition/fcp-mcp-server
29
+ # Then set UNROO_API_KEY in your shell profile and configure Claude Code hooks.
30
+ # ============================================================================
31
+
32
+ # Configuration
33
+ EVENT_TYPE="${1:-tool_end}"
34
+ API_ENDPOINT="${UNROO_API_ENDPOINT:-https://chat.frugpt.com}"
35
+ API_URL="${API_ENDPOINT}/api/unroo/claude-tracker/heartbeat"
36
+
37
+ # Check for API key
38
+ if [ -z "$UNROO_API_KEY" ]; then
39
+ # Silently exit if no API key - don't block Claude Code
40
+ exit 0
41
+ fi
42
+
43
+ # Get git remote URL (if in a git repo)
44
+ GIT_REMOTE=""
45
+ if git rev-parse --git-dir > /dev/null 2>&1; then
46
+ GIT_REMOTE=$(git remote get-url origin 2>/dev/null || echo "")
47
+ fi
48
+
49
+ # Get repo name from current directory
50
+ REPO_NAME=$(basename "$(pwd)")
51
+
52
+ # Check for project-specific config (.unroo file)
53
+ PROJECT_SLUG=""
54
+ if [ -f ".unroo" ]; then
55
+ PROJECT_SLUG=$(grep -E "^project_slug=" .unroo 2>/dev/null | cut -d= -f2)
56
+ fi
57
+
58
+ # Get machine ID (for multi-machine tracking)
59
+ MACHINE_ID=""
60
+ if [ -f /etc/machine-id ]; then
61
+ MACHINE_ID=$(cat /etc/machine-id | head -c 8)
62
+ elif command -v hostid &> /dev/null; then
63
+ MACHINE_ID=$(hostid)
64
+ fi
65
+
66
+ # Build JSON payload
67
+ JSON_PAYLOAD=$(cat <<EOF
68
+ {
69
+ "api_key": "${UNROO_API_KEY}",
70
+ "event_type": "${EVENT_TYPE}",
71
+ "source": "claude-code",
72
+ "git_remote_url": "${GIT_REMOTE}",
73
+ "repo_name": "${REPO_NAME}",
74
+ "machine_id": "${MACHINE_ID}",
75
+ "tool_name": "${TOOL_NAME:-}",
76
+ "tool_description": "${TOOL_DESCRIPTION:-}",
77
+ "project_slug": "${PROJECT_SLUG:-}"
78
+ }
79
+ EOF
80
+ )
81
+
82
+ # Send heartbeat in background to not block Claude Code
83
+ # Redirect all output to /dev/null to avoid any terminal output
84
+ curl -s -X POST "$API_URL" \
85
+ -H "Content-Type: application/json" \
86
+ -d "$JSON_PAYLOAD" \
87
+ > /dev/null 2>&1 &
88
+
89
+ # Exit immediately - don't wait for curl
90
+ exit 0
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://chat.frugpt.com';
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
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "@fruition/fcp-mcp-server",
3
- "version": "1.0.1",
4
- "description": "MCP Server for FCP Launch Coordination System - enables Claude Code to interact with FCP launches",
3
+ "version": "1.2.0",
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",
7
7
  "bin": {
8
- "fcp-mcp-server": "./dist/index.js"
8
+ "fcp-mcp-server": "./dist/index.js",
9
+ "unroo-heartbeat": "./bin/unroo-heartbeat.sh"
9
10
  },
10
11
  "files": [
11
- "dist"
12
+ "dist",
13
+ "bin"
12
14
  ],
13
15
  "scripts": {
14
16
  "build": "tsc",