mstro-app 0.3.1 → 0.3.5

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 (36) hide show
  1. package/README.md +3 -19
  2. package/bin/mstro.js +15 -177
  3. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  4. package/dist/server/mcp/bouncer-integration.js +43 -135
  5. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  6. package/dist/server/services/platform.d.ts.map +1 -1
  7. package/dist/server/services/platform.js +2 -13
  8. package/dist/server/services/platform.js.map +1 -1
  9. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  10. package/dist/server/services/websocket/file-explorer-handlers.js +17 -1
  11. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  12. package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
  13. package/dist/server/services/websocket/file-utils.js +26 -20
  14. package/dist/server/services/websocket/file-utils.js.map +1 -1
  15. package/dist/server/services/websocket/types.d.ts +1 -1
  16. package/dist/server/services/websocket/types.d.ts.map +1 -1
  17. package/dist/server/utils/paths.d.ts +0 -12
  18. package/dist/server/utils/paths.d.ts.map +1 -1
  19. package/dist/server/utils/paths.js +0 -12
  20. package/dist/server/utils/paths.js.map +1 -1
  21. package/package.json +1 -2
  22. package/server/README.md +0 -1
  23. package/server/mcp/README.md +0 -5
  24. package/server/mcp/bouncer-integration.ts +55 -210
  25. package/server/services/platform.ts +2 -12
  26. package/server/services/websocket/file-explorer-handlers.ts +16 -1
  27. package/server/services/websocket/file-utils.ts +29 -24
  28. package/server/services/websocket/types.ts +1 -0
  29. package/server/utils/paths.ts +0 -14
  30. package/bin/configure-claude.js +0 -298
  31. package/dist/server/mcp/bouncer-cli.d.ts +0 -3
  32. package/dist/server/mcp/bouncer-cli.d.ts.map +0 -1
  33. package/dist/server/mcp/bouncer-cli.js +0 -138
  34. package/dist/server/mcp/bouncer-cli.js.map +0 -1
  35. package/hooks/bouncer.sh +0 -159
  36. package/server/mcp/bouncer-cli.ts +0 -180
@@ -271,18 +271,50 @@ or
271
271
  });
272
272
  }
273
273
 
274
+ /**
275
+ * Finalize a bouncer decision: log, track analytics, cache, and return.
276
+ */
277
+ function finalizeDecision(
278
+ operation: string,
279
+ decision: BouncerDecision,
280
+ layer: string,
281
+ startTime: number,
282
+ context: BouncerReviewRequest['context'],
283
+ logFn: typeof import('./security-audit.js')['logBouncerDecision'],
284
+ opts?: { error?: string; skipCache?: boolean; skipAnalytics?: boolean },
285
+ ): BouncerDecision {
286
+ const latencyMs = Math.round(performance.now() - startTime);
287
+
288
+ logFn(operation, decision.decision, decision.confidence, decision.reasoning, {
289
+ context, threatLevel: decision.threatLevel, layer, latencyMs, ...(opts?.error && { error: opts.error }),
290
+ });
291
+
292
+ if (!opts?.skipAnalytics) {
293
+ const event = decision.decision === 'deny' ? AnalyticsEvents.BOUNCER_TOOL_DENIED : AnalyticsEvents.BOUNCER_TOOL_ALLOWED;
294
+ trackEvent(event, {
295
+ layer,
296
+ operation_length: operation.length,
297
+ threat_level: decision.threatLevel,
298
+ confidence: decision.confidence,
299
+ latency_ms: latencyMs,
300
+ });
301
+ }
302
+
303
+ if (!opts?.skipCache) cacheDecision(operation, decision);
304
+ return decision;
305
+ }
306
+
274
307
  /**
275
308
  * Main bouncer review function - 2-layer hybrid system
276
309
  */
