@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.
- package/dist/index.js +208 -7
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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.
|
|
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",
|