@fruition/fcp-mcp-server 1.2.0 → 1.4.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 (2) hide show
  1. package/dist/index.js +208 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,11 +10,111 @@
10
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
12
  import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
13
+ import { readFileSync } from 'fs';
14
+ import { fileURLToPath } from 'url';
15
+ import { dirname, join } from 'path';
16
+ // Get version from package.json
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+ const packageJsonPath = join(__dirname, '..', 'package.json');
20
+ let MCP_SERVER_VERSION = '1.0.0'; // fallback
21
+ try {
22
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
23
+ MCP_SERVER_VERSION = packageJson.version;
24
+ }
25
+ catch {
26
+ // Try one level up (for dist folder)
27
+ try {
28
+ const altPath = join(__dirname, '..', '..', 'package.json');
29
+ const packageJson = JSON.parse(readFileSync(altPath, 'utf-8'));
30
+ MCP_SERVER_VERSION = packageJson.version;
31
+ }
32
+ catch {
33
+ console.error('[MCP Server] Warning: Could not read package.json version');
34
+ }
35
+ }
36
+ import { execSync } from 'child_process';
13
37
  // Configuration
14
38
  const FCP_API_URL = process.env.FCP_API_URL || 'https://fcp.fru.io';
15
39
  const FCP_API_TOKEN = process.env.FCP_API_TOKEN || '';
16
40
  const UNROO_API_URL = process.env.UNROO_API_URL || 'https://chat.frugpt.com';
17
41
  const UNROO_API_KEY = process.env.UNROO_API_KEY || '';
42
+ let currentProject = null;
43
+ /**
44
+ * Detect the current git remote URL
45
+ */
46
+ function detectGitRemote() {
47
+ try {
48
+ const remote = execSync('git config --get remote.origin.url', {
49
+ encoding: 'utf-8',
50
+ timeout: 5000,
51
+ stdio: ['pipe', 'pipe', 'ignore'],
52
+ }).trim();
53
+ return remote || null;
54
+ }
55
+ catch {
56
+ // Not a git repo or no remote configured
57
+ return null;
58
+ }
59
+ }
60
+ /**
61
+ * Resolve project from git remote URL via FCP API
62
+ */
63
+ async function resolveProjectFromRepo(repoUrl) {
64
+ if (!FCP_API_URL) {
65
+ return null;
66
+ }
67
+ try {
68
+ const url = `${FCP_API_URL}/api/mcp/resolve-project?repo_url=${encodeURIComponent(repoUrl)}`;
69
+ const headers = {
70
+ 'Content-Type': 'application/json',
71
+ };
72
+ if (FCP_API_TOKEN && FCP_API_TOKEN !== 'dev_bypass') {
73
+ headers['X-API-Key'] = FCP_API_TOKEN;
74
+ }
75
+ else if (FCP_API_TOKEN === 'dev_bypass') {
76
+ headers['X-Dev-Bypass'] = 'true';
77
+ }
78
+ const response = await fetch(url, { headers });
79
+ if (!response.ok) {
80
+ return null;
81
+ }
82
+ const data = await response.json();
83
+ if (data.found && data.project) {
84
+ return {
85
+ project_key: data.project.project_key,
86
+ website_id: data.project.website_id,
87
+ domain: data.project.domain,
88
+ account_name: data.project.account_name || 'Unknown',
89
+ github: data.project.github,
90
+ };
91
+ }
92
+ return null;
93
+ }
94
+ catch (error) {
95
+ console.error('[MCP Server] Error resolving project:', error);
96
+ return null;
97
+ }
98
+ }
99
+ /**
100
+ * Initialize project detection on startup
101
+ */
102
+ async function initializeProjectDetection() {
103
+ const remoteUrl = detectGitRemote();
104
+ if (!remoteUrl) {
105
+ console.error('[MCP Server] No git remote detected - project will be tracked as general');
106
+ return;
107
+ }
108
+ console.error(`[MCP Server] Detected git remote: ${remoteUrl}`);
109
+ const project = await resolveProjectFromRepo(remoteUrl);
110
+ if (project) {
111
+ currentProject = project;
112
+ console.error(`[MCP Server] Resolved project: ${project.domain} (${project.project_key})`);
113
+ }
114
+ else {
115
+ console.error('[MCP Server] Could not resolve project from repo - check FCP website github config');
116
+ }
117
+ }
18
118
  // API Client
