fraim-framework 2.0.55 → 2.0.57

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 (120) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/src/cli/commands/init-project.js +10 -4
  3. package/dist/src/cli/setup/mcp-config-generator.js +23 -15
  4. package/dist/src/local-mcp-server/stdio-server.js +207 -0
  5. package/dist/src/utils/validate-workflows.js +101 -0
  6. package/dist/src/utils/workflow-parser.js +81 -0
  7. package/package.json +16 -11
  8. package/registry/scripts/pdf-styles.css +172 -0
  9. package/registry/scripts/prep-issue.sh +46 -4
  10. package/registry/scripts/profile-server.ts +131 -130
  11. package/registry/stubs/workflows/customer-development/user-survey-dispatch.md +1 -1
  12. package/registry/stubs/workflows/customer-development/users-to-target.md +1 -1
  13. package/registry/stubs/workflows/product-building/design.md +1 -1
  14. package/registry/stubs/workflows/product-building/implement.md +1 -1
  15. package/Claude.md +0 -1
  16. package/dist/registry/ai-manager-rules/design-phases/design-completeness-review.md +0 -73
  17. package/dist/registry/ai-manager-rules/design-phases/design-design.md +0 -145
  18. package/dist/registry/ai-manager-rules/implement-phases/implement-code.md +0 -283
  19. package/dist/registry/ai-manager-rules/implement-phases/implement-completeness-review.md +0 -120
  20. package/dist/registry/ai-manager-rules/implement-phases/implement-regression.md +0 -173
  21. package/dist/registry/ai-manager-rules/implement-phases/implement-repro.md +0 -104
  22. package/dist/registry/ai-manager-rules/implement-phases/implement-scoping.md +0 -100
  23. package/dist/registry/ai-manager-rules/implement-phases/implement-smoke.md +0 -237
  24. package/dist/registry/ai-manager-rules/implement-phases/implement-spike.md +0 -121
  25. package/dist/registry/ai-manager-rules/implement-phases/implement-validate.md +0 -375
  26. package/dist/registry/ai-manager-rules/retrospective.md +0 -116
  27. package/dist/registry/ai-manager-rules/shared-phases/address-pr-feedback.md +0 -188
  28. package/dist/registry/ai-manager-rules/shared-phases/submit-pr.md +0 -202
  29. package/dist/registry/ai-manager-rules/shared-phases/wait-for-pr-review.md +0 -170
  30. package/dist/registry/ai-manager-rules/spec-phases/spec-competitor-analysis.md +0 -105
  31. package/dist/registry/ai-manager-rules/spec-phases/spec-completeness-review.md +0 -66
  32. package/dist/registry/ai-manager-rules/spec-phases/spec-spec.md +0 -139
  33. package/dist/registry/providers/ado.json +0 -19
  34. package/dist/registry/providers/github.json +0 -19
  35. package/dist/registry/scripts/cleanup-branch.js +0 -287
  36. package/dist/registry/scripts/evaluate-code-quality.js +0 -66
  37. package/dist/registry/scripts/exec-with-timeout.js +0 -142
  38. package/dist/registry/scripts/generate-engagement-emails.js +0 -705
  39. package/dist/registry/scripts/newsletter-helpers.js +0 -671
  40. package/dist/registry/scripts/profile-server.js +0 -388
  41. package/dist/registry/scripts/run-thank-you-workflow.js +0 -92
  42. package/dist/registry/scripts/send-newsletter-simple.js +0 -85
  43. package/dist/registry/scripts/send-thank-you-emails.js +0 -54
  44. package/dist/registry/scripts/validate-openapi-limits.js +0 -311
  45. package/dist/registry/scripts/validate-test-coverage.js +0 -262
  46. package/dist/registry/scripts/verify-test-coverage.js +0 -66
  47. package/dist/scripts/build-stub-registry.js +0 -108
  48. package/dist/src/ai-manager/ai-manager.js +0 -482
  49. package/dist/src/ai-manager/phase-flow.js +0 -357
  50. package/dist/src/ai-manager/types.js +0 -5
  51. package/dist/src/fraim-mcp-server.js +0 -1885
  52. package/dist/tests/debug-tools.js +0 -80
  53. package/dist/tests/shared-server-utils.js +0 -57
  54. package/dist/tests/test-add-ide.js +0 -283
  55. package/dist/tests/test-ai-coach-edge-cases.js +0 -420
  56. package/dist/tests/test-ai-coach-mcp-integration.js +0 -450
  57. package/dist/tests/test-ai-coach-performance.js +0 -328
  58. package/dist/tests/test-ai-coach-phase-content.js +0 -264
  59. package/dist/tests/test-ai-coach-workflows.js +0 -514
  60. package/dist/tests/test-cli.js +0 -228
  61. package/dist/tests/test-client-scripts-validation.js +0 -167
  62. package/dist/tests/test-complete-setup-flow.js +0 -110
  63. package/dist/tests/test-config-system.js +0 -279
  64. package/dist/tests/test-debug-session.js +0 -134
  65. package/dist/tests/test-end-to-end-hybrid-validation.js +0 -328
  66. package/dist/tests/test-enhanced-session-init.js +0 -188
  67. package/dist/tests/test-first-run-journey.js +0 -368
  68. package/dist/tests/test-fraim-issues.js +0 -59
  69. package/dist/tests/test-genericization.js +0 -44
  70. package/dist/tests/test-hybrid-script-execution.js +0 -340
  71. package/dist/tests/test-ide-detector.js +0 -46
  72. package/dist/tests/test-improved-setup.js +0 -121
  73. package/dist/tests/test-mcp-config-generator.js +0 -99
  74. package/dist/tests/test-mcp-connection.js +0 -107
  75. package/dist/tests/test-mcp-issue-integration.js +0 -156
  76. package/dist/tests/test-mcp-lifecycle-methods.js +0 -240
  77. package/dist/tests/test-mcp-shared-server.js +0 -308
  78. package/dist/tests/test-mcp-template-processing.js +0 -160
  79. package/dist/tests/test-modular-issue-tracking.js +0 -165
  80. package/dist/tests/test-node-compatibility.js +0 -95
  81. package/dist/tests/test-npm-install.js +0 -68
  82. package/dist/tests/test-package-size.js +0 -108
  83. package/dist/tests/test-pr-review-workflow.js +0 -307
  84. package/dist/tests/test-prep-issue.js +0 -129
  85. package/dist/tests/test-productivity-integration.js +0 -157
  86. package/dist/tests/test-script-location-independence.js +0 -198
  87. package/dist/tests/test-script-sync.js +0 -557
  88. package/dist/tests/test-server-utils.js +0 -32
  89. package/dist/tests/test-session-rehydration.js +0 -148
  90. package/dist/tests/test-setup-integration.js +0 -98
  91. package/dist/tests/test-setup-scenarios.js +0 -322
  92. package/dist/tests/test-standalone.js +0 -143
  93. package/dist/tests/test-stub-registry.js +0 -136
  94. package/dist/tests/test-sync-stubs.js +0 -143
  95. package/dist/tests/test-sync-version-update.js +0 -93
  96. package/dist/tests/test-telemetry.js +0 -193
  97. package/dist/tests/test-token-validator.js +0 -30
  98. package/dist/tests/test-user-journey.js +0 -236
  99. package/dist/tests/test-users-to-target-workflow.js +0 -253
  100. package/dist/tests/test-utils.js +0 -109
  101. package/dist/tests/test-wizard.js +0 -71
  102. package/dist/tests/test-workflow-discovery.js +0 -242
  103. package/labels.json +0 -52
  104. package/registry/agent-guardrails.md +0 -63
  105. package/registry/fraim.md +0 -48
  106. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase1-customer-profiling.md +0 -11
  107. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase1-survey-scoping.md +0 -11
  108. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase2-platform-discovery.md +0 -11
  109. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase2-survey-build-linkedin.md +0 -11
  110. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase3-prospect-qualification.md +0 -11
  111. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase3-survey-build-reddit.md +0 -11
  112. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase4-inventory-compilation.md +0 -11
  113. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase4-survey-build-x.md +0 -11
  114. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase5-survey-build-facebook.md +0 -11
  115. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase6-survey-build-custom.md +0 -11
  116. package/registry/stubs/workflows/customer-development/ai-coach-phases/phase7-survey-dispatch.md +0 -11
  117. package/registry/stubs/workflows/customer-development/templates/customer-persona-template.md +0 -11
  118. package/registry/stubs/workflows/customer-development/templates/search-strategy-template.md +0 -11
  119. package/setup.js +0 -171
  120. package/tsconfig.json +0 -23