277
310
  export async function reviewOperation(request: BouncerReviewRequest): Promise<BouncerDecision> {
278
- // Import audit logger
279
311
  const { logBouncerDecision } = await import('./security-audit.js');
280
-
281
312
  const startTime = performance.now();
282
-
283
313
  const { operation } = request;
314
+ const fin = (d: BouncerDecision, layer: string, opts?: Parameters<typeof finalizeDecision>[6]) =>
315
+ finalizeDecision(operation, d, layer, startTime, request.context, logBouncerDecision, opts);
284
316
 
285
- // Check cache first (pattern-layer decisions and prior Haiku results)
317
+ // Check cache first
286
318
  const cached = getCachedDecision(operation);
287
319
  if (cached) {
288
320
  console.error(`[Bouncer] ⚡ Cache hit: ${cached.decision} (${cached.confidence}%)`);
@@ -295,257 +327,70 @@ export async function reviewOperation(request: BouncerReviewRequest): Promise<Bo
295
327
  console.error(`[Bouncer] User request: ${request.context.userRequest}`);
296
328
  }
297
329
 
298
- // ========================================
299
- // PRE-CHECK: Malformed/empty tool calls
300
- // ========================================
301
- // Empty-param Edit/Write calls are no-ops that will fail validation anyway.
302
- // Allow immediately instead of wasting ~8s on Haiku analysis.
330
+ // PRE-CHECK: Empty-param Edit/Write calls are no-ops — allow immediately
303
331
  const toolInput = request.context?.toolInput;
304
332
  if (toolInput && typeof toolInput === 'object' && Object.keys(toolInput).length === 0) {
305
333
  console.error('[Bouncer] ⚡ Fast path: Empty tool parameters (no-op)');
306
- const latencyMs = Math.round(performance.now() - startTime);
307
-
308
- const decision: BouncerDecision = {
309
- decision: 'allow',
310
- confidence: 95,
311
- reasoning: 'Empty tool parameters - operation is a no-op with no side effects.',
312
- threatLevel: 'low'
313
- };
314
-
315
- logBouncerDecision(
316
- operation,
317
- decision.decision,
318
- decision.confidence,
319
- decision.reasoning,
320
- { context: request.context, threatLevel: decision.threatLevel, layer: 'pattern-noop', latencyMs }
321
- );
322
-
323
- cacheDecision(operation, decision);
324
- return decision;
334
+ return fin({ decision: 'allow', confidence: 95, reasoning: 'Empty tool parameters - operation is a no-op with no side effects.', threatLevel: 'low' }, 'pattern-noop', { skipAnalytics: true });
325
335
  }
326
336
 
327
- // ========================================
328
337
  // LAYER 1: Pattern-Based Fast Path (< 5ms)
329
- // ========================================
330
338
 
331
- // Check safe operations FIRST - allows trusted sources (e.g., brew, rustup)
339
+ // Check safe operations FIRST allows trusted sources (e.g., brew, rustup)
332
340
  // to pass before hitting critical threat patterns like curl|bash
333
341
  const safeOperation = matchesPattern(operation, SAFE_OPERATIONS);
334
342
  if (safeOperation) {
335
343
  console.error('[Bouncer] ⚡ Fast path: Safe operation approved');
336
- const latencyMs = Math.round(performance.now() - startTime);
337
-
338
- const decision: BouncerDecision = {
339
- decision: 'allow',
340
- confidence: 95,
341
- reasoning: 'Operation matches known-safe patterns. No security concerns detected.',
342
- threatLevel: 'low'
343
- };
344
-
345
- logBouncerDecision(
346
- operation,
347
- decision.decision,
348
- decision.confidence,
349
- decision.reasoning,
350
- { context: request.context, threatLevel: decision.threatLevel, layer: 'pattern-safe', latencyMs }
351
- );
352
- trackEvent(AnalyticsEvents.BOUNCER_TOOL_ALLOWED, {
353
- layer: 'pattern-safe',
354
- operation_length: operation.length,
355
- threat_level: 'low',
356
- confidence: 95,
357
- latency_ms: latencyMs,
358
- });
359
-
360
- cacheDecision(operation, decision);
361
- return decision;
344
+ return fin({ decision: 'allow', confidence: 95, reasoning: 'Operation matches known-safe patterns. No security concerns detected.', threatLevel: 'low' }, 'pattern-safe');
362
345
  }
363
346
 
364
- // Check critical threats (catastrophic operations like rm -rf /, fork bombs)
365
- // These are ALWAYS denied - no context can justify them
347
+ // Critical threats (rm -rf /, fork bombs) — ALWAYS denied
366
348
  const criticalThreat = matchesPattern(operation, CRITICAL_THREATS);
367
349
  if (criticalThreat) {
368
350
  console.error('[Bouncer] ⚡ Fast path: CRITICAL THREAT detected');
369
- const latencyMs = Math.round(performance.now() - startTime);
370
-
371
- const decision: BouncerDecision = {
372
- decision: 'deny',
373
- confidence: 99,
374
- reasoning: `🚨 CRITICAL THREAT: ${criticalThreat.reason}`,
375
- threatLevel: 'critical',
351
+ return fin({
352
+ decision: 'deny', confidence: 99, reasoning: `🚨 CRITICAL THREAT: ${criticalThreat.reason}`, threatLevel: 'critical',
376
353
  alternative: 'This operation should never be performed. If you need to accomplish a specific task, please describe your goal and I can suggest safe alternatives.',
377
- enforceable: true
378
- };
379
-
380
- logBouncerDecision(
381
- operation,
382
- decision.decision,
383
- decision.confidence,
384
- decision.reasoning,
385
- { context: request.context, threatLevel: decision.threatLevel, layer: 'pattern-critical', latencyMs }
386
- );
387
- trackEvent(AnalyticsEvents.BOUNCER_TOOL_DENIED, {
388
- layer: 'pattern-critical',
389
- operation_length: operation.length,
390
- threat_level: 'critical',
391
- confidence: 99,
392
- latency_ms: latencyMs,
393
- });
394
-
395
- cacheDecision(operation, decision);
396
- return decision;
354
+ enforceable: true,
355
+ }, 'pattern-critical');
397
356
  }
398
357
 
399
- // ========================================
400
358
  // LAYER 2: Haiku AI Analysis (~200-500ms)
401
- // ========================================
402
359
 
403
- // Only invoke AI for operations that truly need context
360
+ // Default allow for operations that don't need AI review
404
361
  if (!requiresAIReview(operation)) {
405
- // Default allow for operations that don't match any pattern
406
362
  console.error('[Bouncer] ⚡ Fast path: No concerning patterns, allowing');
407
- const latencyMs = Math.round(performance.now() - startTime);
408
-
409
- const decision: BouncerDecision = {
410
- decision: 'allow',
411
- confidence: 80,
412
- reasoning: 'Operation appears safe based on pattern analysis. No obvious threats detected.',
413
- threatLevel: 'low'
414
- };
415
-
416
- logBouncerDecision(
417
- operation,
418
- decision.decision,
419
- decision.confidence,
420
- decision.reasoning,
421
- { context: request.context, threatLevel: decision.threatLevel, layer: 'pattern-default', latencyMs }
422
- );
423
- trackEvent(AnalyticsEvents.BOUNCER_TOOL_ALLOWED, {
424
- layer: 'pattern-default',
425
- operation_length: operation.length,
426
- threat_level: 'low',
427
- confidence: 80,
428
- latency_ms: latencyMs,
429
- });
430
-
431
- cacheDecision(operation, decision);
432
- return decision;
363
+ return fin({ decision: 'allow', confidence: 80, reasoning: 'Operation appears safe based on pattern analysis. No obvious threats detected.', threatLevel: 'low' }, 'pattern-default');
433
364
  }
434
365
 
435
- // Check if AI analysis is enabled
436
- const useAI = process.env.BOUNCER_USE_AI !== 'false';
437
-
438
- if (!useAI) {
366
+ if (process.env.BOUNCER_USE_AI === 'false') {
439
367
  console.error('[Bouncer] AI analysis disabled (BOUNCER_USE_AI=false)');
440
- const latencyMs = Math.round(performance.now() - startTime);
441
-
442
- const decision: BouncerDecision = {
443
- decision: 'warn_allow',
444
- confidence: 60,
445
- reasoning: 'Operation requires review but AI analysis is disabled. Proceeding with caution.',
446
- threatLevel: 'medium'
447
- };
448
-
449
- logBouncerDecision(
450
- operation,
451
- decision.decision,
452
- decision.confidence,
453
- decision.reasoning,
454
- { context: request.context, threatLevel: decision.threatLevel, layer: 'ai-disabled', latencyMs }
455
- );
456
-
457
- return decision;
368
+ return fin({ decision: 'warn_allow', confidence: 60, reasoning: 'Operation requires review but AI analysis is disabled. Proceeding with caution.', threatLevel: 'medium' }, 'ai-disabled', { skipCache: true, skipAnalytics: true });
458
369
  }
459
370
 
460
371
  console.error('[Bouncer] 🤖 Invoking Haiku for AI analysis...');
461
- trackEvent(AnalyticsEvents.BOUNCER_HAIKU_REVIEW, {
462
- operation_length: operation.length,
463
- });
372
+ trackEvent(AnalyticsEvents.BOUNCER_HAIKU_REVIEW, { operation_length: operation.length });
464
373
 
465
- // Get Claude command and working directory from context or use defaults
466
374
  const claudeCommand = process.env.CLAUDE_COMMAND || 'claude';
467
375
  const workingDir = request.context?.workingDirectory || process.cwd();
468
376
 
469
377
  try {
470
378
  const decision = await analyzeWithHaiku(request, claudeCommand, workingDir);
471
- const latencyMs = Math.round(performance.now() - startTime);
472
- console.error(`[Bouncer] ✓ Haiku decision: ${decision.decision} (${decision.confidence}% confidence) [${latencyMs}ms]`);
379
+ console.error(`[Bouncer] Haiku decision: ${decision.decision} (${decision.confidence}% confidence) [${Math.round(performance.now() - startTime)}ms]`);
473
380
  console.error(`[Bouncer] Reasoning: ${decision.reasoning}`);
474
-
475
- logBouncerDecision(
476
- operation,
477
- decision.decision,
478
- decision.confidence,
479
- decision.reasoning,
480
- { context: request.context, threatLevel: decision.threatLevel, layer: 'haiku-ai', latencyMs }
481
- );
482
- trackEvent(decision.decision === 'deny' ? AnalyticsEvents.BOUNCER_TOOL_DENIED : AnalyticsEvents.BOUNCER_TOOL_ALLOWED, {
483
- layer: 'haiku-ai',
484
- operation_length: operation.length,
485
- threat_level: decision.threatLevel,
486
- confidence: decision.confidence,
487
- latency_ms: latencyMs,
488
- });
489
-
490
- cacheDecision(operation, decision);
491
- return decision;
492
-
381
+ return fin(decision, 'haiku-ai');
493
382
  } catch (error: unknown) {
494
- const latencyMs = Math.round(performance.now() - startTime);
495
383
  const errorMessage = error instanceof Error ? error.message : String(error);
496
- const isTimeout = errorMessage.includes('timed out');
497
384
 
498
- if (isTimeout) {
499
- // Timeout: default to ALLOW — prefer availability over security stall,
500
- // since the user drove the interaction
385
+ if (errorMessage.includes('timed out')) {
501
386
  console.error(`[Bouncer] ⚠️ Haiku analysis timed out after ${HAIKU_TIMEOUT_MS}ms — defaulting to ALLOW`);
502
387
  captureException(error, { context: 'bouncer.haiku_timeout', operation });
503
-
504
- const decision: BouncerDecision = {
505
- decision: 'allow',
506
- confidence: 50,
507
- reasoning: `Security analysis timed out after ${HAIKU_TIMEOUT_MS}ms. Defaulting to allow — user initiated the action.`,
508
- threatLevel: 'medium'
509
- };
510
-
511
- logBouncerDecision(
512
- operation,
513
- decision.decision,
514
- decision.confidence,
515
- decision.reasoning,
516
- { context: request.context, threatLevel: decision.threatLevel, layer: 'haiku-timeout', latencyMs, error: errorMessage }
517
- );
518
- trackEvent(AnalyticsEvents.BOUNCER_TOOL_ALLOWED, {
519
- layer: 'haiku-timeout',
520
- operation_length: operation.length,
521
- threat_level: 'medium',
522
- confidence: 50,
523
- latency_ms: latencyMs,
524
- });
525
-
526
- return decision;
388
+ return fin({ decision: 'allow', confidence: 50, reasoning: `Security analysis timed out after ${HAIKU_TIMEOUT_MS}ms. Defaulting to allow — user initiated the action.`, threatLevel: 'medium' }, 'haiku-timeout', { skipCache: true });
527
389
  }
528
390
 
529
391
  console.error(`[Bouncer] ⚠️ Haiku analysis failed: ${errorMessage}`);
530
392
  captureException(error, { context: 'bouncer.haiku_analysis', operation });
531
-
532
- // Fail-safe: deny on non-timeout AI failure
533
- const decision: BouncerDecision = {
534
- decision: 'deny',
535
- confidence: 0,
536
- reasoning: `Security analysis failed: ${errorMessage}. Denying for safety.`,
537
- threatLevel: 'critical'
538
- };
539
-
540
- logBouncerDecision(
541
- operation,
542
- decision.decision,
543
- decision.confidence,
544
- decision.reasoning,
545
- { context: request.context, threatLevel: decision.threatLevel, layer: 'ai-error', latencyMs, error: errorMessage }
546
- );
547
-
548
- return decision;
393
+ return fin({ decision: 'deny', confidence: 0, reasoning: `Security analysis failed: ${errorMessage}. Denying for safety.`, threatLevel: 'critical' }, 'ai-error', { skipCache: true, skipAnalytics: true, error: errorMessage });
549
394
  }
550
395
  }
551
396
 
@@ -102,18 +102,8 @@ if (typeof WebSocket !== 'undefined') {
102
102
  WebSocketImpl = WS as unknown as typeof WebSocket
103
103
  }
104
104
 
105
- // Read SERVER_URL from ~/.mstro/.env if it exists (for local dev)
106
- function getServerUrl(): string {
107
- try {
108
- const envPath = join(MSTRO_DIR, '.env')
109
- const content = readFileSync(envPath, 'utf-8')
110
- const match = content.match(/^SERVER_URL=(.+)$/m)
111
- if (match) return match[1].trim()
112
- } catch {}
113
- return 'https://api.mstro.app'
114
- }
115
-
116
- const DEFAULT_PLATFORM_URL = process.env.PLATFORM_URL || getServerUrl()
105
+ // PLATFORM_URL is set via --server / --dev flag in mstro.js
106
+ const DEFAULT_PLATFORM_URL = process.env.PLATFORM_URL || 'https://api.mstro.app'
117
107
 
118
108
  interface ConnectionCallbacks {
119
109
  onConnected?: (connectionId: string) => void
@@ -74,7 +74,22 @@ export function handleFileExplorerMessage(ctx: HandlerContext, ws: WSContext, ms
74
74
  cancelSearch: () => handleCancelSearch(ctx, tabId),
75
75
  findDefinition: () => handleFindDefinition(ctx, ws, msg, tabId, workingDir),
76
76
  };
77
- handlers[msg.type]?.();
77
+ const handler = handlers[msg.type];
78
+ if (!handler) return;
79
+
80
+ try {
81
+ handler();
82
+ } catch (error: unknown) {
83
+ // Send a domain-specific fileError so the web client can resolve pending
84
+ // promises instead of letting the generic handler send { type: 'error' }
85
+ // which no file-explorer listener handles (causing orphaned promises).
86
+ const errorMessage = error instanceof Error ? error.message : String(error);
87
+ ctx.send(ws, {
88
+ type: 'fileError',
89
+ tabId,
90
+ data: { operation: msg.type, path: msg.data?.dirPath || msg.data?.filePath || '', error: errorMessage },
91
+ });
92
+ }
78
93
  }
79
94
 
80
95
  function handleListDirectory(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void {
@@ -7,7 +7,7 @@
7
7
  * File type detection, gitignore parsing, and directory scanning utilities.
8
8
  */
9
9
 
10
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
10
+ import { existsSync, readdirSync, readFileSync, type Stats, statSync } from 'node:fs';
11
11
  import { extname, join, relative, sep } from 'node:path';
12
12
  import type { CacheEntry, } from './types.js';
13
13
 
@@ -285,6 +285,31 @@ function readTextContent(fullPath: string, filePath: string, fileName: string, s
285
285
  };
286
286
  }
287
287
 
288
+ function validateFileAccess(fullPath: string, filePath: string, fileName: string, workingDir: string): FileContentResult | null {
289
+ const normalizedPath = join(fullPath);
290
+ if (!normalizedPath.startsWith(join(workingDir)) && !isPathInSafeLocation(normalizedPath)) {
291
+ return { path: filePath, fileName, content: '', error: 'Access denied: path outside allowed locations' };
292
+ }
293
+ if (!existsSync(fullPath)) {
294
+ return { path: filePath, fileName, content: '', error: 'File not found' };
295
+ }
296
+ return null;
297
+ }
298
+
299
+ function readValidatedFile(fullPath: string, filePath: string, fileName: string, stats: Stats): FileContentResult {
300
+ if (stats.isDirectory()) return readDirectoryContent(fullPath, filePath, fileName);
301
+
302
+ const isBin = isBinaryFile(fullPath);
303
+ const MAX_FILE_SIZE = isBin ? 10 * 1024 * 1024 : 1024 * 1024;
304
+ if (stats.size > MAX_FILE_SIZE) {
305
+ return { path: filePath, fileName, content: '', size: stats.size, error: `File too large (${Math.round(stats.size / 1024)}KB). Maximum is ${isBin ? '10MB' : '1MB'}.` };
306
+ }
307
+
308
+ return isBin
309
+ ? readBinaryContent(fullPath, filePath, fileName, stats)
310
+ : readTextContent(fullPath, filePath, fileName, stats);
311
+ }
312
+
288
313
  /**
289
314
  * Read file content for context injection
290
315
  */
@@ -293,30 +318,10 @@ export function readFileContent(filePath: string, workingDir: string): FileConte
293
318
  const fullPath = filePath.startsWith('/') ? filePath : join(workingDir, filePath);
294
319
  const fileName = fullPath.split(sep).pop() || filePath;
295
320
 
296
- const normalizedPath = join(fullPath);
297
- const isInWorkingDir = normalizedPath.startsWith(join(workingDir));
298
- if (!isInWorkingDir && !isPathInSafeLocation(normalizedPath)) {
299
- return { path: filePath, fileName, content: '', error: 'Access denied: path outside allowed locations' };
300
- }
301
-
302
- if (!existsSync(fullPath)) {
303
- return { path: filePath, fileName, content: '', error: 'File not found' };
304
- }
305
-
306
- const stats = statSync(fullPath);
307
- if (stats.isDirectory()) {
308
- return readDirectoryContent(fullPath, filePath, fileName);
309
- }
310
-
311
- const isBin = isBinaryFile(fullPath);
312
- const MAX_FILE_SIZE = isBin ? 10 * 1024 * 1024 : 1024 * 1024;
313
- if (stats.size > MAX_FILE_SIZE) {
314
- return { path: filePath, fileName, content: '', size: stats.size, error: `File too large (${Math.round(stats.size / 1024)}KB). Maximum is ${isBin ? '10MB' : '1MB'}.` };
315
- }
321
+ const accessError = validateFileAccess(fullPath, filePath, fileName, workingDir);
322
+ if (accessError) return accessError;
316
323
 
317
- return isBin
318
- ? readBinaryContent(fullPath, filePath, fileName, stats)
319
- : readTextContent(fullPath, filePath, fileName, stats);
324
+ return readValidatedFile(fullPath, filePath, fileName, statSync(fullPath));
320
325
  } catch (error: unknown) {
321
326
  console.error('[FileUtils] Error reading file:', error);
322
327
  return { path: filePath, fileName: filePath.split(sep).pop() || filePath, content: '', error: (error instanceof Error ? error.message : String(error)) || 'Failed to read file' };
@@ -157,6 +157,7 @@ export interface WebSocketResponse {
157
157
  | 'contentSearchComplete'
158
158
  | 'contentSearchError'
159
159
  | 'definitionResult'
160
+ | 'fileError'
160
161
  // Terminal sync response types
161
162
  | 'terminalCreated'
162
163
  | 'terminalClosed'
@@ -29,17 +29,3 @@ export const MSTRO_ROOT = resolve(__dirname, '../..');
29
29
  */
30
30
  export const MCP_SERVER_PATH = resolve(MSTRO_ROOT, 'server/mcp/server.ts');
31
31
 
32
- /**
33
- * Path to the MCP bouncer configuration template
34
- */
35
- export const MCP_CONFIG_TEMPLATE_PATH = resolve(MSTRO_ROOT, 'mstro-bouncer-mcp.json');
36
-
37
- /**
38
- * Path to the hooks directory
39
- */
40
- export const HOOKS_DIR = resolve(MSTRO_ROOT, 'hooks');
41
-
42
- /**
43
- * Path to the bouncer hook script
44
- */
45
- export const BOUNCER_HOOK = resolve(HOOKS_DIR, 'bouncer.sh');