19
119
  class FCPClient {
20
120
  baseUrl;
@@ -157,6 +257,19 @@ class UnrooClient {
157
257
  body: JSON.stringify({ action: 'heartbeat', ...input }),
158
258
  });
159
259
  }
260
+ async logActivity(taskId, input) {
261
+ return this.fetch(`/api/external/fcp/tasks/${encodeURIComponent(taskId)}/activity`, {
262
+ method: 'POST',
263
+ body: JSON.stringify({
264
+ activity_type: input.activity_type,
265
+ source: input.source || 'claude_code',
266
+ field_changed: input.field_changed,
267
+ old_value: input.old_value,
268
+ new_value: input.new_value,
269
+ metadata: input.metadata,
270
+ }),
271
+ });
272
+ }
160
273
  }
161
274
  // ============================================================================
162
275
  // Auto Session Tracking
@@ -211,18 +324,30 @@ class SessionTracker {
211
324
  return;
212
325
  }
213
326
  try {
214
- await this.unrooClient.startSession({
327
+ // Use auto-detected project if available
328
+ const sessionInput = {
215
329
  task_id: this.currentTaskId || undefined,
216
330
  source: 'claude-code-mcp',
217
331
  machine_id: process.env.HOSTNAME || 'unknown',
218
- });
332
+ };
333
+ // Add auto-detected project info
334
+ if (currentProject) {
335
+ sessionInput.project_key = currentProject.project_key;
336
+ sessionInput.repo_name = `${currentProject.github.owner}/${currentProject.github.repo}`;
337
+ }
338
+ await this.unrooClient.startSession(sessionInput);
219
339
  this.sessionActive = true;
220
340
  this.lastHeartbeat = new Date();
221
341
  // Set up periodic heartbeat (every 5 minutes)
222
342
  this.heartbeatInterval = setInterval(() => {
223
343
  this.sendHeartbeat().catch(() => { });
224
344
  }, 5 * 60 * 1000);
225
- console.error('[SessionTracker] Session started');
345
+ if (currentProject) {
346
+ console.error(`[SessionTracker] Session started for project: ${currentProject.domain}`);
347
+ }
348
+ else {
349
+ console.error('[SessionTracker] Session started (no project detected)');
350
+ }
226
351
  }
227
352
  catch (error) {
228
353
  console.error('[SessionTracker] Failed to start session:', error);
@@ -249,7 +374,7 @@ class SessionTracker {
249
374
  }
250
375
  }
251
376
  /**
252
- * End the current session
377
+ * End the current session and log activity summary to task
253
378
  */
