driftdetect-mcp 0.3.0 โ†’ 0.4.2

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 (140) hide show
  1. package/dist/bin/server.d.ts +12 -2
  2. package/dist/bin/server.d.ts.map +1 -1
  3. package/dist/bin/server.js +25 -5
  4. package/dist/bin/server.js.map +1 -1
  5. package/dist/enterprise-server.d.ts +78 -0
  6. package/dist/enterprise-server.d.ts.map +1 -0
  7. package/dist/enterprise-server.js +201 -0
  8. package/dist/enterprise-server.js.map +1 -0
  9. package/dist/index.d.ts +15 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +17 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/infrastructure/cache.d.ts +86 -0
  14. package/dist/infrastructure/cache.d.ts.map +1 -0
  15. package/dist/infrastructure/cache.js +271 -0
  16. package/dist/infrastructure/cache.js.map +1 -0
  17. package/dist/infrastructure/cursor-manager.d.ts +86 -0
  18. package/dist/infrastructure/cursor-manager.d.ts.map +1 -0
  19. package/dist/infrastructure/cursor-manager.js +175 -0
  20. package/dist/infrastructure/cursor-manager.js.map +1 -0
  21. package/dist/infrastructure/error-handler.d.ts +82 -0
  22. package/dist/infrastructure/error-handler.d.ts.map +1 -0
  23. package/dist/infrastructure/error-handler.js +226 -0
  24. package/dist/infrastructure/error-handler.js.map +1 -0
  25. package/dist/infrastructure/index.d.ts +19 -0
  26. package/dist/infrastructure/index.d.ts.map +1 -0
  27. package/dist/infrastructure/index.js +26 -0
  28. package/dist/infrastructure/index.js.map +1 -0
  29. package/dist/infrastructure/metrics.d.ts +104 -0
  30. package/dist/infrastructure/metrics.d.ts.map +1 -0
  31. package/dist/infrastructure/metrics.js +291 -0
  32. package/dist/infrastructure/metrics.js.map +1 -0
  33. package/dist/infrastructure/rate-limiter.d.ts +59 -0
  34. package/dist/infrastructure/rate-limiter.d.ts.map +1 -0
  35. package/dist/infrastructure/rate-limiter.js +132 -0
  36. package/dist/infrastructure/rate-limiter.js.map +1 -0
  37. package/dist/infrastructure/response-builder.d.ts +104 -0
  38. package/dist/infrastructure/response-builder.d.ts.map +1 -0
  39. package/dist/infrastructure/response-builder.js +207 -0
  40. package/dist/infrastructure/response-builder.js.map +1 -0
  41. package/dist/infrastructure/token-estimator.d.ts +48 -0
  42. package/dist/infrastructure/token-estimator.d.ts.map +1 -0
  43. package/dist/infrastructure/token-estimator.js +131 -0
  44. package/dist/infrastructure/token-estimator.js.map +1 -0
  45. package/dist/server.d.ts.map +1 -1
  46. package/dist/server.js +1136 -18
  47. package/dist/server.js.map +1 -1
  48. package/dist/tools/detail/code-examples.d.ts +33 -0
  49. package/dist/tools/detail/code-examples.d.ts.map +1 -0
  50. package/dist/tools/detail/code-examples.js +126 -0
  51. package/dist/tools/detail/code-examples.js.map +1 -0
  52. package/dist/tools/detail/dna-check.d.ts +32 -0
  53. package/dist/tools/detail/dna-check.d.ts.map +1 -0
  54. package/dist/tools/detail/dna-check.js +231 -0
  55. package/dist/tools/detail/dna-check.js.map +1 -0
  56. package/dist/tools/detail/dna-profile.d.ts +37 -0
  57. package/dist/tools/detail/dna-profile.d.ts.map +1 -0
  58. package/dist/tools/detail/dna-profile.js +101 -0
  59. package/dist/tools/detail/dna-profile.js.map +1 -0
  60. package/dist/tools/detail/file-patterns.d.ts +39 -0
  61. package/dist/tools/detail/file-patterns.d.ts.map +1 -0
  62. package/dist/tools/detail/file-patterns.js +103 -0
  63. package/dist/tools/detail/file-patterns.js.map +1 -0
  64. package/dist/tools/detail/files-list.d.ts +30 -0
  65. package/dist/tools/detail/files-list.d.ts.map +1 -0
  66. package/dist/tools/detail/files-list.js +99 -0
  67. package/dist/tools/detail/files-list.js.map +1 -0
  68. package/dist/tools/detail/impact-analysis.d.ts +53 -0
  69. package/dist/tools/detail/impact-analysis.d.ts.map +1 -0
  70. package/dist/tools/detail/impact-analysis.js +130 -0
  71. package/dist/tools/detail/impact-analysis.js.map +1 -0
  72. package/dist/tools/detail/index.d.ts +23 -0
  73. package/dist/tools/detail/index.d.ts.map +1 -0
  74. package/dist/tools/detail/index.js +200 -0
  75. package/dist/tools/detail/index.js.map +1 -0
  76. package/dist/tools/detail/pattern-get.d.ts +45 -0
  77. package/dist/tools/detail/pattern-get.d.ts.map +1 -0
  78. package/dist/tools/detail/pattern-get.js +87 -0
  79. package/dist/tools/detail/pattern-get.js.map +1 -0
  80. package/dist/tools/detail/reachability.d.ts +60 -0
  81. package/dist/tools/detail/reachability.d.ts.map +1 -0
  82. package/dist/tools/detail/reachability.js +168 -0
  83. package/dist/tools/detail/reachability.js.map +1 -0
  84. package/dist/tools/discovery/capabilities.d.ts +28 -0
  85. package/dist/tools/discovery/capabilities.d.ts.map +1 -0
  86. package/dist/tools/discovery/capabilities.js +112 -0
  87. package/dist/tools/discovery/capabilities.js.map +1 -0
  88. package/dist/tools/discovery/index.d.ts +13 -0
  89. package/dist/tools/discovery/index.d.ts.map +1 -0
  90. package/dist/tools/discovery/index.js +30 -0
  91. package/dist/tools/discovery/index.js.map +1 -0
  92. package/dist/tools/discovery/projects.d.ts +26 -0
  93. package/dist/tools/discovery/projects.d.ts.map +1 -0
  94. package/dist/tools/discovery/projects.js +210 -0
  95. package/dist/tools/discovery/projects.js.map +1 -0
  96. package/dist/tools/discovery/status.d.ts +42 -0
  97. package/dist/tools/discovery/status.d.ts.map +1 -0
  98. package/dist/tools/discovery/status.js +157 -0
  99. package/dist/tools/discovery/status.js.map +1 -0
  100. package/dist/tools/exploration/contracts-list.d.ts +35 -0
  101. package/dist/tools/exploration/contracts-list.d.ts.map +1 -0
  102. package/dist/tools/exploration/contracts-list.js +106 -0
  103. package/dist/tools/exploration/contracts-list.js.map +1 -0
  104. package/dist/tools/exploration/files-list.d.ts +29 -0
  105. package/dist/tools/exploration/files-list.d.ts.map +1 -0
  106. package/dist/tools/exploration/files-list.js +94 -0
  107. package/dist/tools/exploration/files-list.js.map +1 -0
  108. package/dist/tools/exploration/index.d.ts +17 -0
  109. package/dist/tools/exploration/index.d.ts.map +1 -0
  110. package/dist/tools/exploration/index.js +126 -0
  111. package/dist/tools/exploration/index.js.map +1 -0
  112. package/dist/tools/exploration/patterns-list.d.ts +40 -0
  113. package/dist/tools/exploration/patterns-list.d.ts.map +1 -0
  114. package/dist/tools/exploration/patterns-list.js +172 -0
  115. package/dist/tools/exploration/patterns-list.js.map +1 -0
  116. package/dist/tools/exploration/security-summary.d.ts +49 -0
  117. package/dist/tools/exploration/security-summary.d.ts.map +1 -0
  118. package/dist/tools/exploration/security-summary.js +111 -0
  119. package/dist/tools/exploration/security-summary.js.map +1 -0
  120. package/dist/tools/exploration/trends.d.ts +49 -0
  121. package/dist/tools/exploration/trends.d.ts.map +1 -0
  122. package/dist/tools/exploration/trends.js +147 -0
  123. package/dist/tools/exploration/trends.js.map +1 -0
  124. package/dist/tools/index.d.ts +13 -0
  125. package/dist/tools/index.d.ts.map +1 -0
  126. package/dist/tools/index.js +13 -0
  127. package/dist/tools/index.js.map +1 -0
  128. package/dist/tools/orchestration/context.d.ts +72 -0
  129. package/dist/tools/orchestration/context.d.ts.map +1 -0
  130. package/dist/tools/orchestration/context.js +499 -0
  131. package/dist/tools/orchestration/context.js.map +1 -0
  132. package/dist/tools/orchestration/index.d.ts +11 -0
  133. package/dist/tools/orchestration/index.d.ts.map +1 -0
  134. package/dist/tools/orchestration/index.js +56 -0
  135. package/dist/tools/orchestration/index.js.map +1 -0
  136. package/dist/tools/registry.d.ts +41 -0
  137. package/dist/tools/registry.d.ts.map +1 -0
  138. package/dist/tools/registry.js +64 -0
  139. package/dist/tools/registry.js.map +1 -0
  140. package/package.json +3 -3