@@ -1,1885 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || (function () {
20
- var ownKeys = function(o) {
21
- ownKeys = Object.getOwnPropertyNames || function (o) {
22
- var ar = [];
23
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
- return ar;
25
- };
26
- return ownKeys(o);
27
- };
28
- return function (mod) {
29
- if (mod && mod.__esModule) return mod;
30
- var result = {};
31
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
- __setModuleDefault(result, mod);
33
- return result;
34
- };
35
- })();
36
- var __importDefault = (this && this.__importDefault) || function (mod) {
37
- return (mod && mod.__esModule) ? mod : { "default": mod };
38
- };
39
- Object.defineProperty(exports, "__esModule", { value: true });
40
- exports.FraimMCPServer = void 0;
41
- const express_1 = __importDefault(require("express"));
42
- const cors_1 = __importDefault(require("cors"));
43
- const fs_1 = require("fs");
44
- const path_1 = require("path");
45
- const git_utils_1 = require("./utils/git-utils");
46
- const db_service_1 = require("./fraim/db-service");
47
- const ai_manager_1 = require("./ai-manager/ai-manager");
48
- const issues_1 = require("./fraim/issues");
49
- const template_processor_1 = require("./fraim/template-processor");
50
- const crypto_1 = require("crypto");
51
- const dotenv = __importStar(require("dotenv"));
52
- // Load environment variables
53
- dotenv.config();
54
- // Global error handlers for better debugging in production
55
- process.on('uncaughtException', (err) => {
56
- console.error('❌ CRITICAL: Uncaught Exception:', err.message);
57
- console.error(err.stack);
58
- process.exit(1);
59
- });
60
- process.on('unhandledRejection', (reason, promise) => {
61
- console.error('❌ CRITICAL: Unhandled Rejection at:', promise, 'reason:', reason);
62
- process.exit(1);
63
- });
64
- console.log('🚀 FRAIM MCP Server starting up...');
65
- console.log(`📂 Working Directory: ${process.cwd()}`);
66
- console.log(`🏠 Dirname: ${__dirname}`);
67
- try {
68
- console.log('files in current dir:', (0, fs_1.readdirSync)(process.cwd()).join(', '));
69
- const distPath = (0, path_1.join)(process.cwd(), 'dist');
70
- if ((0, fs_1.existsSync)(distPath)) {
71
- console.log('files in dist:', (0, fs_1.readdirSync)(distPath).join(', '));
72
- }
73
- }
74
- catch (e) {
75
- console.warn('⚠️ Could not log directory structure:', e.message);
76
- }
77
- class SessionManager {
78
- constructor(dbService) {
79
- this.sessions = new Map();
80
- this.dbService = dbService;
81
- // Default 1m, configurable for testing
82
- this.FLUSH_INTERVAL_MS = process.env.FRAIM_TELEMETRY_FLUSH_INTERVAL
83
- ? parseInt(process.env.FRAIM_TELEMETRY_FLUSH_INTERVAL)
84
- : 60 * 1000;
85
- // Flush on shutdown
86
- const cleanup = async () => {
87
- console.log('🛑 Flushing telemetry sessions before shutdown...');
88
- // Race flush with a 2s timeout to guarantee exit
89
- const timeout = new Promise(resolve => setTimeout(resolve, 2000));
90
- await Promise.race([this.flushAll(), timeout]);
91
- console.log('🛑 Shutdown flush complete (or timed out). Exiting.');
92
- process.exit(0);
93
- };
94
- process.on('SIGTERM', cleanup);
95
- process.on('SIGINT', cleanup);
96
- }
97
- registerSession(apiKey, sessionId) {
98
- const now = Date.now();
99
- this.sessions.set(apiKey, { sessionId, lastWrite: now, lastActive: now });
100
- }
101
- async updateActivity(apiKey) {
102
- let session = this.sessions.get(apiKey);
103
- const now = Date.now();
104
- if (!session) {
105
- // Re-hydration: Check DB for an active session for this API Key
106
- try {
107
- const dbSession = await this.dbService.getActiveSessionByApiKey(apiKey);
108
- if (dbSession) {
109
- console.log(`🔄 Re-hydrating session for API Key: ${apiKey}`);
110
- session = {
111
- sessionId: dbSession.sessionId,
112
- lastWrite: now,
113
- lastActive: now
114
- };
115
- this.sessions.set(apiKey, session);
116
- }
117
- else {
118
- return false;
119
- }
120
- }
121
- catch (e) {
122
- console.error('⚠️ Failed to re-hydrate session from DB:', e);
123
- return false;
124
- }
125
- }
126
- session.lastActive = now;
127
- // Write-behind: Only flush to DB if > Interval elapsed
128
- if (now - session.lastWrite > this.FLUSH_INTERVAL_MS) {
129
- // Async write
130
- this.dbService.updateSessionActivity(session.sessionId, new Date(now)).catch(e => console.error('⚠️ Failed to flush session activity:', e));
131
- session.lastWrite = now;
132
- }
133
- return true;
134
- }
135
- async flushAll() {
136
- const promises = [];
137
- for (const [_, data] of this.sessions) {
138
- // Flush the memory lastActive time
139
- promises.push(this.dbService.updateSessionActivity(data.sessionId, new Date(data.lastActive)));
140
- }
141
- await Promise.allSettled(promises);
142
- }
143
- }
144
- class FraimMCPServer {
145
- constructor() {
146
- this.fileIndex = new Map();
147
- this.workflowKeywords = new Map();
148
- this.app = (0, express_1.default)();
149
- this.app.use((0, cors_1.default)());
150
- this.app.use(express_1.default.json());
151
- // Load server version
152
- try {
153
- // Try process.cwd() first as it's the root in most deployments
154
- let pkgPath = (0, path_1.join)(process.cwd(), 'package.json');
155
- if (!(0, fs_1.existsSync)(pkgPath)) {
156
- // Fallback to relative to __dirname (dist/src/ -> ../../package.json)
157
- pkgPath = (0, path_1.join)(__dirname, '..', '..', 'package.json');
158
- }
159
- if ((0, fs_1.existsSync)(pkgPath)) {
160
- const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf8'));
161
- this.serverVersion = pkg.version;
162
- }
163
- else {
164
- this.serverVersion = 'unknown';
165
- }
166
- }
167
- catch (e) {
168
- this.serverVersion = 'unknown';
169
- }
170
- // Initialize database service
171
- this.dbService = new db_service_1.FraimDbService();
172
- this.sessionManager = new SessionManager(this.dbService);
173
- // Find registry directory (check dist first for production, then source)
174
- this.registryPath = this.findRegistryPath();
175
- // Build file index from filesystem (includes registry and .fraim)
176
- this.buildFileIndex();
177
- this.buildWorkflowKeywords();
178
- // Initialize AI Coach with file index
179
- this.aiCoach = new ai_manager_1.AICoach(this.fileIndex);
180
- // Apply core middleware first (logging and auth)
181
- this.app.use(this.requestLogger.bind(this));
182
- this.app.use(this.versionCheck.bind(this));
183
- // Public health check (before auth)
184
- this.app.get('/health', (req, res) => {
185
- res.status(200).json({
186
- status: 'ok',
187
- version: this.serverVersion,
188
- timestamp: new Date().toISOString(),
189
- service: 'fraim-mcp-standalone'
190
- });
191
- });
192
- this.app.use(this.authMiddleware.bind(this));
193
- this.app.use(this.telemetryMiddleware.bind(this));
194
- this.setupRoutes();
195
- }
196
- /**
197
- * Middleware to log all requests
198
- */
199
- requestLogger(req, res, next) {
200
- const start = Date.now();
201
- const { method, path } = req;
202
- const params = method === 'POST' ? JSON.stringify(req.body) : '';
203
- res.on('finish', () => {
204
- const duration = Date.now() - start;
205
- const status = res.statusCode;
206
- const apiKey = req.apiKeyData;
207
- const keyStr = apiKey ? apiKey.key : 'ANONYMOUS';
208
- if (params && params.length > 0) {
209
- console.info(`${keyStr} ${method} ${path} ${status} ${duration}ms ${params}`);
210
- }
211
- // Log usage if authenticated
212
- // DISABLE: Granular logging disabled in favor of Session Keep-Alive
213
- /*
214
- if (apiKey) {
215
- this.dbService.logUsage({
216
- keyId: apiKey.key,
217
- userId: apiKey.userId,
218
- method,
219
- path,
220
- status,
221
- duration
222
- }).catch(e => console.error('⚠️ Failed to log usage:', (e as Error).message));
223
- }
224
- */
225
- });
226
- next();
227
- }
228
- async versionCheck(req, res, next) {
229
- // Skip version check in test environment
230
- if (process.env.NODE_ENV === 'test') {
231
- return next();
232
- }
233
- // Only check for /mcp, /files routes, or root POST (MCP) where AI agents are active
234
- const isMcpTraffic = req.path.startsWith('/mcp') ||
235
- req.path.startsWith('/files') ||
236
- (req.path === '/' && req.method === 'POST');
237
- if (!isMcpTraffic) {
238
- return next();
239
- }
240
- const clientVersion = this.getClientVersion();
241
- if (clientVersion && clientVersion !== this.serverVersion) {
242
- // Add a notice header for agents/developers to see
243
- res.setHeader('X-FRAIM-Version-Notice', `Update available: Project has ${clientVersion}, Server has ${this.serverVersion}. Run 'fraim sync' to get latest workflows and features. This will not cause issues, but you're missing all the latest capabilities of FRAIM :)`);
244
- // If it's an /mcp request, we can inject it into the response later in the handler
245
- // For now, storing it in the request object
246
- req.versionMismatch = {
247
- client: clientVersion,
248
- server: this.serverVersion
249
- };
250
- }
251
- next();
252
- }
253
- getClientVersion() {
254
- try {
255
- // Try to find FRAIM framework version in the project's node_modules or package.json
256
- const projectPkgPath = (0, path_1.join)(process.cwd(), 'package.json');
257
- if ((0, fs_1.existsSync)(projectPkgPath)) {
258
- const pkg = JSON.parse((0, fs_1.readFileSync)(projectPkgPath, 'utf8'));
259
- // Check dependencies or devDependencies
260
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
261
- const version = deps['@fraim/framework'];
262
- if (version) {
263
- // Clean version string (remove ^, ~)
264
- return version.replace(/[\^~]/, '');
265
- }
266
- }
267
- // Fallback: check .fraim/config.json if we implement versioning there
268
- const configPath = (0, path_1.join)(process.cwd(), '.fraim', 'config.json');
269
- if ((0, fs_1.existsSync)(configPath)) {
270
- const config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
271
- if (config.version)
272
- return config.version;
273
- }
274
- }
275
- catch (e) {
276
- // Ignore errors
277
- }
278
- return null;
279
- }
280
- /**
281
- * Middleware to authenticate requests via API key
282
- */
283
- async authMiddleware(req, res, next) {
284
- // Skip auth for public health check, admin routes, website signup, and sales inquiries
285
- if (req.path === '/health' || req.path.startsWith('/admin') || req.path === '/api/signup' || req.path === '/api/sales') {
286
- return next();
287
- }
288
- const apiKey = req.headers['x-api-key'] || req.query['api-key'];
289
- // In test mode, still extract API key if provided but don't validate it
290
- if (process.env.NODE_ENV === 'test') {
291
- if (apiKey) {
292
- // Set mock API key data for testing
293
- req.apiKeyData = {
294
- key: apiKey,
295
- userId: 'test-user',
296
- orgId: 'test-org',
297
- isActive: true
298
- };
299
- }
300
- return next();
301
- }
302
- if (!apiKey) {
303
- console.error(`[FRAIM AUTH] Missing API key for ${req.method} ${req.path}`);
304
- res.status(401).json({ error: 'Unauthorized', message: 'Missing API key' });
305
- return;
306
- }
307
- try {
308
- const apiKeyData = await this.dbService.verifyApiKey(apiKey);
309
- if (apiKeyData) {
310
- req.apiKeyData = apiKeyData;
311
- return next();
312
- }
313
- console.error(`❌ FRAIM AUTH: Invalid API key: ${apiKey}`);
314
- res.status(401).json({
315
- error: 'Unauthorized',
316
- message: 'Invalid x-api-key'
317
- });
318
- }
319
- catch (error) {
320
- const msg = error instanceof Error ? error.message : String(error);
321
- console.error('❌ FRAIM AUTH: Error during verification:', msg);
322
- if (error instanceof Error && error.stack)
323
- console.error(error.stack);
324
- res.status(500).json({ error: 'Internal Server Error', details: msg });
325
- }
326
- }
327
- adminAuthMiddleware(req, res, next) {
328
- const adminKey = process.env.FRAIM_ADMIN_KEY;
329
- if (!adminKey) {
330
- console.error('⚠️ FRAIM_ADMIN_KEY not configured on server');
331
- return res.status(503).json({ error: 'Management API disabled (key not set)' });
332
- }
333
- const providedKey = req.headers['x-admin-key'];
334
- if (providedKey !== adminKey) {
335
- return res.status(403).json({ error: 'Forbidden: Invalid admin key' });
336
- }
337
- return next();
338
- }
339
- /**
340
- * Middleware to enforce Session Handshake and track activity
341
- */
342
- async telemetryMiddleware(req, res, next) {
343
- // Skip for non-authenticated or exempt paths
344
- const exemptPaths = ['/health', '/admin', '/mcp'];
345
- if (exemptPaths.some(p => req.path.startsWith(p)) && req.method === 'GET') {
346
- return next();
347
- }
348
- // 1. Standard MCP Lifecycle & Discovery methods (Initialize, List Tools/Resources/Prompts, etc.)
349
- const exemptMethods = [
350
- 'initialize',
351
- 'notifications/initialized',
352
- 'tools/list',
353
- 'resources/list',
354
- 'prompts/list',
355
- 'logging/setLevel'
356
- ];
357
- if (req.body && exemptMethods.includes(req.body.method)) {
358
- return next();
359
- }
360
- // 2. FRAIM Bootstrap/Discovery Tools that don't need an active session
361
- if (req.body && req.body.method === 'tools/call') {
362
- const toolName = req.body.params?.name;
363
- const safeTools = [
364
- 'fraim_connect',
365
- 'get_fraim_init',
366
- 'list_fraim_workflows',
367
- 'fraim_get_local_config'
368
- ];
369
- if (safeTools.includes(toolName))
370
- return next();
371
- }
372
- const apiKey = req.apiKeyData?.key;
373
- if (!apiKey)
374
- return next(); // Auth middleware handles 401 earlier
375
- // Check Session
376
- const isActive = await this.sessionManager.updateActivity(apiKey);
377
- if (!isActive) {
378
- console.log(`⛔ Telemetry blocked request: ${req.method} ${req.path} (No Session for ${apiKey})`);
379
- // Enforce Handshake with clear instructions for agents
380
- res.status(400).json({
381
- jsonrpc: '2.0',
382
- error: {
383
- code: -32600,
384
- message: `⛔ Session Not Started. REQUIRED ACTION: Call 'fraim_connect' tool first with these parameters:
385
-
386
- {
387
- "agent": {
388
- "name": "YourAgentName",
389
- "model": "your-model-name"
390
- },
391
- "machine": {
392
- "hostname": "use os.hostname()",
393
- "platform": "use process.platform"
394
- },
395
- "repo": {
396
- "url": "git remote URL",
397
- "owner": "repo owner",
398
- "name": "repo name"
399
- }
400
- }
401
-
402
- After successful fraim_connect, all other FRAIM tools will work. This is required for telemetry and session management.`
403
- },
404
- id: req.body?.id || null
405
- });
406
- return;
407
- }
408
- console.log(`✅ Telemetry active for ${apiKey}`);
409
- return next();
410
- }
411
- /**
412
- * Get the Express application for mounting in another server
413
- * This allows FRAIM routes to be accessible on the main server port (e.g., on Azure)
414
- */
415
- getApp() {
416
- return this.app;
417
- }
418
- /**
419
- * Find registry directory
420
- * Checks dist/registry (production) first, then registry (development)
421
- */
422
- findRegistryPath() {
423
- const distPath = (0, path_1.join)(process.cwd(), 'dist', 'registry');
424
- const sourcePath = (0, path_1.join)(process.cwd(), 'registry');
425
- if ((0, fs_1.existsSync)(distPath)) {
426
- console.log(`📁 Using registry from dist(production)`);
427
- return distPath;
428
- }
429
- else if ((0, fs_1.existsSync)(sourcePath)) {
430
- console.log(`📁 Using registry from source(development)`);
431
- return sourcePath;
432
- }
433
- else {
434
- console.warn(`⚠️ registry directory not found at ${distPath} or ${sourcePath} `);
435
- return sourcePath; // Default to source path
436
- }
437
- }
438
- /**
439
- * Build an index of all files in registry and .fraim directories
440
- */
441
- buildFileIndex() {
442
- this.fileIndex.clear();
443
- // 1. Index Global registry (packaged with the server)
444
- // In development, prefer source registry over dist registry
445
- let globalRegistryPath = (0, path_1.join)(__dirname, '..', 'registry');
446
- if (!(0, fs_1.existsSync)(globalRegistryPath)) {
447
- // Fallback to dist registry for production
448
- globalRegistryPath = (0, path_1.join)(__dirname, '..', '..', 'registry');
449
- }
450
- if ((0, fs_1.existsSync)(globalRegistryPath)) {
451
- console.log(`🌍 Indexing global registry from ${globalRegistryPath}`);
452
- this.indexDirectory(globalRegistryPath, '');
453
- }
454
- // 2. Index Local registry (project-specific framework files)
455
- const localRegistryPath = (0, path_1.join)(process.cwd(), 'registry');
456
- if ((0, fs_1.existsSync)(localRegistryPath) && localRegistryPath !== globalRegistryPath) {
457
- console.log(`🏠 Indexing local registry overrides from ${localRegistryPath}`);
458
- this.indexDirectory(localRegistryPath, '');
459
- }
460
- // 3. Index .fraim custom directories (project-specific customizations)
461
- const fraimBasePath = (0, path_1.join)(process.cwd(), '.fraim');
462
- if ((0, fs_1.existsSync)(fraimBasePath)) {
463
- console.log(`🎨 Indexing project-specific .fraim customizations`);
464
- // Index custom rules
465
- const rulesPath = (0, path_1.join)(process.cwd(), '.fraim/rules');
466
- if ((0, fs_1.existsSync)(rulesPath)) {
467
- this.indexDirectory(rulesPath, 'fraim/rules');
468
- }
469
- // Index custom workflows (use default path)
470
- const workflowsPath = (0, path_1.join)(process.cwd(), '.fraim/workflows');
471
- if ((0, fs_1.existsSync)(workflowsPath)) {
472
- this.indexDirectory(workflowsPath, 'fraim/workflows');
473
- }
474
- // Index custom templates
475
- const templatesPath = (0, path_1.join)(process.cwd(), '.fraim/templates');
476
- if ((0, fs_1.existsSync)(templatesPath)) {
477
- this.indexDirectory(templatesPath, 'fraim/templates');
478
- }
479
- // Index custom scripts
480
- const scriptsPath = (0, path_1.join)(process.cwd(), '.fraim/scripts');
481
- if ((0, fs_1.existsSync)(scriptsPath)) {
482
- this.indexDirectory(scriptsPath, 'fraim/scripts');
483
- }
484
- }
485
- console.log(`📚 Indexed ${this.fileIndex.size} files total (Global + Local)`);
486
- }
487
- /**
488
- * Index files in a directory
489
- */
490
- indexDirectory(dir, basePath = '') {
491
- const indexFile = (dirPath, relativeBase = '') => {
492
- if (!(0, fs_1.existsSync)(dirPath)) {
493
- return;
494
- }
495
- const entries = (0, fs_1.readdirSync)(dirPath, { withFileTypes: true });
496
- for (const entry of entries) {
497
- const fullPath = (0, path_1.join)(dirPath, entry.name);
498
- const relativePath = relativeBase ? (0, path_1.join)(relativeBase, entry.name) : entry.name;
499
- const normalizedPath = relativePath.replace(/\\/g, '/'); // Normalize path separators
500
- if (entry.isDirectory()) {
501
- // Skip nested workspace directories
502
- if (entry.name.includes('Issue ') || entry.name.includes('Master')) {
503
- continue;
504
- }
505
- indexFile(fullPath, normalizedPath);
506
- }
507
- else if (entry.isFile()) {
508
- // Include .md, .ts, .sh, .html files
509
- const ext = (0, path_1.extname)(entry.name);
510
- if (['.md', '.ts', '.sh', '.html'].includes(ext)) {
511
- // Determine file type based on path
512
- let type = 'other';
513
- let category;
514
- const pathLower = normalizedPath.toLowerCase();
515
- if (pathLower.includes('workflow') || normalizedPath.startsWith('workflows/') || normalizedPath.startsWith('fraim/workflows/')) {
516
- type = 'workflow';
517
- const parts = normalizedPath.split('/');
518
- if (parts.length > 2) {
519
- category = parts[parts.length - 2]; // e.g., 'customer-development'
520
- }
521
- }
522
- else if (pathLower.includes('rule') || normalizedPath.startsWith('rules/') || normalizedPath.startsWith('fraim/rules/')) {
523
- type = 'rule';
524
- }
525
- else if (pathLower.includes('template') || normalizedPath.startsWith('templates/') || normalizedPath.startsWith('fraim/templates/')) {
526
- type = 'template';
527
- const parts = normalizedPath.split('/');
528
- if (parts.length > 2) {
529
- category = parts[parts.length - 2]; // e.g., 'specs', 'evidence'
530
- }
531
- }
532
- else if (pathLower.includes('script') || normalizedPath.startsWith('scripts/') || normalizedPath.startsWith('fraim/scripts/')) {
533
- type = 'script';
534
- }
535
- else if (normalizedPath.includes('ai-manager-rules/') && normalizedPath.includes('-phases/')) {
536
- type = 'phase';
537
- const parts = normalizedPath.split('/');
538
- if (parts.length > 2) {
539
- category = parts[parts.length - 2]; // e.g., 'implement-phases', 'spec-phases'
540
- }
541
- }
542
- // Extract keywords from filename
543
- const keywords = this.extractKeywords(entry.name, normalizedPath);
544
- const metadata = {
545
- path: normalizedPath,
546
- name: entry.name,
547
- type,
548
- category,
549
- keywords,
550
- fullPath: fullPath
551
- };
552
- // Use normalized path as key (e.g., 'workflows/spec.md' or 'fraim/rules/custom-rule.md')
553
- // Custom files override generic ones if they have the same path
554
- this.fileIndex.set(normalizedPath, metadata);
555
- }
556
- }
557
- }
558
- };
559
- indexFile(dir, basePath);
560
- }
561
- /**
562
- * Extract keywords from filename and path
563
- */
564
- extractKeywords(filename, path) {
565
- const keywords = [];
566
- // Add filename without extension
567
- const nameWithoutExt = (0, path_1.basename)(filename, (0, path_1.extname)(filename));
568
- keywords.push(nameWithoutExt.toLowerCase());
569
- // Split on hyphens/underscores
570
- const parts = nameWithoutExt.split(/[-_]/);
571
- keywords.push(...parts.map(p => p.toLowerCase()));
572
- // Add path components
573
- const pathParts = path.split('/');
574
- keywords.push(...pathParts.map(p => p.toLowerCase()));
575
- return [...new Set(keywords)]; // Remove duplicates
576
- }
577
- /**
578
- * Build keyword mappings for workflows to enable context-aware selection
579
- */
580
- buildWorkflowKeywords() {
581
- // Map common user intents to workflow files
582
- this.workflowKeywords.set('spec', ['spec', 'specification', 'write spec', 'create spec', 'specification phase']);
583
- this.workflowKeywords.set('design', ['design', 'rfc', 'technical design', 'architecture', 'design phase']);
584
- this.workflowKeywords.set('implement', ['implement', 'implementation', 'code', 'develop', 'implementation phase']);
585
- this.workflowKeywords.set('test', ['test', 'testing', 'write tests', 'test phase']);
586
- this.workflowKeywords.set('resolve', ['resolve', 'merge', 'close issue', 'resolution']);
587
- this.workflowKeywords.set('retrospect', ['retrospect', 'retrospective', 'review']);
588
- // Customer development workflows
589
- this.workflowKeywords.set('linkedin-outreach', ['linkedin', 'outreach', 'customer development']);
590
- this.workflowKeywords.set('interview-preparation', ['interview', 'customer interview', 'preparation']);
591
- this.workflowKeywords.set('insight-analysis', ['insight', 'analysis', 'customer insight']);
592
- this.workflowKeywords.set('strategic-brainstorming', ['brainstorm', 'strategy', 'strategic']);
593
- this.workflowKeywords.set('thank-customers', ['thank', 'gratitude', 'appreciation']);
594
- this.workflowKeywords.set('weekly-newsletter', ['newsletter', 'weekly update']);
595
- // New lifecycle workflows
596
- this.workflowKeywords.set('prototype', ['prototype', 'poc', 'spike', 'proof of concept']);
597
- this.workflowKeywords.set('marketing-strategy', ['marketing', 'promotion', 'launch strategy', 'gtm']);
598
- this.workflowKeywords.set('content-creation', ['content', 'blog', 'social media', 'docs']);
599
- this.workflowKeywords.set('launch-checklist', ['launch', 'release', 'go live']);
600
- }
601
- /**
602
- * Find workflows and related files based on user query
603
- */
604
- findRelevantFiles(query) {
605
- const queryLower = query.toLowerCase();
606
- const results = [];
607
- const matched = new Set();
608
- // First, check for exact workflow matches
609
- for (const [workflowKey, keywords] of this.workflowKeywords.entries()) {
610
- if (keywords.some(keyword => queryLower.includes(keyword))) {
611
- // Find the workflow file
612
- const workflowPath = `workflows / ${workflowKey}.md`;
613
- const metadata = this.fileIndex.get(workflowPath);
614
- if (metadata && !matched.has(workflowPath)) {
615
- results.push(metadata);
616
- matched.add(workflowPath);
617
- }
618
- // Add related files based on workflow type
619
- this.addRelatedFiles(workflowKey, results, matched);
620
- }
621
- }
622
- // If no exact match, search by keywords in file index
623
- if (results.length === 0) {
624
- for (const [path, metadata] of this.fileIndex.entries()) {
625
- if (metadata.keywords.some(keyword => queryLower.includes(keyword))) {
626
- if (!matched.has(path)) {
627
- results.push(metadata);
628
- matched.add(path);
629
- }
630
- }
631
- }
632
- }
633
- return results;
634
- }
635
- /**
636
- * Add related files based on workflow type
637
- */
638
- addRelatedFiles(workflowKey, results, matched) {
639
- // For spec workflow, include spec template
640
- if (workflowKey === 'spec') {
641
- const specTemplate = this.fileIndex.get('templates/specs/FEATURESPEC-TEMPLATE.md');
642
- if (specTemplate && !matched.has('templates/specs/FEATURESPEC-TEMPLATE.md')) {
643
- results.push(specTemplate);
644
- matched.add('templates/specs/FEATURESPEC-TEMPLATE.md');
645
- }
646
- // Add spec evidence template
647
- const specEvidence = this.fileIndex.get('templates/evidence/Spec-Evidence.md');
648
- if (specEvidence && !matched.has('templates/evidence/Spec-Evidence.md')) {
649
- results.push(specEvidence);
650
- matched.add('templates/evidence/Spec-Evidence.md');
651
- }
652
- // Add relevant rules
653
- this.addRuleFiles(['communication', 'pr-workflow-completeness'], results, matched);
654
- }
655
- // For design workflow, include design templates and rules
656
- if (workflowKey === 'design') {
657
- const techSpecTemplate = this.fileIndex.get('templates/specs/TECHSPEC-TEMPLATE.md');
658
- if (techSpecTemplate && !matched.has('templates/specs/TECHSPEC-TEMPLATE.md')) {
659
- results.push(techSpecTemplate);
660
- matched.add('templates/specs/TECHSPEC-TEMPLATE.md');
661
- }
662
- const bugSpecTemplate = this.fileIndex.get('templates/specs/BUGSPEC-TEMPLATE.md');
663
- if (bugSpecTemplate && !matched.has('templates/specs/BUGSPEC-TEMPLATE.md')) {
664
- results.push(bugSpecTemplate);
665
- matched.add('templates/specs/BUGSPEC-TEMPLATE.md');
666
- }
667
- const designEvidence = this.fileIndex.get('templates/evidence/Design-Evidence.md');
668
- if (designEvidence && !matched.has('templates/evidence/Design-Evidence.md')) {
669
- results.push(designEvidence);
670
- matched.add('templates/evidence/Design-Evidence.md');
671
- }
672
- this.addRuleFiles(['spike-first-development', 'communication', 'pr-workflow-completeness'], results, matched);
673
- }
674
- // For implement workflow, include implementation templates and rules
675
- if (workflowKey === 'implement') {
676
- const implEvidence = this.fileIndex.get('templates/evidence/Implementation-FeatureEvidence.md');
677
- if (implEvidence && !matched.has('templates/evidence/Implementation-FeatureEvidence.md')) {
678
- results.push(implEvidence);
679
- matched.add('templates/evidence/Implementation-FeatureEvidence.md');
680
- }
681
- const bugEvidence = this.fileIndex.get('templates/evidence/Implementation-BugEvidence.md');
682
- if (bugEvidence && !matched.has('templates/evidence/Implementation-BugEvidence.md')) {
683
- results.push(bugEvidence);
684
- matched.add('templates/evidence/Implementation-BugEvidence.md');
685
- }
686
- this.addRuleFiles([
687
- 'mandatory-pre-completion-reflection',
688
- 'agent-testing-guidelines',
689
- 'code-quality-and-debugging-patterns',
690
- 'spike-first-development',
691
- 'pr-workflow-completeness'
692
- ], results, matched);
693
- }
694
- // For test workflow, include test rules
695
- if (workflowKey === 'test') {
696
- this.addRuleFiles(['agent-testing-guidelines', 'integrity-and-test-ethics'], results, matched);
697
- }
698
- // New workflows related files
699
- if (workflowKey === 'prototype') {
700
- this.addRuleFiles(['spike-first-development'], results, matched);
701
- }
702
- if (workflowKey === 'marketing-strategy') {
703
- const contentCreation = this.fileIndex.get('workflows/marketing/content-creation.md');
704
- if (contentCreation && !matched.has('workflows/marketing/content-creation.md')) {
705
- results.push(contentCreation);
706
- matched.add('workflows/marketing/content-creation.md');
707
- }
708
- const launchChecklist = this.fileIndex.get('workflows/marketing/launch-checklist.md');
709
- if (launchChecklist && !matched.has('workflows/marketing/launch-checklist.md')) {
710
- results.push(launchChecklist);
711
- matched.add('workflows/marketing/launch-checklist.md');
712
- }
713
- }
714
- }
715
- /**
716
- * Add rule files to results
717
- */
718
- addRuleFiles(ruleNames, results, matched) {
719
- for (const ruleName of ruleNames) {
720
- // Check for project-specific override first
721
- const customRulePath = `fraim/rules/${ruleName}.md`;
722
- const genericRulePath = `rules/${ruleName}.md`;
723
- const rule = this.fileIndex.get(customRulePath) || this.fileIndex.get(genericRulePath);
724
- if (rule && !matched.has(rule.path)) {
725
- results.push(rule);
726
- matched.add(rule.path);
727
- }
728
- }
729
- }
730
- setupRoutes() {
731
- // Files indexing endpoint
732
- this.app.get('/', (req, res) => {
733
- res.json({
734
- name: 'FRAIM MCP Server',
735
- version: '1.0.0',
736
- description: 'MCP server for .ai-agents files with context-aware workflow selection',
737
- endpoints: {
738
- health: '/health',
739
- mcp: '/mcp',
740
- resources: '/resources'
741
- },
742
- files_indexed: this.fileIndex.size
743
- });
744
- });
745
- // MCP endpoint - GET for SSE connections (optional, some clients use this)
746
- this.app.get('/mcp', (req, res) => {
747
- // Allow GET requests without strict Accept header check
748
- // Some MCP clients may use GET for health checks
749
- const acceptHeader = req.headers.accept || '';
750
- if (acceptHeader.includes('text/event-stream')) {
751
- // SSE connection
752
- res.writeHead(200, {
753
- 'Content-Type': 'text/event-stream',
754
- 'Cache-Control': 'no-cache',
755
- 'Connection': 'keep-alive',
756
- 'Access-Control-Allow-Origin': '*'
757
- });
758
- // Handle client disconnects to prevent server crash
759
- res.on('error', (err) => {
760
- console.error('⚠️ SSE stream error:', err);
761
- });
762
- res.write('event: endpoint\n');
763
- // Use absolute URL to be safe
764
- const protocol = req.protocol;
765
- const host = req.get('host');
766
- res.write(`data: ${protocol}://${host}/mcp\n\n`);
767
- // Remove confusing "connected" message that might interfere with protocol
768
- // res.write(': FRAIM MCP Server\n\n');
769
- // res.write('data: connected\n\n');
770
- return;
771
- }
772
- // Regular GET request
773
- res.json({
774
- message: 'FRAIM MCP Server is running. Use POST /mcp for MCP requests.'
775
- });
776
- });
777
- // Admin routes (protected by admin key)
778
- this.app.use('/admin', this.adminAuthMiddleware.bind(this));
779
- this.app.post('/admin/keys', async (req, res) => {
780
- const { userId, orgId } = req.body;
781
- if (!userId || !orgId) {
782
- return res.status(400).json({ error: 'userId and orgId are required' });
783
- }
784
- const apiKey = this.dbService.generateApiKey(userId, orgId);
785
- await this.dbService.createApiKey({ key: apiKey, userId, orgId });
786
- res.json({ key: apiKey });
787
- });
788
- this.app.get('/admin/keys', async (req, res) => {
789
- const keys = await this.dbService.listApiKeys();
790
- res.json(keys);
791
- });
792
- this.app.post('/admin/keys/revoke', async (req, res) => {
793
- const { key } = req.body;
794
- if (!key) {
795
- return res.status(400).json({ error: 'key is required' });
796
- }
797
- const success = await this.dbService.revokeApiKey(key);
798
- res.json({ revoked: success });
799
- });
800
- // Website signup endpoint (public, no auth required)
801
- this.app.post('/api/signup', async (req, res) => {
802
- try {
803
- const { email, company, useCase, source } = req.body;
804
- if (!email) {
805
- return res.status(400).json({ error: 'Email is required' });
806
- }
807
- // Basic email validation
808
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
809
- if (!emailRegex.test(email)) {
810
- return res.status(400).json({ error: 'Invalid email format' });
811
- }
812
- // Get client IP and user agent for analytics
813
- const ipAddress = req.ip || req.connection.remoteAddress || req.headers['x-forwarded-for'];
814
- const userAgent = req.headers['user-agent'];
815
- const signup = {
816
- email: email.toLowerCase().trim(),
817
- company: company?.trim() || undefined,
818
- useCase: useCase || undefined,
819
- source: source || 'website',
820
- timestamp: new Date(),
821
- ipAddress,
822
- userAgent
823
- };
824
- await this.dbService.createWebsiteSignup(signup);
825
- console.log(`✅ New website signup: ${email} (${company || 'No company'}) - ${useCase || 'No use case'}`);
826
- res.json({
827
- success: true,
828
- message: 'Successfully joined the waitlist!'
829
- });
830
- }
831
- catch (error) {
832
- console.error('❌ Website signup error:', error);
833
- if (error.message === 'Email already registered') {
834
- return res.status(409).json({
835
- error: 'Email already registered',
836
- message: 'This email is already on our waitlist.'
837
- });
838
- }
839
- res.status(500).json({
840
- error: 'Internal server error',
841
- message: 'Failed to process signup. Please try again.'
842
- });
843
- }
844
- });
845
- // Sales inquiry endpoint (public, no auth required)
846
- this.app.post('/api/sales', async (req, res) => {
847
- try {
848
- const { email, company, projectDetails, teamSize, timeline, budget, source } = req.body;
849
- if (!email || !company || !projectDetails) {
850
- return res.status(400).json({
851
- error: 'Missing required fields',
852
- message: 'Email, company, project details are required'
853
- });
854
- }
855
- // Basic email validation
856
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
857
- if (!emailRegex.test(email)) {
858
- return res.status(400).json({ error: 'Invalid email format' });
859
- }
860
- // Get client IP and user agent for analytics
861
- const ipAddress = req.ip || req.connection.remoteAddress || req.headers['x-forwarded-for'];
862
- const userAgent = req.headers['user-agent'];
863
- const inquiry = {
864
- email: email.toLowerCase().trim(),
865
- company: company.trim(),
866
- projectDetails: projectDetails.trim(),
867
- teamSize: teamSize?.trim() || undefined,
868
- timeline: timeline?.trim() || undefined,
869
- budget: budget?.trim() || undefined,
870
- source: source || 'website',
871
- timestamp: new Date(),
872
- ipAddress,
873
- userAgent
874
- };
875
- await this.dbService.createSalesInquiry(inquiry);
876
- console.log(`💼 New sales inquiry: ${email} from ${company} - Team: ${teamSize}`);
877
- res.json({
878
- success: true,
879
- message: 'Sales inquiry submitted successfully! We\'ll be in touch soon.'
880
- });
881
- }
882
- catch (error) {
883
- console.error('❌ Sales inquiry error:', error);
884
- res.status(500).json({
885
- error: 'Internal server error',
886
- message: 'Failed to process sales inquiry. Please try again.'
887
- });
888
- }
889
- });
890
- // Get signups endpoint (admin only)
891
- this.app.get('/admin/signups', async (req, res) => {
892
- try {
893
- const limit = parseInt(req.query.limit) || 100;
894
- const signups = await this.dbService.getWebsiteSignups(limit);
895
- res.json({ signups, total: signups.length });
896
- }
897
- catch (error) {
898
- console.error('❌ Failed to get signups:', error);
899
- res.status(500).json({ error: 'Failed to retrieve signups' });
900
- }
901
- });
902
- // Get sales inquiries endpoint (admin only)
903
- this.app.get('/admin/sales', async (req, res) => {
904
- try {
905
- const limit = parseInt(req.query.limit) || 100;
906
- const inquiries = await this.dbService.getSalesInquiries(limit);
907
- res.json({ inquiries, total: inquiries.length });
908
- }
909
- catch (error) {
910
- console.error('❌ Failed to get sales inquiries:', error);
911
- res.status(500).json({ error: 'Failed to retrieve sales inquiries' });
912
- }
913
- });
914
- // MCP endpoint - POST for actual MCP requests
915
- // Handle MCP requests on both /mcp (standard) and / (easier config)
916
- this.app.post(['/', '/mcp'], async (req, res) => {
917
- const { jsonrpc, method, params, id } = req.body;
918
- if (jsonrpc !== '2.0') {
919
- return res.status(400).json({ error: 'Invalid JSON-RPC version' });
920
- }
921
- try {
922
- let result;
923
- if (method === 'initialize') {
924
- result = this.handleInitialize();
925
- }
926
- else if (method === 'tools/list') {
927
- result = this.handleListTools();
928
- }
929
- else if (method === 'resources/list') {
930
- result = this.handleListResources();
931
- }
932
- else if (method === 'prompts/list') {
933
- result = this.handleListPrompts();
934
- }
935
- else if (method === 'tools/call') {
936
- // Pass API Key context to handleToolCall
937
- const context = {
938
- apiKey: req.apiKeyData?.key,
939
- userId: req.apiKeyData?.userId
940
- };
941
- result = await this.handleToolCall(params, context);
942
- }
943
- else if (method === 'notifications/initialized') {
944
- // Client initialized notification - no response needed but must not error
945
- return res.json({ jsonrpc: '2.0', id: id || null, result: true });
946
- }
947
- else {
948
- throw new Error(`Unknown method: ${method}`);
949
- }
950
- if (req.versionMismatch) {
951
- const { client, server } = req.versionMismatch;
952
- const notice = `ℹ️ **FRAIM Update Available**\n\nYour project is using FRAIM ${client}, but version ${server} is available with new features and improvements.\n\n**To update:** Run \`fraim sync\` to get the latest workflows and templates.\n\n---\n\n`;
953
- if (result && Array.isArray(result.content)) {
954
- result.content.unshift({
955
- type: 'text',
956
- text: notice
957
- });
958
- }
959
- }
960
- res.json({
961
- jsonrpc: '2.0',
962
- result,
963
- id
964
- });
965
- }
966
- catch (error) {
967
- res.status(500).json({
968
- jsonrpc: '2.0',
969
- error: {
970
- code: -32603,
971
- message: error instanceof Error ? error.message : 'Internal error'
972
- },
973
- id
974
- });
975
- }
976
- });
977
- }
978
- handleInitialize() {
979
- return {
980
- protocolVersion: '2024-11-05',
981
- serverInfo: {
982
- name: 'fraim-mcp-server',
983
- version: this.serverVersion
984
- },
985
- capabilities: {
986
- tools: {},
987
- resources: {}
988
- }
989
- };
990
- }
991
- handleListTools() {
992
- return {
993
- tools: [
994
- {
995
- name: 'get_fraim_init',
996
- description: `Initialize FRAIM with all available rules, workflows, and usage instructions.
997
-
998
- This should be the FIRST call to FRAIM in any new session.
999
- Returns:
1000
- - All rules (organized by category)
1001
- - All workflows (organized by category)
1002
- - Usage instructions and best practices
1003
- - Workflow execution guidance
1004
-
1005
- Call this before fetching any workflow or file to understand the system.`,
1006
- inputSchema: {
1007
- type: 'object',
1008
- properties: {},
1009
- required: []
1010
- }
1011
- },
1012
- {
1013
- name: 'fraim_connect',
1014
- description: `🚨 REQUIRED FIRST CALL: Handshake to start a FRAIM session. MUST be called at the start of every session.
1015
-
1016
- ⚠️ CRITICAL: If you get a "Session Not Started" error from any FRAIM tool, call this tool first.
1017
-
1018
- Registers your environment (machine/repo/agent) for telemetry and unlocks other tools.
1019
-
1020
- **Agent Information**: REQUIRED - Provide your actual agent name and model (validated against known agents).
1021
- **Machine Information**: Use os.hostname() and process.platform - required for telemetry.
1022
- **Repository Information**: Extract and send this information from .fraim/config.json.
1023
-
1024
- Example call:
1025
- {
1026
- "agent": {"name": "Claude", "model": "claude-3.5-sonnet"},
1027
- "machine": {"hostname": "my-machine", "platform": "darwin"},
1028
- "repo": {"url": "https://github.com/owner/repo", "owner": "owner", "name": "repo"}
1029
- }`,
1030
- inputSchema: {
1031
- type: 'object',
1032
- properties: {
1033
- agent: {
1034
- type: 'object',
1035
- description: 'Agent identification and capabilities',
1036
- properties: {
1037
- name: {
1038
- type: 'string',
1039
- description: 'Agent name (e.g., "Claude", "Cursor", "Kiro", "Windsurf", "Antigravity")',
1040
- examples: ['Claude', 'Cursor', 'Kiro', 'Windsurf', 'Antigravity']
1041
- },
1042
- model: {
1043
- type: 'string',
1044
- description: 'Model name/version (e.g., "claude-3.5-sonnet", "gpt-4", "cursor-small")',
1045
- examples: ['claude-3.5-sonnet', 'gpt-4', 'cursor-small', 'kiro-agent']
1046
- },
1047
- version: {
1048
- type: 'string',
1049
- description: 'Agent version if available',
1050
- examples: ['1.0.0', '2024.12.1']
1051
- }
1052
- },
1053
- required: ['name', 'model'],
1054
- additionalProperties: true
1055
- },
1056
- machine: {
1057
- type: 'object',
1058
- description: 'Machine specs - use os.hostname(), process.platform, os.totalmem(), os.cpus().length',
1059
- properties: {
1060
- hostname: {
1061
- type: 'string',
1062
- description: 'Machine hostname from os.hostname()'
1063
- },
1064
- platform: {
1065
- type: 'string',
1066
- description: 'Platform from process.platform (win32, darwin, linux)'
1067
- },
1068
- memory: {
1069
- type: 'number',
1070
- description: 'Total memory in bytes from os.totalmem()'
1071
- },
1072
- cpus: {
1073
- type: 'number',
1074
- description: 'CPU count from os.cpus().length'
1075
- }
1076
- },
1077
- required: ['hostname', 'platform'],
1078
- additionalProperties: true
1079
- },
1080
- repo: {
1081
- type: 'object',
1082
- description: 'Repository context - use git remote -v or check .git/config for URL',
1083
- properties: {
1084
- url: {
1085
- type: 'string',
1086
- description: 'Git repository URL from git remote -v'
1087
- },
1088
- owner: {
1089
- type: 'string',
1090
- description: 'Repository owner (extracted from URL)'
1091
- },
1092
- name: {
1093
- type: 'string',
1094
- description: 'Repository name (extracted from URL)'
1095
- },
1096
- branch: {
1097
- type: 'string',
1098
- description: 'Current branch from git branch --show-current'
1099
- }
1100
- },
1101
- required: ['url'],
1102
- additionalProperties: true
1103
- }
1104
- },
1105
- required: ['agent', 'machine', 'repo']
1106
- }
1107
- },
1108
- {
1109
- name: 'get_fraim_file',
1110
- description: `Get a specific file from FRAIM by path.
1111
-
1112
- Examples:
1113
- - get_fraim_file({ path: "rules/communication.md" })
1114
- - get_fraim_file({ path: "workflows/spec.md" })
1115
- - get_fraim_file({ path: "templates/specs/FEATURESPEC-TEMPLATE.md" })
1116
-
1117
- NOTE: If a .fraim override exists for a rule, workflow, template, or script,
1118
- FRAIM will return the project-specific version instead of the generic one.`,
1119
- inputSchema: {
1120
- type: 'object',
1121
- properties: {
1122
- path: {
1123
- type: 'string',
1124
- description: 'Path to the file (e.g., rules/communication.md, workflows/spec.md)'
1125
- }
1126
- },
1127
- required: ['path']
1128
- }
1129
- },
1130
- {
1131
- name: 'get_fraim_workflow',
1132
- description: `Get a workflow file by name. Returns the workflow content.
1133
-
1134
- Examples:
1135
- - get_fraim_workflow({ workflow: "spec" })
1136
- - get_fraim_workflow({ workflow: "design" })
1137
- - get_fraim_workflow({ workflow: "implement" })
1138
-
1139
- After receiving the workflow, follow any ACTION items to fetch related files.
1140
-
1141
- Common workflows:
1142
- - "prep-issue" - Prepare environment for working on an issue
1143
- - "spec" - Write feature specifications
1144
- - "design" - Create technical designs
1145
- - "implement" - Implement features / fixes
1146
- - "test" - Write tests
1147
- - "resolve" - Merge to master
1148
-
1149
- Call get_fraim_init first to see all available workflows organized by category.`,
1150
- inputSchema: {
1151
- type: 'object',
1152
- properties: {
1153
- workflow: {
1154
- type: 'string',
1155
- description: 'Workflow name (e.g., "spec", "design", "implement", "prep-issue")'
1156
- }
1157
- },
1158
- required: ['workflow']
1159
- }
1160
- },
1161
- {
1162
- name: 'list_fraim_workflows',
1163
- description: `List all available workflows organized by category.
1164
-
1165
- Returns workflows grouped by:
1166
- - Product Development (prep-issue, spec, design, implement, test, resolve)
1167
- - Customer Development (linkedin-outreach, customer-interview, etc.)
1168
- - Business Development (partnership-outreach, investor-pitch, etc.)
1169
-
1170
- Use this to discover which workflow to call with get_fraim_workflow.`,
1171
- inputSchema: {
1172
- type: 'object',
1173
- properties: {},
1174
- required: []
1175
- }
1176
- },
1177
- {
1178
- name: 'fraim_get_local_config',
1179
- description: 'Get instructions for reading and using local .fraim/config.json from your workspace. Use this to understand project-specific settings and replace template variables.',
1180
- inputSchema: {
1181
- type: 'object',
1182
- properties: {},
1183
- required: []
1184
- }
1185
- },
1186
- {
1187
- name: 'file_issue',
1188
- description: `Create a GitHub issue in the FRAIM repository.
1189
-
1190
- Use this tool when you need to report a bug, request a feature, or track a task.
1191
- Supports dry-run mode to preview the operation.`,
1192
- inputSchema: {
1193
- type: 'object',
1194
- properties: {
1195
- title: {
1196
- type: 'string',
1197
- description: 'Title of the issue'
1198
- },
1199
- body: {
1200
- type: 'string',
1201
- description: 'Body/Content of the issue'
1202
- },
1203
- labels: {
1204
- type: 'array',
1205
- items: { type: 'string' },
1206
- description: 'List of labels to apply'
1207
- },
1208
- dryRun: {
1209
- type: 'boolean',
1210
- description: 'If true, simulates the creation without actually notifying GitHub'
1211
- }
1212
- },
1213
- required: ['title', 'body']
1214
- }
1215
- },
1216
- {
1217
- name: 'seekCoachingOnNextStep',
1218
- description: `Get coaching on what to do next in your implementation workflow.
1219
-
1220
- The AI Coach provides phase-specific instructions based on your current progress:
1221
- - If starting: Get initial phase instructions (usually scoping)
1222
- - If completed phase: Provide evidence for validation, get next phase instructions
1223
- - If stuck/incomplete: Get guidance to continue current phase
1224
-
1225
- This is your single point of contact for workflow guidance with evidence validation.`,
1226
- inputSchema: {
1227
- type: 'object',
1228
- properties: {
1229
- workflowType: {
1230
- type: 'string',
1231
- description: 'Type of workflow you are following',
1232
- enum: ['implement', 'spec', 'design', 'test']
1233
- },
1234
- issueNumber: {
1235
- type: 'string',
1236
- description: 'Issue number you are working on'
1237
- },
1238
- currentPhase: {
1239
- type: 'string',
1240
- description: 'Phase you just completed or are working on (e.g., "scoping", "repro", "code")'
1241
- },
1242
- status: {
1243
- type: 'string',
1244
- description: 'Status of your current phase',
1245
- enum: ['starting', 'complete', 'incomplete', 'failure']
1246
- },
1247
- findings: {
1248
- type: 'object',
1249
- description: 'Your findings/results from the current phase (varies by phase)',
1250
- properties: {
1251
- uncertainties: {
1252
- type: 'array',
1253
- items: { type: 'string' },
1254
- description: 'Any unclear aspects that need clarification (used for help messages)'
1255
- }
1256
- },
1257
- additionalProperties: true
1258
- },
1259
- evidence: {
1260
- type: 'object',
1261
- description: 'Evidence submission for validation (varies by phase - can contain any relevant evidence)',
1262
- additionalProperties: true
1263
- }
1264
- },
1265
- required: ['workflowType', 'issueNumber', 'currentPhase', 'status']
1266
- }
1267
- }
1268
- ]
1269
- };
1270
- }
1271
- handleListResources() {
1272
- return {
1273
- resources: []
1274
- };
1275
- }
1276
- handleListPrompts() {
1277
- return {
1278
- prompts: []
1279
- };
1280
- }
1281
- async handleToolCall(params, context = {}) {
1282
- const { name: toolName, arguments: toolArgs } = params;
1283
- console.log(`🔧 MCP Server: handleToolCall called with tool: ${toolName}, args:`, JSON.stringify(toolArgs, null, 2));
1284
- switch (toolName) {
1285
- case 'get_fraim_init':
1286
- return await this.handleGetInit();
1287
- case 'get_fraim_file':
1288
- return await this.handleGetFile(toolArgs.path);
1289
- case 'get_fraim_workflow':
1290
- return await this.handleGetWorkflow(toolArgs.workflow, context.apiKey);
1291
- case 'list_fraim_workflows':
1292
- return await this.handleListWorkflows();
1293
- case 'fraim_get_local_config':
1294
- return await this.handleGetLocalConfig();
1295
- case 'file_issue':
1296
- const result = await (0, issues_1.fileFraimIssue)(toolArgs);
1297
- return {
1298
- content: [{
1299
- type: 'text',
1300
- text: JSON.stringify(result, null, 2)
1301
- }]
1302
- };
1303
- case 'fraim_connect':
1304
- return await this.handleFraimConnect(toolArgs, context.apiKey, context.userId);
1305
- case 'seekCoachingOnNextStep':
1306
- return await this.handleSeekCoachingOnNextStep(toolArgs);
1307
- default:
1308
- throw new Error(`Unknown tool: ${toolName} `);
1309
- }
1310
- }
1311
- async handleGetInit() {
1312
- // Get all workflows organized by category
1313
- const workflows = Array.from(this.fileIndex.values()).filter(f => f.type === 'workflow');
1314
- // Organize workflows by category
1315
- const categorized = {};
1316
- for (const wf of workflows) {
1317
- const category = wf.category || 'Other';
1318
- if (!categorized[category]) {
1319
- categorized[category] = [];
1320
- }
1321
- // Extract workflow name without .md extension
1322
- const workflowName = wf.name.replace('.md', '');
1323
- categorized[category].push({
1324
- name: workflowName,
1325
- path: wf.path,
1326
- description: this.getWorkflowDescription(workflowName)
1327
- });
1328
- }
1329
- // Build initialization content
1330
- let initContent = `# FRAIM - Your Development Workflow Assistant\n\n`;
1331
- initContent += `## How to Use FRAIM\n\n`;
1332
- initContent += `When the user says: \n`;
1333
- initContent += `- "I need to prototype a PoC" → Call \`get_fraim_workflow({ workflow: "prototype" })\`\n`;
1334
- initContent += `- "I want to write a spec" → Call \`get_fraim_workflow({ workflow: "spec" })\`\n`;
1335
- initContent += `- "I need to design X" → Call \`get_fraim_workflow({ workflow: "design" })\`\n`;
1336
- initContent += `- "Implement issue #123" → Call \`get_fraim_workflow({ workflow: "prep-issue" })\`, then \`implement\`\n`;
1337
- initContent += `- "Write tests" → Call \`get_fraim_workflow({ workflow: "test" })\`\n`;
1338
- initContent += `- "Marketing/Launch a feature" → Call \`get_fraim_workflow({ workflow: "marketing-strategy" })\`\n`;
1339
- initContent += `- "Merge this PR" → Call \`get_fraim_workflow({ workflow: "resolve" })\`\n\n`;
1340
- initContent += `## Available Workflows\n\n`;
1341
- for (const [category, wfs] of Object.entries(categorized)) {
1342
- initContent += `### ${category}\n\n`;
1343
- for (const wf of wfs) {
1344
- initContent += `- **${wf.name}**: ${wf.description}\n`;
1345
- initContent += ` - Call: \`get_fraim_workflow({ workflow: "${wf.name}" })\`\n\n`;
1346
- }
1347
- }
1348
- initContent += `## Workflow Execution Pattern\n\n`;
1349
- initContent += `1. Call \`get_fraim_workflow({ workflow: "<name>" })\` to get the workflow file\n`;
1350
- initContent += `2. The workflow will contain **ACTION** items like:\n`;
1351
- initContent += ` - \`ACTION: Call get_fraim_file({ path: "rules/X.md" })\`\n`;
1352
- initContent += ` - \`ACTION: Call get_fraim_file({ path: "templates/Y.md" })\`\n`;
1353
- initContent += `3. Execute each ACTION item to fetch referenced files\n`;
1354
- initContent += `4. Follow the workflow steps with the fetched context\n\n`;
1355
- initContent += `## Important Notes\n\n`;
1356
- initContent += `- Files are NOT sent automatically - you must fetch them via ACTION items\n`;
1357
- initContent += `- This keeps context minimal and focused\n`;
1358
- initContent += `- Use \`get_fraim_file({ path: "..." })\` to fetch any referenced file\n\n`;
1359
- initContent += `## 📊 Proactive Codebase Analysis\n\n`;
1360
- initContent += `Would you like a quick analysis of your codebase for recommendations on test coverage, quality, and architecture? If yes, you can run the following tools:\n`;
1361
- initContent += `- **Test Coverage**: Run \`npm run validate-coverage\` and report gaps.\n`;
1362
- initContent += `- **Code Quality**: Run \`./.ai-agents/scripts/code-quality-check.sh\` to identify anti-patterns.\n`;
1363
- initContent += `If you find issues, offer to create GitHub bugs to address them.\n\n`;
1364
- initContent += `## 🚀 FRAIM Value Proposition\n\n`;
1365
- initContent += `**For Engineers**:\n`;
1366
- initContent += `- Deterministic coding via BAML and clear rule enforcement.\n`;
1367
- initContent += `- Automated repository setup and indexing.\n`;
1368
- initContent += `- Continuous learning from project retrospectives.\n\n`;
1369
- initContent += `**For Product/Marketing**:\n`;
1370
- initContent += `- End-to-end lifecycle support from rapid Prototyping to Go-To-Market.\n`;
1371
- initContent += `- Spec-first development with high-fidelity HTML/CSS mocks.\n`;
1372
- initContent += `- Automated customer research and outreach workflows.\n`;
1373
- initContent += `- Consistent documentation and evidence gathering.\n`;
1374
- return {
1375
- content: [
1376
- {
1377
- type: 'text',
1378
- text: initContent
1379
- }
1380
- ]
1381
- };
1382
- }
1383
- /**
1384
- * Get description for a workflow
1385
- */
1386
- getWorkflowDescription(workflowName) {
1387
- const descriptions = {
1388
- // Core Development Workflows
1389
- 'prep-issue': 'Prepare environment for working on an issue',
1390
- 'spec': 'Write feature specifications',
1391
- 'design': 'Create technical designs',
1392
- 'implement': 'Implement features/fixes',
1393
- 'test': 'Write tests',
1394
- 'resolve': 'Merge to master',
1395
- 'retrospect': 'Conduct retrospectives after task completion',
1396
- // Review Workflows
1397
- 'review-implementation-vs-design-spec': 'Review implementation against design spec',
1398
- 'review-implementation-vs-feature-spec': 'Review implementation against feature spec',
1399
- // Quality Assurance
1400
- 'browser-validation': 'Validate in browser',
1401
- 'iterative-improvement-cycle': 'Iterative improvement cycle',
1402
- // Customer Development
1403
- 'interview-preparation': 'Prepare for customer interviews',
1404
- 'linkedin-outreach': 'LinkedIn customer outreach',
1405
- 'insight-analysis': 'Analyze customer insights',
1406
- 'insight-triage': 'Triage customer insights',
1407
- 'strategic-brainstorming': 'Strategic brainstorming sessions',
1408
- 'weekly-newsletter': 'Create weekly newsletter',
1409
- 'thank-customers': 'Thank customers workflow',
1410
- // Replicate / Reverse Engineering
1411
- 'visual-analysis': 'Analyze visual designs',
1412
- 'website-discovery-analysis': 'Discover and analyze websites',
1413
- 'use-case-extraction': 'Extract use cases',
1414
- 're-implementation-strategy': 'Plan re-implementation strategy',
1415
- // New Lifecycle Workflows
1416
- 'prototype': 'Quickly explore ideas and prove concepts (spike code)',
1417
- 'marketing-strategy': 'Define value prop, target audience, and launch plans',
1418
- 'content-creation': 'Generate high-quality blog posts, social media, and docs',
1419
- 'launch-checklist': 'Final technical and marketing sign-off before release'
1420
- };
1421
- return descriptions[workflowName] || 'Workflow';
1422
- }
1423
- async handleGetFile(path) {
1424
- const metadata = this.fileIndex.get(path);
1425
- if (!metadata) {
1426
- throw new Error(`File not found: ${path}. Use list_fraim_files to see available files.`);
1427
- }
1428
- try {
1429
- let content = (0, fs_1.readFileSync)(metadata.fullPath, 'utf-8');
1430
- // Check if file contains template variables
1431
- const hasTemplateVars = /\{\{config\.[^}]+\}\}/.test(content);
1432
- // Don't process template variables here - let agent do it with local config
1433
- // content = processTemplateVariables(content, this.config);
1434
- let header = `# ${metadata.name}\n\n`;
1435
- header += `**Path:** ${path}\n`;
1436
- header += `**Type:** ${metadata.type}\n`;
1437
- if (metadata.category) {
1438
- header += `**Category:** ${metadata.category}\n`;
1439
- }
1440
- if (hasTemplateVars) {
1441
- header += `\n## ⚠️ Template Variables Detected\n\n`;
1442
- header += `This file contains template variables like \`{{config.project.name}}\`.\n\n`;
1443
- header += `**You must replace these with values from your local \`.fraim/config.json\`:**\n\n`;
1444
- header += `1. Read \`.fraim/config.json\` from your workspace\n`;
1445
- header += `2. Find the config path (e.g., for \`{{config.project.name}}\`, read \`config.project.name\`)\n`;
1446
- header += `3. Replace the variable with the actual value\n\n`;
1447
- header += `**Example:**\n`;
1448
- header += `- Template: "Project: {{config.project.name}}"`;
1449
- header += `- Your config: \`{ "project": { "name": "My App" } }\`\n`;
1450
- header += `- Result: "Project: My App"\n\n`;
1451
- header += `Use \`fraim_get_local_config\` tool for detailed instructions.\n\n`;
1452
- }
1453
- if (metadata.type === 'script') {
1454
- header += `\n## 🛡️ Ephemeral Execution Mandatory (IP Protection)\n\n`;
1455
- header += `This script MUST be executed ephemerally:\n\n`;
1456
- header += `1. **Fetch** this content from the MCP.\n`;
1457
- header += `2. **Save** to a system temporary directory (e.g., \`/tmp\` or \`%TEMP%\`), NOT the local repository.\n`;
1458
- header += `3. **Execute** from the temporary location.\n`;
1459
- header += `4. **Delete** the temporary file immediately after execution.\n\n`;
1460
- header += `Refer to \`rules/ephemeral-execution.md\` for the full policy.\n\n`;
1461
- }
1462
- header += `---\n\n`;
1463
- return {
1464
- content: [
1465
- {
1466
- type: 'text',
1467
- text: header + content
1468
- }
1469
- ]
1470
- };
1471
- }
1472
- catch (error) {
1473
- throw new Error(`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`);
1474
- }
1475
- }
1476
- async handleGetWorkflow(workflowName, apiKey) {
1477
- // Normalize workflow name (remove .md if present)
1478
- const normalizedName = workflowName.replace(/\.md$/, '');
1479
- // Try to find the workflow file
1480
- const workflowPath = `workflows/${normalizedName}.md`;
1481
- const metadata = this.fileIndex.get(workflowPath);
1482
- if (!metadata) {
1483
- // Try alternative category paths - dynamically discover categories from file index
1484
- const workflowCategories = new Set();
1485
- // Extract all unique workflow categories from the file index
1486
- for (const [path, fileMetadata] of this.fileIndex) {
1487
- if (fileMetadata.type === 'workflow' && path.startsWith('workflows/') && path.includes('/')) {
1488
- const pathParts = path.split('/');
1489
- if (pathParts.length >= 3) { // workflows/category/file.md
1490
- workflowCategories.add(pathParts[1]);
1491
- }
1492
- }
1493
- }
1494
- // Search through all discovered categories
1495
- for (const cat of workflowCategories) {
1496
- const altPath = `workflows/${cat}/${normalizedName}.md`;
1497
- const altMetadata = this.fileIndex.get(altPath);
1498
- if (altMetadata) {
1499
- return this.returnWorkflowFile(altMetadata, apiKey);
1500
- }
1501
- }
1502
- // Workflow not found
1503
- const availableWorkflows = Array.from(this.fileIndex.values())
1504
- .filter(f => f.type === 'workflow')
1505
- .map(f => f.name.replace('.md', ''));
1506
- return {
1507
- content: [{
1508
- type: 'text',
1509
- text: `Workflow "${workflowName}" not found.\n\nAvailable workflows:\n${availableWorkflows.map(w => `- ${w}`).join('\n')}\n\nUse list_fraim_workflows to see all workflows organized by category.`
1510
- }]
1511
- };
1512
- }
1513
- return this.returnWorkflowFile(metadata, apiKey);
1514
- }
1515
- /**
1516
- * Return a workflow file with its content
1517
- */
1518
- async returnWorkflowFile(metadata, apiKey) {
1519
- try {
1520
- const rawContent = (0, fs_1.readFileSync)(metadata.fullPath, 'utf-8');
1521
- // Get repository info from session if available
1522
- let repositoryInfo = null;
1523
- if (apiKey) {
1524
- try {
1525
- const session = await this.dbService.getActiveSessionByApiKey(apiKey);
1526
- if (session?.repo) {
1527
- repositoryInfo = {
1528
- owner: session.repo.owner,
1529
- name: session.repo.name,
1530
- organization: session.repo.organization,
1531
- project: session.repo.project,
1532
- url: session.repo.url,
1533
- provider: this.detectProviderFromUrl(session.repo.url)
1534
- };
1535
- }
1536
- }
1537
- catch (e) {
1538
- // Ignore session lookup errors, continue without repository info
1539
- }
1540
- }
1541
- // Process templates with provider-specific actions
1542
- const templateEngine = (0, template_processor_1.getTemplateEngine)(this.registryPath);
1543
- const processedContent = templateEngine.processWorkflow(rawContent, repositoryInfo);
1544
- // Build response
1545
- let response = `# Workflow: ${metadata.name.replace('.md', '')}\n\n`;
1546
- response += `**Path:** \`${metadata.path}\`\n`;
1547
- if (metadata.category) {
1548
- response += `**Category:** ${metadata.category}\n`;
1549
- }
1550
- // Add provider info if available
1551
- const provider = repositoryInfo?.provider || 'GITHUB';
1552
- response += `**Platform:** ${provider.toUpperCase()}\n`;
1553
- response += `\n---\n\n`;
1554
- response += processedContent;
1555
- return {
1556
- content: [{
1557
- type: 'text',
1558
- text: response
1559
- }]
1560
- };
1561
- }
1562
- catch (error) {
1563
- throw new Error(`Failed to read workflow: ${error instanceof Error ? error.message : 'Unknown error'}`);
1564
- }
1565
- }
1566
- /**
1567
- * List all workflows organized by category
1568
- */
1569
- async handleListWorkflows() {
1570
- const workflows = Array.from(this.fileIndex.values()).filter(f => f.type === 'workflow');
1571
- // Organize by category
1572
- const categorized = {};
1573
- for (const wf of workflows) {
1574
- const category = wf.category || 'Other';
1575
- if (!categorized[category]) {
1576
- categorized[category] = [];
1577
- }
1578
- const workflowName = wf.name.replace('.md', '');
1579
- categorized[category].push({
1580
- name: workflowName,
1581
- path: wf.path,
1582
- description: this.getWorkflowDescription(workflowName)
1583
- });
1584
- }
1585
- // Build response - cleaner format without file paths
1586
- let response = `# Available FRAIM Workflows\n\n`;
1587
- for (const [category, wfs] of Object.entries(categorized)) {
1588
- response += `## ${category}\n\n`;
1589
- for (const wf of wfs) {
1590
- response += `- **${wf.name}**: ${wf.description}\n`;
1591
- }
1592
- response += `\n`;
1593
- }
1594
- response += `## How to Use\n\n`;
1595
- response += `Call \`get_fraim_workflow({ workflow: "<name>" })\` with the workflow name.\n`;
1596
- response += `Example: \`get_fraim_workflow({ workflow: "spec" })\`\n`;
1597
- return {
1598
- content: [{
1599
- type: 'text',
1600
- text: response
1601
- }]
1602
- };
1603
- }
1604
- async handleListFiles(type, category) {
1605
- let files = Array.from(this.fileIndex.values());
1606
- if (type !== 'all') {
1607
- files = files.filter(f => f.type === type);
1608
- }
1609
- if (category) {
1610
- files = files.filter(f => f.category === category);
1611
- }
1612
- // Sort by type, then by path
1613
- files.sort((a, b) => {
1614
- if (a.type !== b.type) {
1615
- return a.type.localeCompare(b.type);
1616
- }
1617
- return a.path.localeCompare(b.path);
1618
- });
1619
- const filesList = files.map(f => {
1620
- return `- **${f.type}**${f.category ? ` (${f.category})` : ''}: \`${f.path}\``;
1621
- }).join('\n');
1622
- return {
1623
- content: [
1624
- {
1625
- type: 'text',
1626
- text: `# FRAIM Files\n\n**Filter:** ${type}${category ? `, category: ${category}` : ''}\n\n**Total:** ${files.length} file(s)\n\n${filesList}`
1627
- }
1628
- ]
1629
- };
1630
- }
1631
- async handleGetLocalConfig() {
1632
- const instructions = `# Using Local .fraim/config.json
1633
-
1634
- ## Overview
1635
-
1636
- FRAIM workflows and templates are generic and served from the cloud. However, **you must use your local project configuration** from \`.fraim/config.json\` in your workspace to customize them for your specific project.
1637
-
1638
- ## How to Read Local Config
1639
-
1640
- 1. **Locate the Config File**
1641
- - Path: \`.fraim/config.json\` in your current workspace directory
1642
- - If it doesn't exist, use the \`fraim_setup\` tool to create it
1643
-
1644
- 2. **Read the Config**
1645
- - Use your file reading tools to read \`.fraim/config.json\`
1646
- - Parse it as JSON
1647
- - Access nested values using dot notation (e.g., \`config.project.name\`)
1648
-
1649
- 3. **Use Config Values**
1650
- - Replace template variables like \`{{config.project.name}}\` with actual values
1651
- - Use config values to customize commands, file paths, and workflows
1652
- - Example: If config says \`testing.framework: "pytest"\`, use \`pytest\` instead of generic test commands
1653
-
1654
- ## Template Variable Replacement
1655
-
1656
- When you see variables like \`{{config.project.name}}\` in workflows or templates:
1657
-
1658
- 1. **Extract the path**: \`{{config.project.name}}\` → path is \`project.name\`
1659
- 2. **Read your local config**: \`.fraim/config.json\`
1660
- 3. **Navigate to the value**: \`config.project.name\` → \`"My Project"\`
1661
- 4. **Replace**: \`{{config.project.name}}\` → \`"My Project"\`
1662
-
1663
- ### Example
1664
-
1665
- **Template:**
1666
- \`\`\`
1667
- Project: {{config.project.name}}
1668
- Language: {{config.project.primaryLanguage}}
1669
- Test Framework: {{config.testing.framework}}
1670
- \`\`\`
1671
-
1672
- **Your .fraim/config.json:**
1673
- \`\`\`json
1674
- {
1675
- "project": {
1676
- "name": "My Awesome App",
1677
- "primaryLanguage": "python"
1678
- },
1679
- "testing": {
1680
- "framework": "pytest"
1681
- }
1682
- }
1683
- \`\`\`
1684
-
1685
- **Result:**
1686
- \`\`\`
1687
- Project: My Awesome App
1688
- Language: python
1689
- Test Framework: pytest
1690
- \`\`\`
1691
-
1692
- ## Local Customizations
1693
-
1694
- Your project may have customizations in:
1695
-
1696
- - **\`.fraim/rules/\`** - Custom rules that override generic ones
1697
- - **\`.fraim/workflows/\`** - Custom workflows that override generic ones
1698
- - **\`.fraim/templates/\`** - Custom templates that override generic ones
1699
- - **\`.fraim/scripts/\`** - Custom scripts that override generic ones
1700
-
1701
- **Priority**: Local customizations always take precedence over generic files from the cloud.
1702
-
1703
- ## Common Config Paths
1704
-
1705
- - \`config.project.name\` - Project name
1706
- - \`config.project.primaryLanguage\` - Main programming language
1707
- - \`config.project.type\` - Project type (saas-product, library, etc.)
1708
- - \`config.testing.framework\` - Test framework (pytest, jest, etc.)
1709
- - \`config.testing.testLocation\` - Where tests are located
1710
- - \`config.testing.testNaming\` - Test file naming pattern
1711
- - \`config.git.defaultBranch\` - Default git branch
1712
- - \`config.git.branchNaming\` - Branch naming pattern
1713
-
1714
- ## Workflow Execution
1715
-
1716
- When executing workflows:
1717
-
1718
- 1. **Read local config first** - Understand your project's settings
1719
- 2. **Replace template variables** - Use config values in commands and file paths
1720
- 3. **Check for local customizations** - Use local overrides if they exist
1721
- 4. **Execute in workspace** - All operations happen in your project directory
1722
-
1723
- ## If Config Doesn't Exist
1724
-
1725
- If \`.fraim/config.json\` doesn't exist:
1726
-
1727
- 1. Use \`fraim_setup\` tool to create it interactively
1728
- 2. Or use default values (see FRAIM defaults)
1729
- 3. Or create it manually with project-specific settings
1730
-
1731
- ## Next Steps
1732
-
1733
- 1. Read \`.fraim/config.json\` from your workspace
1734
- 2. Use config values when you see \`{{config.*}}\` variables
1735
- 3. Check for local customizations in \`.fraim/\` directories
1736
- 4. Execute workflows using your project's specific settings`;
1737
- return {
1738
- content: [
1739
- {
1740
- type: 'text',
1741
- text: instructions
1742
- }
1743
- ]
1744
- };
1745
- }
1746
- async handleFraimConnect(args, apiKey, userId) {
1747
- if (!apiKey) {
1748
- throw new Error('No API Key found in context for fraim_connect');
1749
- }
1750
- // Validate required agent information
1751
- if (!args.agent?.name || !args.agent?.model) {
1752
- throw new Error('Agent information is required. Please provide agent.name and agent.model in your fraim_connect call.');
1753
- }
1754
- // Validate agent information to prevent BS
1755
- const validAgentNames = ['Claude', 'Cursor', 'Kiro', 'Windsurf', 'Antigravity', 'ChatGPT', 'Copilot'];
1756
- if (!validAgentNames.some(valid => args.agent.name.toLowerCase().includes(valid.toLowerCase()))) {
1757
- throw new Error(`Invalid agent name "${args.agent.name}". Please use one of: ${validAgentNames.join(', ')}`);
1758
- }
1759
- // Validate machine information
1760
- if (!args.machine?.hostname || !args.machine?.platform) {
1761
- throw new Error('Machine information is required. Please provide machine.hostname and machine.platform. Use os.hostname() and process.platform.');
1762
- }
1763
- // Validate repo information - check if .fraim/config.json exists and use it
1764
- let repoInfo = args.repo || {};
1765
- // Note: MCP server cannot access client-side .fraim/config.json
1766
- // Repository info must be provided by the client in the fraim_connect call
1767
- if (!repoInfo.url) {
1768
- throw new Error('Repository information is required. Please provide repo.url in the fraim_connect call.');
1769
- }
1770
- // 1. Generate Session ID
1771
- const sessionId = (0, crypto_1.randomUUID)();
1772
- // userId passed from context
1773
- const finalUserId = userId || 'unknown';
1774
- // 2. Create Session in DB with enhanced information
1775
- const session = {
1776
- sessionId,
1777
- userId: finalUserId,
1778
- agent: {
1779
- name: args.agent.name,
1780
- model: args.agent.model,
1781
- version: args.agent.version
1782
- },
1783
- machine: args.machine,
1784
- repo: repoInfo,
1785
- startTime: new Date(),
1786
- lastActive: new Date()
1787
- };
1788
- const keyData = await this.dbService.verifyApiKey(apiKey);
1789
- if (keyData)
1790
- session.userId = keyData.userId;
1791
- await this.dbService.createSession(session);
1792
- // 3. Register in SessionManager
1793
- this.sessionManager.registerSession(apiKey, sessionId);
1794
- // 4. Generate informative response
1795
- const agentInfo = `${args.agent.name}${args.agent.model ? ` (${args.agent.model})` : ''}${args.agent.version ? ` v${args.agent.version}` : ''}`;
1796
- const machineInfo = `${args.machine.hostname || 'unknown'} (${args.machine.platform || 'unknown'})`;
1797
- const repoInfoDisplay = repoInfo.name || repoInfo.url || 'unknown';
1798
- return {
1799
- content: [{
1800
- type: 'text',
1801
- text: `✅ **FRAIM Session Connected!**
1802
-
1803
- **Session ID**: ${sessionId}
1804
- **Agent**: ${agentInfo}
1805
- **Machine**: ${machineInfo}
1806
- **Repository**: ${repoInfoDisplay}
1807
-
1808
- 🔄 Telemetry active. All FRAIM tools are now unlocked.
1809
-
1810
- **Next Steps**:
1811
- - Use \`get_fraim_init\` to see available workflows
1812
- - Use \`list_fraim_workflows\` to browse by category
1813
- - Use \`get_fraim_workflow({ workflow: "name" })\` to start working`
1814
- }],
1815
- sessionId: sessionId
1816
- };
1817
- }
1818
- async handleSeekCoachingOnNextStep(args) {
1819
- console.log('🔧 MCP Server: handleSeekCoachingOnNextStep called with args:', JSON.stringify(args, null, 2));
1820
- try {
1821
- const response = await this.aiCoach.handleCoachingRequest({
1822
- workflowType: args.workflowType,
1823
- issueNumber: args.issueNumber,
1824
- currentPhase: args.currentPhase,
1825
- status: args.status,
1826
- evidence: args.evidence,
1827
- findings: args.findings
1828
- });
1829
- console.log('✅ MCP Server: AI Coach returned response, length:', response.length);
1830
- return {
1831
- content: [{
1832
- type: 'text',
1833
- text: response
1834
- }]
1835
- };
1836
- }
1837
- catch (error) {
1838
- console.error('❌ AI Coach guidance failed:', error);
1839
- return {
1840
- content: [{
1841
- type: 'text',
1842
- text: `# ❌ AI Coach Guidance Failed\n\n**Error**: ${error instanceof Error ? error.message : 'Unknown error'}\n\nPlease check your request parameters and try again.`
1843
- }],
1844
- isError: true,
1845
- error: error instanceof Error ? error.message : 'Unknown error'
1846
- };
1847
- }
1848
- }
1849
- async start(port = 3002) {
1850
- try {
1851
- // Connect to database before starting server
1852
- await this.dbService.connect();
1853
- this.app.listen(port, () => {
1854
- console.log(`🚀 FRAIM MCP Server running on port ${port}`);
1855
- console.log(`📡 MCP endpoint: http://localhost:${port}/mcp`);
1856
- console.log(`🏥 Health check: http://localhost:${port}/health`);
1857
- console.log(`📚 Serving ${this.fileIndex.size} files from registry`);
1858
- console.log(` Path: ${this.registryPath}`);
1859
- });
1860
- }
1861
- catch (error) {
1862
- console.error('❌ Failed to connect to database during startup:', error);
1863
- throw error;
1864
- }
1865
- }
1866
- detectProviderFromUrl(url) {
1867
- if (!url)
1868
- return 'github';
1869
- if (url.includes('dev.azure.com') || url.includes('visualstudio.com')) {
1870
- return 'ado';
1871
- }
1872
- return 'github';
1873
- }
1874
- }
1875
- exports.FraimMCPServer = FraimMCPServer;
1876
- // Start the server if this file is run directly
1877
- // In Azure App Service, the port is provided via process.env.PORT
1878
- const serverPort = (0, git_utils_1.getPort)();
1879
- const defaultFraimPort = process.env.FRAIM_MCP_PORT ? parseInt(process.env.FRAIM_MCP_PORT) : serverPort + 2;
1880
- const port = process.env.PORT ? parseInt(process.env.PORT) : defaultFraimPort;
1881
- const server = new FraimMCPServer();
1882
- server.start(port).catch((error) => {
1883
- console.error('Failed to start FRAIM MCP Server:', error);
1884
- process.exit(1);
1885
- });