254
379
  async endSession() {
255
380
  if (!this.sessionActive || !UNROO_API_KEY) {
@@ -260,9 +385,34 @@ class SessionTracker {
260
385
  this.heartbeatInterval = null;
261
386
  }
262
387
  try {
263
- await this.unrooClient.endSession();
388
+ // End the session first
389
+ const result = await this.unrooClient.endSession();
264
390
  this.sessionActive = false;
265
391
  console.error('[SessionTracker] Session ended');
392
+ // Log activity summary to the task if we have a current task
393
+ if (this.currentTaskId && result.session) {
394
+ try {
395
+ const session = result.session;
396
+ const uniqueTools = [...new Set(this.activities.map(a => a.tool))];
397
+ await this.unrooClient.logActivity(this.currentTaskId, {
398
+ activity_type: 'claude_code_session',
399
+ source: 'claude_code',
400
+ metadata: {
401
+ session_id: session.id,
402
+ duration_seconds: session.duration_minutes ? session.duration_minutes * 60 : 0,
403
+ total_tool_calls: session.total_tool_calls || this.toolCallCount,
404
+ tools_used: uniqueTools.slice(0, 20), // Limit to 20 tools
405
+ started_at: session.started_at,
406
+ ended_at: session.ended_at || new Date().toISOString(),
407
+ },
408
+ });
409
+ console.error(`[SessionTracker] Logged session activity to task ${this.currentTaskId}`);
410
+ }
411
+ catch (activityError) {
412
+ console.error('[SessionTracker] Failed to log activity to task:', activityError);
413
+ // Don't throw - session end was successful
414
+ }
415
+ }
266
416
  }
267
417
  catch (error) {
268
418
  console.error('[SessionTracker] Failed to end session:', error);
@@ -283,7 +433,7 @@ class SessionTracker {
283
433
  // Create server
284
434
  const server = new Server({
285
435
  name: 'fcp-mcp-server',
286
- version: '1.0.0',
436
+ version: MCP_SERVER_VERSION,
287
437
  }, {
288
438
  capabilities: {
289
439
  tools: {},
@@ -990,11 +1140,62 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
990
1140
  }
991
1141
  throw new Error(`Unknown resource: ${uri}`);
992
1142
  });
1143
+ // Version comparison helper
1144
+ function compareVersions(v1, v2) {
1145
+ const parts1 = v1.split('.').map(Number);
1146
+ const parts2 = v2.split('.').map(Number);
1147
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
1148
+ const p1 = parts1[i] || 0;
1149
+ const p2 = parts2[i] || 0;
1150
+ if (p1 > p2)
1151
+ return 1;
1152
+ if (p1 < p2)
1153
+ return -1;
1154
+ }
1155
+ return 0;
1156
+ }
1157
+ // Check for updates on startup
1158
+ async function checkForUpdates() {
1159
+ if (!FCP_API_URL)
1160
+ return;
1161
+ try {
1162
+ const response = await fetch(`${FCP_API_URL}/api/mcp/version`, {
1163
+ headers: FCP_API_TOKEN ? { 'X-API-Key': FCP_API_TOKEN } : {},
1164
+ });
1165
+ if (!response.ok) {
1166
+ // Silently ignore - version endpoint may not exist on older FCP versions
1167
+ return;
1168
+ }
1169
+ const data = await response.json();
1170
+ const latestVersion = data.version;
1171
+ const minVersion = data.minVersion;
1172
+ if (compareVersions(MCP_SERVER_VERSION, minVersion) < 0) {
1173
+ console.error(`\n⚠️ [MCP Server] INCOMPATIBLE VERSION`);
1174
+ console.error(` Your version: ${MCP_SERVER_VERSION}`);
1175
+ console.error(` Minimum required: ${minVersion}`);
1176
+ console.error(` Please update: ${data.updateUrl}\n`);
1177
+ }
1178
+ else if (compareVersions(MCP_SERVER_VERSION, latestVersion) < 0) {
1179
+ console.error(`\n📦 [MCP Server] Update available: ${MCP_SERVER_VERSION} → ${latestVersion}`);
1180
+ console.error(` Update: ${data.updateUrl}\n`);
1181
+ }
1182
+ else {
1183
+ console.error(`[MCP Server] Version ${MCP_SERVER_VERSION} (up to date)`);
1184
+ }
1185
+ }
1186
+ catch {
1187
+ // Silently ignore network errors - don't block startup
1188
+ }
1189
+ }
993
1190
  // Start server
994
1191
  async function main() {
995
1192
  const transport = new StdioServerTransport();
996
1193
  await server.connect(transport);
997
- console.error('FCP MCP Server running on stdio');
1194
+ console.error(`FCP MCP Server v${MCP_SERVER_VERSION} running on stdio`);
1195
+ // Auto-detect project from git remote (non-blocking)
1196
+ initializeProjectDetection();
1197
+ // Check for updates (non-blocking)
1198
+ checkForUpdates();
998
1199
  // Handle graceful shutdown
999
1200
  const shutdown = async () => {
1000
1201
  console.error('Shutting down MCP server...');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fruition/fcp-mcp-server",
3
- "version": "1.2.0",
3
+ "version": "1.4.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",