package/dist/server.js CHANGED
@@ -5,9 +5,10 @@
5
5
  */
6
6
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
7
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
- import { PatternStore, ManifestStore, HistoryStore, DNAStore, PlaybookGenerator, AIContextBuilder, GENE_IDS, BoundaryStore, } from 'driftdetect-core';
8
+ import { PatternStore, ManifestStore, HistoryStore, DNAStore, PlaybookGenerator, AIContextBuilder, GENE_IDS, BoundaryStore, createCallGraphAnalyzer, createBoundaryScanner, createSemanticDataAccessScanner, createImpactAnalyzer, createDeadCodeDetector, createCoverageAnalyzer, } from 'driftdetect-core';
9
9
  import { PackManager } from './packs.js';
10
10
  import { FeedbackManager } from './feedback.js';
11
+ import { handleProjects } from './tools/discovery/projects.js';
11
12
  const PATTERN_CATEGORIES = [
12
13
  'structural', 'components', 'styling', 'api', 'auth', 'errors',
13
14
  'data-access', 'testing', 'logging', 'security', 'config',
@@ -38,6 +39,14 @@ const TOOLS = [
38
39
  type: 'number',
39
40
  description: 'Minimum confidence score (0.0-1.0)',
40
41
  },
42
+ limit: {
43
+ type: 'number',
44
+ description: 'Maximum patterns to return (default: 30)',
45
+ },
46
+ includeExamples: {
47
+ type: 'boolean',
48
+ description: 'Include example locations (default: true, set false for compact output)',
49
+ },
41
50
  },
42
51
  required: [],
43
52
  },
@@ -56,6 +65,18 @@ const TOOLS = [
56
65
  type: 'string',
57
66
  description: 'Filter by category',
58
67
  },
68
+ limit: {
69
+ type: 'number',
70
+ description: 'Maximum patterns to return (default: 20)',
71
+ },
72
+ offset: {
73
+ type: 'number',
74
+ description: 'Skip first N patterns for pagination (default: 0)',
75
+ },
76
+ compact: {
77
+ type: 'boolean',
78
+ description: 'Return summary only without full pattern details (default: false)',
79
+ },
59
80
  },
60
81
  required: ['path'],
61
82
  },
@@ -74,6 +95,14 @@ const TOOLS = [
74
95
  type: 'string',
75
96
  description: 'Filter by category',
76
97
  },
98
+ limit: {
99
+ type: 'number',
100
+ description: 'Maximum patterns to return (default: 10)',
101
+ },
102
+ maxLocations: {
103
+ type: 'number',
104
+ description: 'Maximum locations per pattern (default: 5)',
105
+ },
77
106
  },
78
107
  required: ['pattern'],
79
108
  },
@@ -394,6 +423,83 @@ const TOOLS = [
394
423
  type: 'boolean',
395
424
  description: 'Include boundary violations in response (default: true)',
396
425
  },
426
+ limit: {
427
+ type: 'number',
428
+ description: 'Maximum items per section (default: 10)',
429
+ },
430
+ },
431
+ required: [],
432
+ },
433
+ },
434
+ // Call Graph Tool
435
+ {
436
+ name: 'drift_callgraph',
437
+ description: 'Build and query call graphs for code reachability analysis. Answers "What data can this code access?", "Who can reach this data?", "What breaks if I change this?", "What code is never called?", and "Which sensitive data paths are tested?" Use this to understand the impact of security findings or code changes.',
438
+ inputSchema: {
439
+ type: 'object',
440
+ properties: {
441
+ action: {
442
+ type: 'string',
443
+ enum: ['status', 'build', 'reach', 'inverse', 'function', 'security', 'impact', 'dead', 'coverage'],
444
+ description: 'Action to perform (default: status). "status" shows overview, "build" builds the call graph, "reach" finds what data a location can access, "inverse" finds who can access specific data, "function" shows function details, "security" shows security-prioritized view (P0-P4 tiers), "impact" analyzes what breaks if you change a file or function, "dead" finds functions that are never called, "coverage" analyzes test coverage for sensitive data access paths',
445
+ },
446
+ location: {
447
+ type: 'string',
448
+ description: 'Code location for "reach" action (file:line or function_name)',
449
+ },
450
+ target: {
451
+ type: 'string',
452
+ description: 'Data target for "inverse" action (table or table.field), or file/function for "impact" action',
453
+ },
454
+ functionName: {
455
+ type: 'string',
456
+ description: 'Function name for "function" action',
457
+ },
458
+ maxDepth: {
459
+ type: 'number',
460
+ description: 'Maximum traversal depth (default: 10)',
461
+ },
462
+ confidence: {
463
+ type: 'string',
464
+ enum: ['high', 'medium', 'low'],
465
+ description: 'Minimum confidence level for "dead" action (default: low)',
466
+ },
467
+ limit: {
468
+ type: 'number',
469
+ description: 'Maximum items per section in output (default: 10)',
470
+ },
471
+ },
472
+ required: [],
473
+ },
474
+ },
475
+ // Projects Tool
476
+ {
477
+ name: 'drift_projects',
478
+ description: 'List and manage registered drift projects. Enables working across multiple codebases. Use this to see all projects, switch between them, or get details about a specific project.',
479
+ inputSchema: {
480
+ type: 'object',
481
+ properties: {
482
+ action: {
483
+ type: 'string',
484
+ enum: ['list', 'info', 'switch', 'recent'],
485
+ description: 'Action to perform (default: list). "list" shows all projects, "info" shows details for a project, "switch" changes active project, "recent" shows recently used projects',
486
+ },
487
+ project: {
488
+ type: 'string',
489
+ description: 'Project name, ID, or path (for info/switch actions)',
490
+ },
491
+ language: {
492
+ type: 'string',
493
+ description: 'Filter by language (for list action)',
494
+ },
495
+ framework: {
496
+ type: 'string',
497
+ description: 'Filter by framework (for list action)',
498
+ },
499
+ limit: {
500
+ type: 'number',
501
+ description: 'Maximum projects to return (default: all)',
502
+ },
397
503
  },
398
504
  required: [],
399
505
  },
@@ -449,6 +555,10 @@ export function createDriftMCPServer(config) {
449
555
  return await handleDNACheck(config.projectRoot, dnaStore, args);
450
556
  case 'drift_boundaries':
451
557
  return await handleBoundaries(boundaryStore, args);
558
+ case 'drift_callgraph':
559
+ return await handleCallGraph(config.projectRoot, args);
560
+ case 'drift_projects':
561
+ return await handleProjects(args);
452
562
  default:
453
563
  return {
454
564
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
@@ -496,6 +606,11 @@ async function handlePatterns(store, args) {
496
606
  if (args.minConfidence !== undefined) {
497
607
  patterns = patterns.filter(p => p.confidence.score >= args.minConfidence);
498
608
  }
609
+ const totalCount = patterns.length;
610
+ const limit = args.limit ?? 30;
611
+ const includeExamples = args.includeExamples ?? true;
612
+ // Apply limit
613
+ patterns = patterns.slice(0, limit);
499
614
  // Format for AI consumption
500
615
  const result = patterns.map(p => ({
501
616
  id: p.id,
@@ -507,13 +622,22 @@ async function handlePatterns(store, args) {
507
622
  confidenceLevel: p.confidence.level,
508
623
  locationCount: p.locations.length,
509
624
  outlierCount: p.outliers.length,
510
- exampleLocations: p.locations.slice(0, 3).map(l => ({
511
- file: l.file,
512
- line: l.line,
513
- })),
625
+ ...(includeExamples ? {
626
+ exampleLocations: p.locations.slice(0, 2).map(l => ({
627
+ file: l.file,
628
+ line: l.line,
629
+ })),
630
+ } : {}),
514
631
  }));
632
+ // Add pagination info
633
+ const output = {
634
+ patterns: result,
635
+ total: totalCount,
636
+ showing: result.length,
637
+ ...(totalCount > limit ? { truncated: true, hint: `Use limit parameter to see more (showing ${limit} of ${totalCount})` } : {}),
638
+ };
515
639
  return {
516
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
640
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
517
641
  };
518
642
  }
519
643
  async function handleFiles(store, args) {
@@ -530,6 +654,80 @@ async function handleFiles(store, args) {
530
654
  content: [{ type: 'text', text: `No patterns found in "${args.path}"` }],
531
655
  };
532
656
  }
657
+ const limit = args.limit ?? 20;
658
+ const offset = args.offset ?? 0;
659
+ const compact = args.compact ?? false;
660
+ // Handle array results (from glob patterns)
661
+ if (Array.isArray(result)) {
662
+ const totalCount = result.length;
663
+ const paginatedResult = result.slice(offset, offset + limit);
664
+ if (compact) {
665
+ // Return summary only
666
+ const summary = {
667
+ totalFiles: totalCount,
668
+ showing: paginatedResult.length,
669
+ offset,
670
+ files: paginatedResult.map((r) => ({
671
+ file: r.file,
672
+ patternCount: r.patterns?.length ?? 0,
673
+ })),
674
+ ...(totalCount > offset + limit ? {
675
+ truncated: true,
676
+ hint: `Use offset=${offset + limit} to see next page`
677
+ } : {}),
678
+ };
679
+ return {
680
+ content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
681
+ };
682
+ }
683
+ const output = {
684
+ totalFiles: totalCount,
685
+ showing: paginatedResult.length,
686
+ offset,
687
+ results: paginatedResult,
688
+ ...(totalCount > offset + limit ? {
689
+ truncated: true,
690
+ hint: `Use offset=${offset + limit} to see next page`
691
+ } : {}),
692
+ };
693
+ return {
694
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
695
+ };
696
+ }
697
+ // Single file result - limit patterns within the file
698
+ if (result.patterns && Array.isArray(result.patterns)) {
699
+ const totalPatterns = result.patterns.length;
700
+ const paginatedPatterns = result.patterns.slice(offset, offset + limit);
701
+ if (compact) {
702
+ const summary = {
703
+ file: result.file,
704
+ totalPatterns,
705
+ showing: paginatedPatterns.length,
706
+ categories: [...new Set(result.patterns.map((p) => p.category))],
707
+ ...(totalPatterns > offset + limit ? {
708
+ truncated: true,
709
+ hint: `Use offset=${offset + limit} to see next page`
710
+ } : {}),
711
+ };
712
+ return {
713
+ content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
714
+ };
715
+ }
716
+ const output = {
717
+ ...result,
718
+ patterns: paginatedPatterns,
719
+ totalPatterns,
720
+ showing: paginatedPatterns.length,
721
+ offset,
722
+ ...(totalPatterns > offset + limit ? {
723
+ truncated: true,
724
+ hint: `Use offset=${offset + limit} to see next page`
725
+ } : {}),
726
+ };
727
+ return {
728
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
729
+ };
730
+ }
533
731
  return {
534
732
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
535
733
  };
@@ -542,18 +740,36 @@ async function handleWhere(store, args) {
542
740
  if (args.category) {
543
741
  patterns = patterns.filter(p => p.category === args.category);
544
742
  }
743
+ const limit = args.limit ?? 10;
744
+ const maxLocations = args.maxLocations ?? 5;
745
+ const totalPatterns = patterns.length;
746
+ // Apply pattern limit
747
+ patterns = patterns.slice(0, limit);
545
748
  const result = patterns.map(p => ({
546
749
  id: p.id,
547
750
  name: p.name,
548
751
  category: p.category,
549
- locations: p.locations.map(l => ({
752
+ totalLocations: p.locations.length,
753
+ locations: p.locations.slice(0, maxLocations).map(l => ({
550
754
  file: l.file,
551
755
  line: l.line,
552
756
  column: l.column,
553
757
  })),
758
+ ...(p.locations.length > maxLocations ? {
759
+ locationsHint: `Showing ${maxLocations} of ${p.locations.length}. Use maxLocations parameter to see more.`
760
+ } : {}),
554
761
  }));
762
+ const output = {
763
+ patterns: result,
764
+ total: totalPatterns,
765
+ showing: result.length,
766
+ ...(totalPatterns > limit ? {
767
+ truncated: true,
768
+ hint: `Showing ${limit} of ${totalPatterns} patterns. Use limit parameter to see more.`
769
+ } : {}),
770
+ };
555
771
  return {
556
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
772
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
557
773
  };
558
774
  }
559
775
  async function handleExport(store, args) {
@@ -1366,6 +1582,43 @@ async function handleParserInfo(args) {
1366
1582
  loadingError: javaLoadingError,
1367
1583
  };
1368
1584
  }
1585
+ // PHP parser info
1586
+ if (language === 'php' || language === 'all') {
1587
+ let phpTreeSitterAvailable = false;
1588
+ let phpLoadingError;
1589
+ try {
1590
+ const core = await import('driftdetect-core');
1591
+ // Check if the functions exist
1592
+ if ('isPhpTreeSitterAvailable' in core && 'getPhpLoadingError' in core) {
1593
+ phpTreeSitterAvailable = core.isPhpTreeSitterAvailable();
1594
+ phpLoadingError = core.getPhpLoadingError() ?? undefined;
1595
+ }
1596
+ else {
1597
+ phpLoadingError = 'PHP parser functions not yet available in driftdetect-core';
1598
+ }
1599
+ }
1600
+ catch {
1601
+ phpLoadingError = 'PHP parser not available';
1602
+ }
1603
+ info.php = {
1604
+ treeSitterAvailable: phpTreeSitterAvailable,
1605
+ activeParser: phpTreeSitterAvailable ? 'tree-sitter' : 'regex',
1606
+ capabilities: {
1607
+ basicParsing: true,
1608
+ classExtraction: phpTreeSitterAvailable,
1609
+ methodExtraction: phpTreeSitterAvailable,
1610
+ attributeExtraction: phpTreeSitterAvailable,
1611
+ laravelControllers: phpTreeSitterAvailable,
1612
+ laravelModels: phpTreeSitterAvailable,
1613
+ traitExtraction: phpTreeSitterAvailable,
1614
+ enumExtraction: phpTreeSitterAvailable,
1615
+ },
1616
+ supportedFrameworks: phpTreeSitterAvailable
1617
+ ? ['laravel', 'symfony', 'php8']
1618
+ : ['laravel'],
1619
+ loadingError: phpLoadingError,
1620
+ };
1621
+ }
1369
1622
  // Build human-readable output
1370
1623
  let output = '# Parser Information\n\n';
1371
1624
  if (info.python) {
@@ -1438,8 +1691,28 @@ async function handleParserInfo(args) {
1438
1691
  output += `> โš ๏ธ Loading error: ${java.loadingError}\n\n`;
1439
1692
  }
1440
1693
  }
1694
+ if (info.php) {
1695
+ const php = info.php;
1696
+ output += '## PHP\n\n';
1697
+ output += `- **Active Parser:** ${php.activeParser}\n`;
1698
+ output += `- **Tree-sitter:** ${php.treeSitterAvailable ? 'โœ“ available' : 'โœ— not installed'}\n`;
1699
+ if (php.supportedFrameworks.length > 0) {
1700
+ output += `- **Supported Frameworks:** ${php.supportedFrameworks.join(', ')}\n`;
1701
+ }
1702
+ output += '\n';
1703
+ output += '### Capabilities\n\n';
1704
+ for (const [cap, enabled] of Object.entries(php.capabilities)) {
1705
+ const emoji = enabled ? 'โœ“' : 'โœ—';
1706
+ const capName = cap.replace(/([A-Z])/g, ' $1').toLowerCase().trim();
1707
+ output += `- ${emoji} ${capName}\n`;
1708
+ }
1709
+ output += '\n';
1710
+ if (php.loadingError) {
1711
+ output += `> โš ๏ธ Loading error: ${php.loadingError}\n\n`;
1712
+ }
1713
+ }
1441
1714
  // Installation tips
1442
- if ((info.python && !info.python.treeSitterAvailable) || (info.csharp && !info.csharp.treeSitterAvailable) || (info.java && !info.java.treeSitterAvailable)) {
1715
+ if ((info.python && !info.python.treeSitterAvailable) || (info.csharp && !info.csharp.treeSitterAvailable) || (info.java && !info.java.treeSitterAvailable) || (info.php && !info.php.treeSitterAvailable)) {
1443
1716
  output += '## Installation Tips\n\n';
1444
1717
  if (info.python && !info.python.treeSitterAvailable) {
1445
1718
  output += 'To enable full Python support (Pydantic, Django, nested types):\n';
@@ -1453,6 +1726,10 @@ async function handleParserInfo(args) {
1453
1726
  output += 'To enable full Java support (Spring Boot, annotations, records):\n';
1454
1727
  output += '```bash\npnpm add tree-sitter tree-sitter-java\n```\n\n';
1455
1728
  }
1729
+ if (info.php && !info.php.treeSitterAvailable) {
1730
+ output += 'To enable full PHP support (Laravel, Symfony, PHP 8 attributes):\n';
1731
+ output += '```bash\npnpm add tree-sitter tree-sitter-php\n```\n\n';
1732
+ }
1456
1733
  }
1457
1734
  return {
1458
1735
  content: [{ type: 'text', text: output }],
@@ -1768,6 +2045,7 @@ async function handleBoundaries(store, args) {
1768
2045
  await store.initialize();
1769
2046
  const action = args.action ?? 'overview';
1770
2047
  const includeViolations = args.includeViolations ?? true;
2048
+ const limit = args.limit ?? 10;
1771
2049
  switch (action) {
1772
2050
  case 'overview': {
1773
2051
  const accessMap = store.getAccessMap();
@@ -1785,12 +2063,16 @@ async function handleBoundaries(store, args) {
1785
2063
  accessCount: info.accessedBy.length,
1786
2064
  hasSensitive: info.sensitiveFields.length > 0,
1787
2065
  }))
1788
- .sort((a, b) => b.accessCount - a.accessCount)
1789
- .slice(0, 10);
1790
- for (const table of tableEntries) {
2066
+ .sort((a, b) => b.accessCount - a.accessCount);
2067
+ const totalTables = tableEntries.length;
2068
+ const limitedTables = tableEntries.slice(0, limit);
2069
+ for (const table of limitedTables) {
1791
2070
  const sensitive = table.hasSensitive ? ' โš ๏ธ' : '';
1792
2071
  output += `- **${table.name}**${sensitive}: ${table.accessCount} access points\n`;
1793
2072
  }
2073
+ if (totalTables > limit) {
2074
+ output += `\n*... and ${totalTables - limit} more tables. Use \`drift_boundaries action="table" table="<name>"\` for details.*\n`;
2075
+ }
1794
2076
  output += '\n';
1795
2077
  }
1796
2078
  if (sensitiveFields.length > 0) {
@@ -1801,11 +2083,15 @@ async function handleBoundaries(store, args) {
1801
2083
  fieldCounts.set(key, (fieldCounts.get(key) ?? 0) + 1);
1802
2084
  }
1803
2085
  const sortedFields = Array.from(fieldCounts.entries())
1804
- .sort((a, b) => b[1] - a[1])
1805
- .slice(0, 10);
1806
- for (const [fieldName, count] of sortedFields) {
2086
+ .sort((a, b) => b[1] - a[1]);
2087
+ const totalFields = sortedFields.length;
2088
+ const limitedFields = sortedFields.slice(0, limit);
2089
+ for (const [fieldName, count] of limitedFields) {
1807
2090
  output += `- **${fieldName}**: ${count} locations\n`;
1808
2091
  }
2092
+ if (totalFields > limit) {
2093
+ output += `\n*... and ${totalFields - limit} more fields. Use \`drift_boundaries action="sensitive"\` for full list.*\n`;
2094
+ }
1809
2095
  output += '\n';
1810
2096
  }
1811
2097
  if (includeViolations) {
@@ -1813,12 +2099,13 @@ async function handleBoundaries(store, args) {
1813
2099
  if (rules) {
1814
2100
  const violations = store.checkAllViolations();
1815
2101
  if (violations.length > 0) {
2102
+ const limitedViolations = violations.slice(0, limit);
1816
2103
  output += `## โš ๏ธ Violations (${violations.length})\n\n`;
1817
- for (const v of violations.slice(0, 5)) {
2104
+ for (const v of limitedViolations) {
1818
2105
  output += `- **${v.file}:${v.line}** - ${v.message}\n`;
1819
2106
  }
1820
- if (violations.length > 5) {
1821
- output += `- ... and ${violations.length - 5} more\n`;
2107
+ if (violations.length > limit) {
2108
+ output += `\n*... and ${violations.length - limit} more. Use \`drift_boundaries action="check"\` for full list.*\n`;
1822
2109
  }
1823
2110
  }
1824
2111
  else {
@@ -2056,4 +2343,835 @@ async function handleBoundaries(store, args) {
2056
2343
  };
2057
2344
  }
2058
2345
  }
2346
+ // ============================================================================
2347
+ // Call Graph Handler Function
2348
+ // ============================================================================
2349
+ /**
2350
+ * Handle drift_callgraph tool - Call graph analysis
2351
+ */
2352
+ async function handleCallGraph(projectRoot, args) {
2353
+ const action = args.action ?? 'status';
2354
+ const maxDepth = args.maxDepth ?? 10;
2355
+ const limit = args.limit ?? 10;
2356
+ const analyzer = createCallGraphAnalyzer({ rootDir: projectRoot });
2357
+ switch (action) {
2358
+ case 'build': {
2359
+ try {
2360
+ // File patterns to scan
2361
+ const filePatterns = [
2362
+ '**/*.ts',
2363
+ '**/*.tsx',
2364
+ '**/*.js',
2365
+ '**/*.jsx',
2366
+ '**/*.py',
2367
+ ];
2368
+ // Step 1: Run semantic data access scanner (tree-sitter/TypeScript compiler based)
2369
+ const semanticScanner = createSemanticDataAccessScanner({ rootDir: projectRoot });
2370
+ const semanticResult = await semanticScanner.scanDirectory({ patterns: filePatterns });
2371
+ // Use semantic results as primary source
2372
+ let dataAccessPoints = semanticResult.accessPoints;
2373
+ // Step 2: Fall back to boundary scanner for additional coverage (regex-based)
2374
+ const boundaryScanner = createBoundaryScanner({ rootDir: projectRoot });
2375
+ await boundaryScanner.initialize();
2376
+ const boundaryResult = await boundaryScanner.scanDirectory({ patterns: filePatterns });
2377
+ // Merge boundary results with semantic results (semantic takes precedence)
2378
+ for (const [, accessPoint] of Object.entries(boundaryResult.accessMap.accessPoints)) {
2379
+ const existing = dataAccessPoints.get(accessPoint.file) ?? [];
2380
+ // Only add if not already detected by semantic scanner
2381
+ const isDuplicate = existing.some(ap => ap.line === accessPoint.line && ap.table === accessPoint.table);
2382
+ if (!isDuplicate) {
2383
+ existing.push(accessPoint);
2384
+ dataAccessPoints.set(accessPoint.file, existing);
2385
+ }
2386
+ }
2387
+ // Step 3: Build call graph with data access points
2388
+ await analyzer.initialize();
2389
+ const graph = await analyzer.scan(filePatterns, dataAccessPoints);
2390
+ let output = '# Call Graph Built\n\n';
2391
+ output += `- **Functions:** ${graph.stats.totalFunctions}\n`;
2392
+ output += `- **Call Sites:** ${graph.stats.totalCallSites}\n`;
2393
+ output += `- **Resolved:** ${graph.stats.resolvedCallSites} (${Math.round(graph.stats.resolvedCallSites / Math.max(1, graph.stats.totalCallSites) * 100)}%)\n`;
2394
+ output += `- **Entry Points:** ${graph.entryPoints.length}\n`;
2395
+ output += `- **Data Accessors:** ${graph.dataAccessors.length}\n\n`;
2396
+ // Semantic scanner stats (primary)
2397
+ if (semanticResult.stats.accessPointsFound > 0) {
2398
+ output += '## Data Access Detection (Semantic)\n\n';
2399
+ output += `- **Files Scanned:** ${semanticResult.stats.filesScanned}\n`;
2400
+ output += `- **Access Points:** ${semanticResult.stats.accessPointsFound}\n`;
2401
+ if (Object.keys(semanticResult.stats.byOrm).length > 0) {
2402
+ const orms = Object.entries(semanticResult.stats.byOrm)
2403
+ .sort((a, b) => b[1] - a[1])
2404
+ .map(([orm, count]) => `${orm}: ${count}`)
2405
+ .join(', ');
2406
+ output += `- **By ORM:** ${orms}\n`;
2407
+ }
2408
+ output += '\n';
2409
+ }
2410
+ // Boundary stats (fallback)
2411
+ if (boundaryResult.stats.accessPointsFound > 0) {
2412
+ output += '## Data Access Detection (Regex Fallback)\n\n';
2413
+ output += `- **Tables Found:** ${boundaryResult.stats.tablesFound}\n`;
2414
+ output += `- **Access Points:** ${boundaryResult.stats.accessPointsFound}\n`;
2415
+ output += `- **Sensitive Fields:** ${boundaryResult.stats.sensitiveFieldsFound}\n\n`;
2416
+ }
2417
+ const languages = Object.entries(graph.stats.byLanguage)
2418
+ .filter(([, count]) => count > 0)
2419
+ .sort((a, b) => b[1] - a[1]);
2420
+ if (languages.length > 0) {
2421
+ output += '## By Language\n\n';
2422
+ for (const [lang, count] of languages) {
2423
+ output += `- **${lang}:** ${count} functions\n`;
2424
+ }
2425
+ }
2426
+ return { content: [{ type: 'text', text: output }] };
2427
+ }
2428
+ catch (error) {
2429
+ return {
2430
+ content: [{ type: 'text', text: `Error building call graph: ${error}` }],
2431
+ isError: true,
2432
+ };
2433
+ }
2434
+ }
2435
+ case 'status': {
2436
+ try {
2437
+ await analyzer.initialize();
2438
+ const graph = analyzer.getGraph();
2439
+ if (!graph) {
2440
+ return {
2441
+ content: [{
2442
+ type: 'text',
2443
+ text: '# No Call Graph Found\n\n' +
2444
+ 'Run `drift_callgraph action="build"` to build the call graph first.\n\n' +
2445
+ 'The call graph enables:\n' +
2446
+ '- Forward reachability: "What data can this code access?"\n' +
2447
+ '- Inverse reachability: "Who can access this data?"\n' +
2448
+ '- Security impact analysis\n',
2449
+ }],
2450
+ };
2451
+ }
2452
+ let output = '# Call Graph Status\n\n';
2453
+ output += `- **Functions:** ${graph.stats.totalFunctions}\n`;
2454
+ output += `- **Call Sites:** ${graph.stats.totalCallSites} (${graph.stats.resolvedCallSites} resolved)\n`;
2455
+ output += `- **Entry Points:** ${graph.entryPoints.length}\n`;
2456
+ output += `- **Data Accessors:** ${graph.dataAccessors.length}\n\n`;
2457
+ if (graph.entryPoints.length > 0) {
2458
+ output += '## Entry Points\n\n';
2459
+ const limitedEntryPoints = graph.entryPoints.slice(0, limit);
2460
+ for (const id of limitedEntryPoints) {
2461
+ const func = graph.functions.get(id);
2462
+ if (func) {
2463
+ output += `- **${func.qualifiedName}** @ ${func.file}:${func.startLine}\n`;
2464
+ }
2465
+ }
2466
+ if (graph.entryPoints.length > limit) {
2467
+ output += `\n*... and ${graph.entryPoints.length - limit} more entry points*\n`;
2468
+ }
2469
+ output += '\n';
2470
+ }
2471
+ if (graph.dataAccessors.length > 0) {
2472
+ output += '## Data Accessors\n\n';
2473
+ const limitedAccessors = graph.dataAccessors.slice(0, limit);
2474
+ for (const id of limitedAccessors) {
2475
+ const func = graph.functions.get(id);
2476
+ if (func) {
2477
+ const tables = [...new Set(func.dataAccess.map(d => d.table))];
2478
+ output += `- **${func.qualifiedName}** โ†’ [${tables.join(', ')}]\n`;
2479
+ }
2480
+ }
2481
+ if (graph.dataAccessors.length > limit) {
2482
+ output += `\n*... and ${graph.dataAccessors.length - limit} more data accessors*\n`;
2483
+ }
2484
+ }
2485
+ return { content: [{ type: 'text', text: output }] };
2486
+ }
2487
+ catch (error) {
2488
+ return {
2489
+ content: [{ type: 'text', text: `Error: ${error}` }],
2490
+ isError: true,
2491
+ };
2492
+ }
2493
+ }
2494
+ case 'reach': {
2495
+ if (!args.location) {
2496
+ return {
2497
+ content: [{
2498
+ type: 'text',
2499
+ text: 'Error: location parameter required for "reach" action.\n\n' +
2500
+ 'Examples:\n' +
2501
+ '- `drift_callgraph action="reach" location="src/auth.py:45"`\n' +
2502
+ '- `drift_callgraph action="reach" location="login_user"`',
2503
+ }],
2504
+ isError: true,
2505
+ };
2506
+ }
2507
+ try {
2508
+ await analyzer.initialize();
2509
+ const graph = analyzer.getGraph();
2510
+ if (!graph) {
2511
+ return {
2512
+ content: [{ type: 'text', text: 'No call graph found. Run build first.' }],
2513
+ isError: true,
2514
+ };
2515
+ }
2516
+ // Parse location
2517
+ let result;
2518
+ if (args.location.includes(':')) {
2519
+ const parts = args.location.split(':');
2520
+ const file = parts[0];
2521
+ const lineStr = parts[1];
2522
+ if (file && lineStr) {
2523
+ const line = parseInt(lineStr, 10);
2524
+ if (!isNaN(line)) {
2525
+ result = analyzer.getReachableData(file, line, { maxDepth });
2526
+ }
2527
+ }
2528
+ }
2529
+ if (!result) {
2530
+ // Try as function name
2531
+ let funcId;
2532
+ for (const [id, func] of graph.functions) {
2533
+ if (func.name === args.location || func.qualifiedName === args.location) {
2534
+ funcId = id;
2535
+ break;
2536
+ }
2537
+ }
2538
+ if (funcId) {
2539
+ result = analyzer.getReachableDataFromFunction(funcId, { maxDepth });
2540
+ }
2541
+ else {
2542
+ return {
2543
+ content: [{ type: 'text', text: `Function or location '${args.location}' not found.` }],
2544
+ isError: true,
2545
+ };
2546
+ }
2547
+ }
2548
+ let output = '# Reachability Analysis\n\n';
2549
+ output += `**Origin:** ${args.location}\n`;
2550
+ output += `**Tables Reachable:** ${result.tables.join(', ') || 'none'}\n`;
2551
+ output += `**Functions Traversed:** ${result.functionsTraversed}\n`;
2552
+ output += `**Max Depth:** ${result.maxDepth}\n\n`;
2553
+ if (result.sensitiveFields.length > 0) {
2554
+ output += '## โš ๏ธ Sensitive Fields Accessible\n\n';
2555
+ for (const sf of result.sensitiveFields) {
2556
+ output += `- **${sf.field.table}.${sf.field.field}** (${sf.field.sensitivityType})\n`;
2557
+ output += ` - ${sf.accessCount} access point(s), ${sf.paths.length} path(s)\n`;
2558
+ }
2559
+ output += '\n';
2560
+ }
2561
+ if (result.reachableAccess.length > 0) {
2562
+ output += '## Data Access Points\n\n';
2563
+ for (const ra of result.reachableAccess.slice(0, 15)) {
2564
+ output += `- **${ra.access.operation}** ${ra.access.table}.${ra.access.fields.join(', ')}\n`;
2565
+ output += ` - Path: ${ra.path.map(p => p.functionName).join(' โ†’ ')}\n`;
2566
+ }
2567
+ if (result.reachableAccess.length > 15) {
2568
+ output += `- ... and ${result.reachableAccess.length - 15} more\n`;
2569
+ }
2570
+ }
2571
+ return { content: [{ type: 'text', text: output }] };
2572
+ }
2573
+ catch (error) {
2574
+ return {
2575
+ content: [{ type: 'text', text: `Error: ${error}` }],
2576
+ isError: true,
2577
+ };
2578
+ }
2579
+ }
2580
+ case 'inverse': {
2581
+ if (!args.target) {
2582
+ return {
2583
+ content: [{
2584
+ type: 'text',
2585
+ text: 'Error: target parameter required for "inverse" action.\n\n' +
2586
+ 'Examples:\n' +
2587
+ '- `drift_callgraph action="inverse" target="users"`\n' +
2588
+ '- `drift_callgraph action="inverse" target="users.password_hash"`',
2589
+ }],
2590
+ isError: true,
2591
+ };
2592
+ }
2593
+ try {
2594
+ await analyzer.initialize();
2595
+ const graph = analyzer.getGraph();
2596
+ if (!graph) {
2597
+ return {
2598
+ content: [{ type: 'text', text: 'No call graph found. Run build first.' }],
2599
+ isError: true,
2600
+ };
2601
+ }
2602
+ const parts = args.target.split('.');
2603
+ const table = parts[0] ?? '';
2604
+ const field = parts.length > 1 ? parts.slice(1).join('.') : undefined;
2605
+ const result = analyzer.getCodePathsToData(field ? { table, field, maxDepth } : { table, maxDepth });
2606
+ let output = '# Inverse Reachability\n\n';
2607
+ output += `**Target:** ${args.target}\n`;
2608
+ output += `**Direct Accessors:** ${result.totalAccessors}\n`;
2609
+ output += `**Entry Points That Can Reach:** ${result.entryPoints.length}\n\n`;
2610
+ if (result.accessPaths.length > 0) {
2611
+ output += '## Access Paths\n\n';
2612
+ for (const ap of result.accessPaths.slice(0, 10)) {
2613
+ const entryFunc = graph.functions.get(ap.entryPoint);
2614
+ if (entryFunc) {
2615
+ output += `### ${entryFunc.qualifiedName}\n`;
2616
+ output += `- Path: ${ap.path.map(p => p.functionName).join(' โ†’ ')}\n\n`;
2617
+ }
2618
+ }
2619
+ if (result.accessPaths.length > 10) {
2620
+ output += `... and ${result.accessPaths.length - 10} more paths\n`;
2621
+ }
2622
+ }
2623
+ else {
2624
+ output += 'No entry points can reach this data.\n';
2625
+ }
2626
+ return { content: [{ type: 'text', text: output }] };
2627
+ }
2628
+ catch (error) {
2629
+ return {
2630
+ content: [{ type: 'text', text: `Error: ${error}` }],
2631
+ isError: true,
2632
+ };
2633
+ }
2634
+ }
2635
+ case 'function': {
2636
+ if (!args.functionName) {
2637
+ return {
2638
+ content: [{
2639
+ type: 'text',
2640
+ text: 'Error: functionName parameter required for "function" action.',
2641
+ }],
2642
+ isError: true,
2643
+ };
2644
+ }
2645
+ try {
2646
+ await analyzer.initialize();
2647
+ const graph = analyzer.getGraph();
2648
+ if (!graph) {
2649
+ return {
2650
+ content: [{ type: 'text', text: 'No call graph found. Run build first.' }],
2651
+ isError: true,
2652
+ };
2653
+ }
2654
+ let func;
2655
+ for (const [, f] of graph.functions) {
2656
+ if (f.name === args.functionName || f.qualifiedName === args.functionName) {
2657
+ func = f;
2658
+ break;
2659
+ }
2660
+ }
2661
+ if (!func) {
2662
+ return {
2663
+ content: [{ type: 'text', text: `Function '${args.functionName}' not found.` }],
2664
+ isError: true,
2665
+ };
2666
+ }
2667
+ let output = `# Function: ${func.qualifiedName}\n\n`;
2668
+ output += `- **File:** ${func.file}:${func.startLine}\n`;
2669
+ output += `- **Language:** ${func.language}\n`;
2670
+ if (func.className)
2671
+ output += `- **Class:** ${func.className}\n`;
2672
+ output += `- **Exported:** ${func.isExported ? 'yes' : 'no'}\n`;
2673
+ output += `- **Async:** ${func.isAsync ? 'yes' : 'no'}\n\n`;
2674
+ if (func.parameters.length > 0) {
2675
+ output += '## Parameters\n\n';
2676
+ for (const p of func.parameters) {
2677
+ output += `- **${p.name}**${p.type ? `: ${p.type}` : ''}\n`;
2678
+ }
2679
+ output += '\n';
2680
+ }
2681
+ if (func.calls.length > 0) {
2682
+ output += `## Calls (${func.calls.length})\n\n`;
2683
+ for (const c of func.calls.slice(0, 10)) {
2684
+ const status = c.resolved ? 'โœ“' : '?';
2685
+ output += `- ${status} **${c.calleeName}** (line ${c.line})\n`;
2686
+ }
2687
+ if (func.calls.length > 10) {
2688
+ output += `- ... and ${func.calls.length - 10} more\n`;
2689
+ }
2690
+ output += '\n';
2691
+ }
2692
+ if (func.calledBy.length > 0) {
2693
+ output += `## Called By (${func.calledBy.length})\n\n`;
2694
+ for (const c of func.calledBy.slice(0, 10)) {
2695
+ const caller = graph.functions.get(c.callerId);
2696
+ output += `- **${caller?.qualifiedName ?? c.callerId}**\n`;
2697
+ }
2698
+ if (func.calledBy.length > 10) {
2699
+ output += `- ... and ${func.calledBy.length - 10} more\n`;
2700
+ }
2701
+ output += '\n';
2702
+ }
2703
+ if (func.dataAccess.length > 0) {
2704
+ output += '## Data Access\n\n';
2705
+ for (const d of func.dataAccess) {
2706
+ output += `- **${d.operation}** ${d.table}.${d.fields.join(', ')}\n`;
2707
+ }
2708
+ }
2709
+ return { content: [{ type: 'text', text: output }] };
2710
+ }
2711
+ catch (error) {
2712
+ return {
2713
+ content: [{ type: 'text', text: `Error: ${error}` }],
2714
+ isError: true,
2715
+ };
2716
+ }
2717
+ }
2718
+ case 'security': {
2719
+ try {
2720
+ // Import security prioritizer dynamically to avoid circular deps
2721
+ const { createSecurityPrioritizer } = await import('driftdetect-core');
2722
+ // Run boundary scan
2723
+ const boundaryScanner = createBoundaryScanner({ rootDir: projectRoot });
2724
+ await boundaryScanner.initialize();
2725
+ const filePatterns = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py'];
2726
+ const boundaryResult = await boundaryScanner.scanDirectory({ patterns: filePatterns });
2727
+ // Prioritize by security
2728
+ const prioritizer = createSecurityPrioritizer();
2729
+ const prioritized = prioritizer.prioritize(boundaryResult.accessMap);
2730
+ const { summary } = prioritized;
2731
+ let output = '# ๐Ÿ”’ Security-Prioritized Data Access\n\n';
2732
+ output += '## Summary\n\n';
2733
+ output += `- **Total Access Points:** ${summary.totalAccessPoints}\n`;
2734
+ output += `- **๐Ÿ”ด Critical (P0/P1):** ${summary.criticalCount}\n`;
2735
+ output += `- **๐ŸŸก High (P2):** ${summary.highCount}\n`;
2736
+ output += `- **โšช Low (P3/P4):** ${summary.lowCount}\n`;
2737
+ output += `- **๐Ÿ“ฆ Noise (filtered):** ${summary.noiseCount}\n\n`;
2738
+ // Regulations
2739
+ if (summary.regulations.length > 0) {
2740
+ output += '## Regulatory Implications\n\n';
2741
+ output += summary.regulations.map(r => `**${r.toUpperCase()}**`).join(', ') + '\n\n';
2742
+ }
2743
+ // Critical items - use limit parameter
2744
+ if (prioritized.critical.length > 0) {
2745
+ const limitedCritical = prioritized.critical.slice(0, limit);
2746
+ output += '## ๐Ÿšจ Critical Security Items (P0/P1)\n\n';
2747
+ output += 'These require immediate attention:\n\n';
2748
+ for (const p of limitedCritical) {
2749
+ const sensitivityIcon = p.security.maxSensitivity === 'credentials' ? '๐Ÿ”‘' :
2750
+ p.security.maxSensitivity === 'financial' ? '๐Ÿ’ฐ' :
2751
+ p.security.maxSensitivity === 'health' ? '๐Ÿฅ' :
2752
+ p.security.maxSensitivity === 'pii' ? '๐Ÿ‘ค' : 'โ“';
2753
+ output += `### ${p.security.tier} ${sensitivityIcon} ${p.accessPoint.table}\n`;
2754
+ output += `- **Operation:** ${p.accessPoint.operation}\n`;
2755
+ output += `- **Fields:** ${p.accessPoint.fields.join(', ') || '*'}\n`;
2756
+ output += `- **Location:** ${p.accessPoint.file}:${p.accessPoint.line}\n`;
2757
+ output += `- **Risk Score:** ${p.security.riskScore}/100\n`;
2758
+ output += `- **Rationale:** ${p.security.rationale}\n`;
2759
+ if (p.security.regulations.length > 0) {
2760
+ output += `- **Regulations:** ${p.security.regulations.join(', ')}\n`;
2761
+ }
2762
+ output += '\n';
2763
+ }
2764
+ if (prioritized.critical.length > limit) {
2765
+ output += `*... and ${prioritized.critical.length - limit} more critical items. Use \`limit=${limit + 10}\` to see more.*\n\n`;
2766
+ }
2767
+ }
2768
+ // High priority items - use limit parameter
2769
+ if (prioritized.high.length > 0) {
2770
+ const highLimit = Math.max(5, Math.floor(limit / 2));
2771
+ const limitedHigh = prioritized.high.slice(0, highLimit);
2772
+ output += '## โš ๏ธ High Priority Items (P2)\n\n';
2773
+ for (const p of limitedHigh) {
2774
+ output += `- **${p.accessPoint.table}**.${p.accessPoint.fields.join(', ') || '*'} (${p.accessPoint.operation})\n`;
2775
+ output += ` - ${p.accessPoint.file}:${p.accessPoint.line}\n`;
2776
+ }
2777
+ if (prioritized.high.length > highLimit) {
2778
+ output += `\n*... and ${prioritized.high.length - highLimit} more high priority items*\n`;
2779
+ }
2780
+ output += '\n';
2781
+ }
2782
+ // Sensitivity breakdown
2783
+ output += '## By Sensitivity Type\n\n';
2784
+ if (summary.bySensitivity.credentials > 0) {
2785
+ output += `- **๐Ÿ”‘ Credentials:** ${summary.bySensitivity.credentials}\n`;
2786
+ }
2787
+ if (summary.bySensitivity.financial > 0) {
2788
+ output += `- **๐Ÿ’ฐ Financial:** ${summary.bySensitivity.financial}\n`;
2789
+ }
2790
+ if (summary.bySensitivity.health > 0) {
2791
+ output += `- **๐Ÿฅ Health:** ${summary.bySensitivity.health}\n`;
2792
+ }
2793
+ if (summary.bySensitivity.pii > 0) {
2794
+ output += `- **๐Ÿ‘ค PII:** ${summary.bySensitivity.pii}\n`;
2795
+ }
2796
+ output += `- **โ“ Unknown:** ${summary.bySensitivity.unknown}\n`;
2797
+ return { content: [{ type: 'text', text: output }] };
2798
+ }
2799
+ catch (error) {
2800
+ return {
2801
+ content: [{ type: 'text', text: `Error: ${error}` }],
2802
+ isError: true,
2803
+ };
2804
+ }
2805
+ }
2806
+ case 'impact': {
2807
+ if (!args.target) {
2808
+ return {
2809
+ content: [{
2810
+ type: 'text',
2811
+ text: 'Error: target parameter required for "impact" action.\n\n' +
2812
+ 'Examples:\n' +
2813
+ '- `drift_callgraph action="impact" target="src/auth.py"` (analyze file)\n' +
2814
+ '- `drift_callgraph action="impact" target="login_user"` (analyze function)',
2815
+ }],
2816
+ isError: true,
2817
+ };
2818
+ }
2819
+ try {
2820
+ await analyzer.initialize();
2821
+ const graph = analyzer.getGraph();
2822
+ if (!graph) {
2823
+ return {
2824
+ content: [{ type: 'text', text: 'No call graph found. Run build first.' }],
2825
+ isError: true,
2826
+ };
2827
+ }
2828
+ const impactAnalyzer = createImpactAnalyzer(graph);
2829
+ let result;
2830
+ // Determine if target is a file or function
2831
+ if (args.target.includes('/') || args.target.includes('.py') || args.target.includes('.ts') || args.target.includes('.js')) {
2832
+ result = impactAnalyzer.analyzeFile(args.target);
2833
+ }
2834
+ else {
2835
+ result = impactAnalyzer.analyzeFunctionByName(args.target);
2836
+ }
2837
+ let output = '# ๐Ÿ’ฅ Impact Analysis\n\n';
2838
+ // Target info
2839
+ if (result.target.type === 'file') {
2840
+ output += `**Target:** ${result.target.file} (${result.changedFunctions.length} functions)\n\n`;
2841
+ }
2842
+ else {
2843
+ output += `**Target:** ${result.target.functionName ?? result.target.functionId ?? 'unknown'}\n\n`;
2844
+ }
2845
+ // Risk assessment
2846
+ const riskEmoji = result.risk === 'critical' ? '๐Ÿ”ด' :
2847
+ result.risk === 'high' ? '๐ŸŸ ' :
2848
+ result.risk === 'medium' ? '๐ŸŸก' : '๐ŸŸข';
2849
+ output += `**Risk Level:** ${riskEmoji} **${result.risk.toUpperCase()}** (score: ${result.riskScore}/100)\n\n`;
2850
+ // Summary
2851
+ output += '## Summary\n\n';
2852
+ output += `- **Direct Callers:** ${result.summary.directCallers}\n`;
2853
+ output += `- **Transitive Callers:** ${result.summary.transitiveCallers}\n`;
2854
+ output += `- **Affected Entry Points:** ${result.summary.affectedEntryPoints}\n`;
2855
+ output += `- **Sensitive Data Paths:** ${result.summary.affectedDataPaths}\n`;
2856
+ output += `- **Max Call Depth:** ${result.summary.maxDepth}\n\n`;
2857
+ // Entry points affected - use limit
2858
+ if (result.entryPoints.length > 0) {
2859
+ const limitedEntryPoints = result.entryPoints.slice(0, limit);
2860
+ output += '## ๐Ÿšช Affected Entry Points (User-Facing Impact)\n\n';
2861
+ for (const ep of limitedEntryPoints) {
2862
+ output += `### ${ep.qualifiedName}\n`;
2863
+ output += `- **Location:** ${ep.file}:${ep.line}\n`;
2864
+ output += `- **Path:** ${ep.pathToChange.map(p => p.functionName).join(' โ†’ ')}\n\n`;
2865
+ }
2866
+ if (result.entryPoints.length > limit) {
2867
+ output += `*... and ${result.entryPoints.length - limit} more entry points. Use \`limit=${limit + 10}\` to see more.*\n\n`;
2868
+ }
2869
+ }
2870
+ // Sensitive data paths - use limit
2871
+ if (result.sensitiveDataPaths.length > 0) {
2872
+ const limitedPaths = result.sensitiveDataPaths.slice(0, limit);
2873
+ output += '## ๐Ÿ”’ Sensitive Data Paths Affected\n\n';
2874
+ for (const dp of limitedPaths) {
2875
+ const sensitivityIcon = dp.sensitivity === 'credentials' ? '๐Ÿ”‘' :
2876
+ dp.sensitivity === 'financial' ? '๐Ÿ’ฐ' :
2877
+ dp.sensitivity === 'health' ? '๐Ÿฅ' : '๐Ÿ‘ค';
2878
+ output += `### ${sensitivityIcon} ${dp.sensitivity}: ${dp.table}.${dp.fields.join(', ')}\n`;
2879
+ output += `- **Operation:** ${dp.operation}\n`;
2880
+ output += `- **Entry Point:** ${dp.entryPoint}\n`;
2881
+ output += `- **Path:** ${dp.fullPath.map(n => n.functionName).join(' โ†’ ')}\n\n`;
2882
+ }
2883
+ if (result.sensitiveDataPaths.length > limit) {
2884
+ output += `*... and ${result.sensitiveDataPaths.length - limit} more sensitive paths*\n\n`;
2885
+ }
2886
+ }
2887
+ // Direct callers - use limit
2888
+ const directCallers = result.affected.filter(a => a.depth === 1);
2889
+ if (directCallers.length > 0) {
2890
+ const limitedDirect = directCallers.slice(0, limit);
2891
+ output += '## ๐Ÿ“ž Direct Callers (Immediate Impact)\n\n';
2892
+ for (const caller of limitedDirect) {
2893
+ const icon = caller.accessesSensitiveData ? '๐Ÿ”ด' : 'โšช';
2894
+ output += `- ${icon} **${caller.qualifiedName}** @ ${caller.file}:${caller.line}\n`;
2895
+ }
2896
+ if (directCallers.length > limit) {
2897
+ output += `\n*... and ${directCallers.length - limit} more direct callers*\n`;
2898
+ }
2899
+ output += '\n';
2900
+ }
2901
+ // Transitive callers - use smaller limit
2902
+ const transitiveCallers = result.affected.filter(a => a.depth > 1);
2903
+ if (transitiveCallers.length > 0) {
2904
+ const transitiveLimit = Math.max(5, Math.floor(limit / 2));
2905
+ const limitedTransitive = transitiveCallers.slice(0, transitiveLimit);
2906
+ output += '## ๐Ÿ”— Transitive Callers (Ripple Effect)\n\n';
2907
+ for (const caller of limitedTransitive) {
2908
+ output += `- [depth ${caller.depth}] **${caller.qualifiedName}**\n`;
2909
+ }
2910
+ if (transitiveCallers.length > transitiveLimit) {
2911
+ output += `\n*... and ${transitiveCallers.length - transitiveLimit} more transitive callers*\n`;
2912
+ }
2913
+ output += '\n';
2914
+ }
2915
+ // Recommendations
2916
+ if (result.risk === 'critical' || result.risk === 'high') {
2917
+ output += '## โš ๏ธ Recommendations\n\n';
2918
+ if (result.sensitiveDataPaths.length > 0) {
2919
+ output += '- Review all sensitive data paths before merging\n';
2920
+ }
2921
+ if (result.entryPoints.length > 5) {
2922
+ output += '- Consider incremental rollout - many entry points affected\n';
2923
+ }
2924
+ if (result.summary.maxDepth > 5) {
2925
+ output += '- Deep call chain - test thoroughly for regressions\n';
2926
+ }
2927
+ }
2928
+ return { content: [{ type: 'text', text: output }] };
2929
+ }
2930
+ catch (error) {
2931
+ return {
2932
+ content: [{ type: 'text', text: `Error: ${error}` }],
2933
+ isError: true,
2934
+ };
2935
+ }
2936
+ }
2937
+ case 'dead': {
2938
+ try {
2939
+ await analyzer.initialize();
2940
+ const graph = analyzer.getGraph();
2941
+ if (!graph) {
2942
+ return {
2943
+ content: [{ type: 'text', text: 'No call graph found. Run build first.' }],
2944
+ isError: true,
2945
+ };
2946
+ }
2947
+ const detector = createDeadCodeDetector(graph);
2948
+ const minConfidence = args.confidence ?? 'low';
2949
+ const result = detector.detect({ minConfidence });
2950
+ let output = '# ๐Ÿ’€ Dead Code Analysis\n\n';
2951
+ // Summary
2952
+ output += '## Summary\n\n';
2953
+ output += `- **Total Functions:** ${result.summary.totalFunctions}\n`;
2954
+ output += `- **Dead Candidates:** ${result.summary.deadCandidates}\n`;
2955
+ output += `- **High Confidence:** ${result.summary.highConfidence}\n`;
2956
+ output += `- **Medium Confidence:** ${result.summary.mediumConfidence}\n`;
2957
+ output += `- **Low Confidence:** ${result.summary.lowConfidence}\n`;
2958
+ output += `- **Estimated Dead Lines:** ~${result.summary.estimatedDeadLines.toLocaleString()}\n\n`;
2959
+ // Excluded
2960
+ output += '## Excluded from Analysis\n\n';
2961
+ output += `- Entry Points: ${result.excluded.entryPoints}\n`;
2962
+ output += `- Functions with Callers: ${result.excluded.withCallers}\n`;
2963
+ output += `- Framework Hooks: ${result.excluded.frameworkHooks}\n\n`;
2964
+ // By language
2965
+ if (Object.keys(result.summary.byLanguage).length > 0) {
2966
+ output += '## By Language\n\n';
2967
+ for (const [lang, count] of Object.entries(result.summary.byLanguage)) {
2968
+ output += `- **${lang}:** ${count}\n`;
2969
+ }
2970
+ output += '\n';
2971
+ }
2972
+ // High confidence candidates - use limit
2973
+ const highConf = result.candidates.filter(c => c.confidence === 'high');
2974
+ if (highConf.length > 0) {
2975
+ const limitedHigh = highConf.slice(0, limit);
2976
+ output += '## ๐Ÿ”ด High Confidence Dead Code\n\n';
2977
+ output += 'These functions are very likely unused:\n\n';
2978
+ for (const c of limitedHigh) {
2979
+ output += `### ${c.qualifiedName}\n`;
2980
+ output += `- **File:** ${c.file}:${c.line}\n`;
2981
+ output += `- **Lines:** ${c.linesOfCode}\n`;
2982
+ if (c.hasDataAccess) {
2983
+ output += `- โš ๏ธ Has data access\n`;
2984
+ }
2985
+ output += '\n';
2986
+ }
2987
+ if (highConf.length > limit) {
2988
+ output += `*... and ${highConf.length - limit} more high-confidence candidates. Use \`limit=${limit + 10}\` to see more.*\n\n`;
2989
+ }
2990
+ }
2991
+ // Medium confidence candidates - use smaller limit
2992
+ const medConf = result.candidates.filter(c => c.confidence === 'medium');
2993
+ if (medConf.length > 0) {
2994
+ const medLimit = Math.max(5, Math.floor(limit / 2));
2995
+ const limitedMed = medConf.slice(0, medLimit);
2996
+ output += '## ๐ŸŸก Medium Confidence Dead Code\n\n';
2997
+ output += 'These might be unused but have some indicators they could be called:\n\n';
2998
+ for (const c of limitedMed) {
2999
+ output += `- **${c.qualifiedName}** @ ${c.file}:${c.line}`;
3000
+ if (c.possibleFalsePositives.length > 0) {
3001
+ output += ` (${c.possibleFalsePositives.join(', ')})`;
3002
+ }
3003
+ output += '\n';
3004
+ }
3005
+ if (medConf.length > medLimit) {
3006
+ output += `\n*... and ${medConf.length - medLimit} more medium-confidence candidates*\n`;
3007
+ }
3008
+ output += '\n';
3009
+ }
3010
+ // Files with most dead code - use limit
3011
+ if (result.summary.byFile.length > 0) {
3012
+ const limitedFiles = result.summary.byFile.slice(0, limit);
3013
+ output += '## Files with Most Dead Code\n\n';
3014
+ for (const f of limitedFiles) {
3015
+ output += `- **${f.file}**: ${f.count} functions (~${f.lines} lines)\n`;
3016
+ }
3017
+ if (result.summary.byFile.length > limit) {
3018
+ output += `\n*... and ${result.summary.byFile.length - limit} more files*\n`;
3019
+ }
3020
+ output += '\n';
3021
+ }
3022
+ // Recommendations
3023
+ if (result.summary.highConfidence > 0) {
3024
+ output += '## ๐Ÿ’ก Recommendations\n\n';
3025
+ output += '1. Start with high-confidence candidates - they\'re safest to remove\n';
3026
+ output += '2. Search for dynamic calls (getattr, reflection) before removing\n';
3027
+ output += '3. Check if functions are called from tests\n';
3028
+ output += '4. Consider adding `# pragma: no cover` for intentionally unused code\n';
3029
+ }
3030
+ return { content: [{ type: 'text', text: output }] };
3031
+ }
3032
+ catch (error) {
3033
+ return {
3034
+ content: [{ type: 'text', text: `Error: ${error}` }],
3035
+ isError: true,
3036
+ };
3037
+ }
3038
+ }
3039
+ case 'coverage': {
3040
+ try {
3041
+ await analyzer.initialize();
3042
+ const graph = analyzer.getGraph();
3043
+ if (!graph) {
3044
+ return {
3045
+ content: [{ type: 'text', text: 'No call graph found. Run build first.' }],
3046
+ isError: true,
3047
+ };
3048
+ }
3049
+ const coverageAnalyzer = createCoverageAnalyzer(graph);
3050
+ const result = coverageAnalyzer.analyze();
3051
+ let output = '# ๐Ÿงช Sensitive Data Test Coverage\n\n';
3052
+ // Summary
3053
+ output += '## Summary\n\n';
3054
+ output += `- **Sensitive Fields:** ${result.summary.totalSensitiveFields}\n`;
3055
+ output += `- **Access Paths:** ${result.summary.totalAccessPaths}\n`;
3056
+ output += `- **Tested Paths:** ${result.summary.testedAccessPaths}\n`;
3057
+ output += `- **Coverage:** ${result.summary.coveragePercent}%\n`;
3058
+ output += `- **Test Files:** ${result.testFiles.length}\n`;
3059
+ output += `- **Test Functions:** ${result.testFunctions}\n\n`;
3060
+ // By sensitivity
3061
+ output += '## Coverage by Sensitivity\n\n';
3062
+ const sensOrder = ['credentials', 'financial', 'health', 'pii'];
3063
+ for (const sens of sensOrder) {
3064
+ const s = result.summary.bySensitivity[sens];
3065
+ if (s.fields > 0) {
3066
+ const icon = sens === 'credentials' ? '๐Ÿ”‘' :
3067
+ sens === 'financial' ? '๐Ÿ’ฐ' :
3068
+ sens === 'health' ? '๐Ÿฅ' : '๐Ÿ‘ค';
3069
+ const status = s.coveragePercent >= 80 ? 'โœ“' : s.coveragePercent >= 50 ? 'โ—' : 'โœ—';
3070
+ output += `- ${icon} **${sens}**: ${status} ${s.coveragePercent}% (${s.testedPaths}/${s.paths} paths)\n`;
3071
+ }
3072
+ }
3073
+ output += '\n';
3074
+ // Field coverage - use limit
3075
+ if (result.fields.length > 0) {
3076
+ const limitedFields = result.fields.slice(0, limit);
3077
+ output += '## Field Coverage\n\n';
3078
+ for (const f of limitedFields) {
3079
+ const statusIcon = f.status === 'covered' ? 'โœ“' :
3080
+ f.status === 'partial' ? 'โ—' : 'โœ—';
3081
+ const sensIcon = f.sensitivity === 'credentials' ? '๐Ÿ”‘' :
3082
+ f.sensitivity === 'financial' ? '๐Ÿ’ฐ' :
3083
+ f.sensitivity === 'health' ? '๐Ÿฅ' : '๐Ÿ‘ค';
3084
+ output += `- ${statusIcon} ${sensIcon} **${f.fullName}**: ${f.testedPaths}/${f.totalPaths} paths tested (${f.coveragePercent}%)\n`;
3085
+ }
3086
+ if (result.fields.length > limit) {
3087
+ output += `\n*... and ${result.fields.length - limit} more fields. Use \`limit=${limit + 10}\` to see more.*\n`;
3088
+ }
3089
+ output += '\n';
3090
+ }
3091
+ // Uncovered paths by priority - use smaller limits for each sensitivity type
3092
+ const pathLimit = Math.max(3, Math.floor(limit / 3));
3093
+ const uncoveredByCredentials = result.uncoveredPaths.filter((p) => p.sensitivity === 'credentials');
3094
+ const uncoveredByFinancial = result.uncoveredPaths.filter((p) => p.sensitivity === 'financial');
3095
+ const uncoveredByHealth = result.uncoveredPaths.filter((p) => p.sensitivity === 'health');
3096
+ const uncoveredByPii = result.uncoveredPaths.filter((p) => p.sensitivity === 'pii');
3097
+ if (uncoveredByCredentials.length > 0) {
3098
+ const limitedCreds = uncoveredByCredentials.slice(0, pathLimit);
3099
+ output += '## ๐Ÿ”‘ Untested Credential Access Paths\n\n';
3100
+ output += '**CRITICAL: These paths access credentials without test coverage**\n\n';
3101
+ for (const p of limitedCreds) {
3102
+ output += `### ${p.table}.${p.field}\n`;
3103
+ output += `- **Entry Point:** ${p.entryPoint.name} @ ${p.entryPoint.file}:${p.entryPoint.line}\n`;
3104
+ output += `- **Accessor:** ${p.accessor.name} @ ${p.accessor.file}:${p.accessor.line}\n`;
3105
+ output += `- **Depth:** ${p.depth}\n\n`;
3106
+ }
3107
+ if (uncoveredByCredentials.length > pathLimit) {
3108
+ output += `*... and ${uncoveredByCredentials.length - pathLimit} more credential paths*\n\n`;
3109
+ }
3110
+ }
3111
+ if (uncoveredByFinancial.length > 0) {
3112
+ const limitedFinancial = uncoveredByFinancial.slice(0, pathLimit);
3113
+ output += '## ๐Ÿ’ฐ Untested Financial Data Paths\n\n';
3114
+ for (const p of limitedFinancial) {
3115
+ output += `- **${p.table}.${p.field}**: ${p.entryPoint.name} โ†’ ${p.accessor.name}\n`;
3116
+ }
3117
+ if (uncoveredByFinancial.length > pathLimit) {
3118
+ output += `\n*... and ${uncoveredByFinancial.length - pathLimit} more*\n`;
3119
+ }
3120
+ output += '\n';
3121
+ }
3122
+ if (uncoveredByHealth.length > 0) {
3123
+ const limitedHealth = uncoveredByHealth.slice(0, pathLimit);
3124
+ output += '## ๐Ÿฅ Untested Health Data Paths\n\n';
3125
+ for (const p of limitedHealth) {
3126
+ output += `- **${p.table}.${p.field}**: ${p.entryPoint.name} โ†’ ${p.accessor.name}\n`;
3127
+ }
3128
+ if (uncoveredByHealth.length > pathLimit) {
3129
+ output += `\n*... and ${uncoveredByHealth.length - pathLimit} more*\n`;
3130
+ }
3131
+ output += '\n';
3132
+ }
3133
+ if (uncoveredByPii.length > 0) {
3134
+ const limitedPii = uncoveredByPii.slice(0, pathLimit);
3135
+ output += '## ๐Ÿ‘ค Untested PII Access Paths\n\n';
3136
+ for (const p of limitedPii) {
3137
+ output += `- **${p.table}.${p.field}**: ${p.entryPoint.name} โ†’ ${p.accessor.name}\n`;
3138
+ }
3139
+ if (uncoveredByPii.length > pathLimit) {
3140
+ output += `\n*... and ${uncoveredByPii.length - pathLimit} more*\n`;
3141
+ }
3142
+ output += '\n';
3143
+ }
3144
+ // Recommendations
3145
+ if (result.uncoveredPaths.length > 0) {
3146
+ output += '## ๐Ÿ’ก Recommendations\n\n';
3147
+ if (uncoveredByCredentials.length > 0) {
3148
+ output += `1. **CRITICAL:** ${uncoveredByCredentials.length} credential access paths need tests\n`;
3149
+ }
3150
+ if (uncoveredByFinancial.length > 0) {
3151
+ output += `2. ${uncoveredByFinancial.length} financial data paths need tests\n`;
3152
+ }
3153
+ if (result.summary.coveragePercent < 50) {
3154
+ output += `3. Overall coverage is ${result.summary.coveragePercent}% - consider adding integration tests\n`;
3155
+ }
3156
+ }
3157
+ else {
3158
+ output += '## โœ“ All Sensitive Data Paths Tested\n\n';
3159
+ output += 'Great job! All sensitive data access paths are covered by tests.\n';
3160
+ }
3161
+ return { content: [{ type: 'text', text: output }] };
3162
+ }
3163
+ catch (error) {
3164
+ return {
3165
+ content: [{ type: 'text', text: `Error: ${error}` }],
3166
+ isError: true,
3167
+ };
3168
+ }
3169
+ }
3170
+ default:
3171
+ return {
3172
+ content: [{ type: 'text', text: `Unknown action: ${action}` }],
3173
+ isError: true,
3174
+ };
3175
+ }
3176
+ }
2059
3177
  //# sourceMappingURL=server.js.map