mcp-vector-search 0.12.6__py3-none-any.whl → 1.1.22__py3-none-any.whl
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.
- mcp_vector_search/__init__.py +3 -3
- mcp_vector_search/analysis/__init__.py +111 -0
- mcp_vector_search/analysis/baseline/__init__.py +68 -0
- mcp_vector_search/analysis/baseline/comparator.py +462 -0
- mcp_vector_search/analysis/baseline/manager.py +621 -0
- mcp_vector_search/analysis/collectors/__init__.py +74 -0
- mcp_vector_search/analysis/collectors/base.py +164 -0
- mcp_vector_search/analysis/collectors/cohesion.py +463 -0
- mcp_vector_search/analysis/collectors/complexity.py +743 -0
- mcp_vector_search/analysis/collectors/coupling.py +1162 -0
- mcp_vector_search/analysis/collectors/halstead.py +514 -0
- mcp_vector_search/analysis/collectors/smells.py +325 -0
- mcp_vector_search/analysis/debt.py +516 -0
- mcp_vector_search/analysis/interpretation.py +685 -0
- mcp_vector_search/analysis/metrics.py +414 -0
- mcp_vector_search/analysis/reporters/__init__.py +7 -0
- mcp_vector_search/analysis/reporters/console.py +646 -0
- mcp_vector_search/analysis/reporters/markdown.py +480 -0
- mcp_vector_search/analysis/reporters/sarif.py +377 -0
- mcp_vector_search/analysis/storage/__init__.py +93 -0
- mcp_vector_search/analysis/storage/metrics_store.py +762 -0
- mcp_vector_search/analysis/storage/schema.py +245 -0
- mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
- mcp_vector_search/analysis/trends.py +308 -0
- mcp_vector_search/analysis/visualizer/__init__.py +90 -0
- mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
- mcp_vector_search/analysis/visualizer/exporter.py +484 -0
- mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
- mcp_vector_search/analysis/visualizer/schemas.py +525 -0
- mcp_vector_search/cli/commands/analyze.py +1062 -0
- mcp_vector_search/cli/commands/chat.py +1455 -0
- mcp_vector_search/cli/commands/index.py +621 -5
- mcp_vector_search/cli/commands/index_background.py +467 -0
- mcp_vector_search/cli/commands/init.py +13 -0
- mcp_vector_search/cli/commands/install.py +597 -335
- mcp_vector_search/cli/commands/install_old.py +8 -4
- mcp_vector_search/cli/commands/mcp.py +78 -6
- mcp_vector_search/cli/commands/reset.py +68 -26
- mcp_vector_search/cli/commands/search.py +224 -8
- mcp_vector_search/cli/commands/setup.py +1184 -0
- mcp_vector_search/cli/commands/status.py +339 -5
- mcp_vector_search/cli/commands/uninstall.py +276 -357
- mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
- mcp_vector_search/cli/commands/visualize/cli.py +292 -0
- mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
- mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/graph_builder.py +647 -0
- mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
- mcp_vector_search/cli/commands/visualize/server.py +600 -0
- mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
- mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
- mcp_vector_search/cli/commands/visualize/templates/base.py +234 -0
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +4542 -0
- mcp_vector_search/cli/commands/visualize/templates/styles.py +2522 -0
- mcp_vector_search/cli/didyoumean.py +27 -2
- mcp_vector_search/cli/main.py +127 -160
- mcp_vector_search/cli/output.py +158 -13
- mcp_vector_search/config/__init__.py +4 -0
- mcp_vector_search/config/default_thresholds.yaml +52 -0
- mcp_vector_search/config/settings.py +12 -0
- mcp_vector_search/config/thresholds.py +273 -0
- mcp_vector_search/core/__init__.py +16 -0
- mcp_vector_search/core/auto_indexer.py +3 -3
- mcp_vector_search/core/boilerplate.py +186 -0
- mcp_vector_search/core/config_utils.py +394 -0
- mcp_vector_search/core/database.py +406 -94
- mcp_vector_search/core/embeddings.py +24 -0
- mcp_vector_search/core/exceptions.py +11 -0
- mcp_vector_search/core/git.py +380 -0
- mcp_vector_search/core/git_hooks.py +4 -4
- mcp_vector_search/core/indexer.py +632 -54
- mcp_vector_search/core/llm_client.py +756 -0
- mcp_vector_search/core/models.py +91 -1
- mcp_vector_search/core/project.py +17 -0
- mcp_vector_search/core/relationships.py +473 -0
- mcp_vector_search/core/scheduler.py +11 -11
- mcp_vector_search/core/search.py +179 -29
- mcp_vector_search/mcp/server.py +819 -9
- mcp_vector_search/parsers/python.py +285 -5
- mcp_vector_search/utils/__init__.py +2 -0
- mcp_vector_search/utils/gitignore.py +0 -3
- mcp_vector_search/utils/gitignore_updater.py +212 -0
- mcp_vector_search/utils/monorepo.py +66 -4
- mcp_vector_search/utils/timing.py +10 -6
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +184 -53
- mcp_vector_search-1.1.22.dist-info/RECORD +120 -0
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +1 -1
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +1 -0
- mcp_vector_search/cli/commands/visualize.py +0 -1467
- mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
mcp_vector_search/mcp/server.py
CHANGED
|
@@ -18,6 +18,12 @@ from mcp.types import (
|
|
|
18
18
|
Tool,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
+
from ..analysis import (
|
|
22
|
+
ProjectMetrics,
|
|
23
|
+
SmellDetector,
|
|
24
|
+
SmellSeverity,
|
|
25
|
+
)
|
|
26
|
+
from ..config.thresholds import ThresholdConfig
|
|
21
27
|
from ..core.database import ChromaVectorDatabase
|
|
22
28
|
from ..core.embeddings import create_embedding_function
|
|
23
29
|
from ..core.exceptions import ProjectNotFoundError
|
|
@@ -25,6 +31,7 @@ from ..core.indexer import SemanticIndexer
|
|
|
25
31
|
from ..core.project import ProjectManager
|
|
26
32
|
from ..core.search import SemanticSearchEngine
|
|
27
33
|
from ..core.watcher import FileWatcher
|
|
34
|
+
from ..parsers.registry import ParserRegistry
|
|
28
35
|
|
|
29
36
|
|
|
30
37
|
class MCPVectorSearchServer:
|
|
@@ -38,11 +45,28 @@ class MCPVectorSearchServer:
|
|
|
38
45
|
"""Initialize the MCP server.
|
|
39
46
|
|
|
40
47
|
Args:
|
|
41
|
-
project_root: Project root directory. If None, will auto-detect
|
|
48
|
+
project_root: Project root directory. If None, will auto-detect from:
|
|
49
|
+
1. PROJECT_ROOT or MCP_PROJECT_ROOT environment variable
|
|
50
|
+
2. Current working directory
|
|
42
51
|
enable_file_watching: Enable file watching for automatic reindexing.
|
|
43
52
|
If None, checks MCP_ENABLE_FILE_WATCHING env var (default: True).
|
|
44
53
|
"""
|
|
45
|
-
|
|
54
|
+
# Auto-detect project root from environment or current directory
|
|
55
|
+
if project_root is None:
|
|
56
|
+
# Priority 1: MCP_PROJECT_ROOT (new standard)
|
|
57
|
+
# Priority 2: PROJECT_ROOT (legacy)
|
|
58
|
+
# Priority 3: Current working directory
|
|
59
|
+
env_project_root = os.getenv("MCP_PROJECT_ROOT") or os.getenv(
|
|
60
|
+
"PROJECT_ROOT"
|
|
61
|
+
)
|
|
62
|
+
if env_project_root:
|
|
63
|
+
project_root = Path(env_project_root).resolve()
|
|
64
|
+
logger.info(f"Using project root from environment: {project_root}")
|
|
65
|
+
else:
|
|
66
|
+
project_root = Path.cwd()
|
|
67
|
+
logger.info(f"Using current directory as project root: {project_root}")
|
|
68
|
+
|
|
69
|
+
self.project_root = project_root
|
|
46
70
|
self.project_manager = ProjectManager(self.project_root)
|
|
47
71
|
self.search_engine: SemanticSearchEngine | None = None
|
|
48
72
|
self.file_watcher: FileWatcher | None = None
|
|
@@ -142,7 +166,7 @@ class MCPVectorSearchServer:
|
|
|
142
166
|
tools = [
|
|
143
167
|
Tool(
|
|
144
168
|
name="search_code",
|
|
145
|
-
description="Search
|
|
169
|
+
description="Search codebase using natural language queries (text-to-code search). Use when you know what functionality you're looking for but not where it's implemented. Example: 'authentication middleware' or 'database connection pooling' to find relevant code.",
|
|
146
170
|
inputSchema={
|
|
147
171
|
"type": "object",
|
|
148
172
|
"properties": {
|
|
@@ -191,7 +215,7 @@ class MCPVectorSearchServer:
|
|
|
191
215
|
),
|
|
192
216
|
Tool(
|
|
193
217
|
name="search_similar",
|
|
194
|
-
description="Find code similar to a specific file or function",
|
|
218
|
+
description="Find code snippets similar to a specific file or function (code-to-code similarity). Use when looking for duplicate code, similar patterns, or related implementations. Example: 'Find functions similar to auth_handler.py' to discover related authentication code.",
|
|
195
219
|
inputSchema={
|
|
196
220
|
"type": "object",
|
|
197
221
|
"properties": {
|
|
@@ -223,7 +247,7 @@ class MCPVectorSearchServer:
|
|
|
223
247
|
),
|
|
224
248
|
Tool(
|
|
225
249
|
name="search_context",
|
|
226
|
-
description="Search for code
|
|
250
|
+
description="Search for code using rich contextual descriptions with optional focus areas. Use when you need broader context around specific concerns. Example: 'code handling user sessions' with focus_areas=['security', 'authentication'] to find session management with security emphasis.",
|
|
227
251
|
inputSchema={
|
|
228
252
|
"type": "object",
|
|
229
253
|
"properties": {
|
|
@@ -272,6 +296,116 @@ class MCPVectorSearchServer:
|
|
|
272
296
|
"required": [],
|
|
273
297
|
},
|
|
274
298
|
),
|
|
299
|
+
Tool(
|
|
300
|
+
name="analyze_project",
|
|
301
|
+
description="Returns project-wide metrics summary",
|
|
302
|
+
inputSchema={
|
|
303
|
+
"type": "object",
|
|
304
|
+
"properties": {
|
|
305
|
+
"threshold_preset": {
|
|
306
|
+
"type": "string",
|
|
307
|
+
"description": "Threshold preset: 'strict', 'standard', or 'relaxed'",
|
|
308
|
+
"enum": ["strict", "standard", "relaxed"],
|
|
309
|
+
"default": "standard",
|
|
310
|
+
},
|
|
311
|
+
"output_format": {
|
|
312
|
+
"type": "string",
|
|
313
|
+
"description": "Output format: 'summary' or 'detailed'",
|
|
314
|
+
"enum": ["summary", "detailed"],
|
|
315
|
+
"default": "summary",
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
"required": [],
|
|
319
|
+
},
|
|
320
|
+
),
|
|
321
|
+
Tool(
|
|
322
|
+
name="analyze_file",
|
|
323
|
+
description="Returns file-level metrics",
|
|
324
|
+
inputSchema={
|
|
325
|
+
"type": "object",
|
|
326
|
+
"properties": {
|
|
327
|
+
"file_path": {
|
|
328
|
+
"type": "string",
|
|
329
|
+
"description": "Path to the file to analyze (relative or absolute)",
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
"required": ["file_path"],
|
|
333
|
+
},
|
|
334
|
+
),
|
|
335
|
+
Tool(
|
|
336
|
+
name="find_smells",
|
|
337
|
+
description="Identify code quality issues, anti-patterns, bad practices, and technical debt. Detects Long Methods, Deep Nesting, Long Parameter Lists, God Classes, and Complex Methods. Use when assessing code quality, finding refactoring opportunities, or identifying maintainability issues.",
|
|
338
|
+
inputSchema={
|
|
339
|
+
"type": "object",
|
|
340
|
+
"properties": {
|
|
341
|
+
"smell_type": {
|
|
342
|
+
"type": "string",
|
|
343
|
+
"description": "Filter by smell type: 'Long Method', 'Deep Nesting', 'Long Parameter List', 'God Class', 'Complex Method'",
|
|
344
|
+
"enum": [
|
|
345
|
+
"Long Method",
|
|
346
|
+
"Deep Nesting",
|
|
347
|
+
"Long Parameter List",
|
|
348
|
+
"God Class",
|
|
349
|
+
"Complex Method",
|
|
350
|
+
],
|
|
351
|
+
},
|
|
352
|
+
"severity": {
|
|
353
|
+
"type": "string",
|
|
354
|
+
"description": "Filter by severity level",
|
|
355
|
+
"enum": ["info", "warning", "error"],
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
"required": [],
|
|
359
|
+
},
|
|
360
|
+
),
|
|
361
|
+
Tool(
|
|
362
|
+
name="get_complexity_hotspots",
|
|
363
|
+
description="Returns top N most complex functions",
|
|
364
|
+
inputSchema={
|
|
365
|
+
"type": "object",
|
|
366
|
+
"properties": {
|
|
367
|
+
"limit": {
|
|
368
|
+
"type": "integer",
|
|
369
|
+
"description": "Maximum number of hotspots to return",
|
|
370
|
+
"default": 10,
|
|
371
|
+
"minimum": 1,
|
|
372
|
+
"maximum": 50,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
"required": [],
|
|
376
|
+
},
|
|
377
|
+
),
|
|
378
|
+
Tool(
|
|
379
|
+
name="check_circular_dependencies",
|
|
380
|
+
description="Returns circular dependency cycles",
|
|
381
|
+
inputSchema={"type": "object", "properties": {}, "required": []},
|
|
382
|
+
),
|
|
383
|
+
Tool(
|
|
384
|
+
name="interpret_analysis",
|
|
385
|
+
description="Interpret analysis results with natural language explanations and recommendations",
|
|
386
|
+
inputSchema={
|
|
387
|
+
"type": "object",
|
|
388
|
+
"properties": {
|
|
389
|
+
"analysis_json": {
|
|
390
|
+
"type": "string",
|
|
391
|
+
"description": "JSON string from analyze command with --include-context",
|
|
392
|
+
},
|
|
393
|
+
"focus": {
|
|
394
|
+
"type": "string",
|
|
395
|
+
"description": "Focus area: 'summary', 'recommendations', or 'priorities'",
|
|
396
|
+
"enum": ["summary", "recommendations", "priorities"],
|
|
397
|
+
"default": "summary",
|
|
398
|
+
},
|
|
399
|
+
"verbosity": {
|
|
400
|
+
"type": "string",
|
|
401
|
+
"description": "Verbosity level: 'brief', 'normal', or 'detailed'",
|
|
402
|
+
"enum": ["brief", "normal", "detailed"],
|
|
403
|
+
"default": "normal",
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
"required": ["analysis_json"],
|
|
407
|
+
},
|
|
408
|
+
),
|
|
275
409
|
]
|
|
276
410
|
|
|
277
411
|
return tools
|
|
@@ -282,7 +416,8 @@ class MCPVectorSearchServer:
|
|
|
282
416
|
|
|
283
417
|
async def call_tool(self, request: CallToolRequest) -> CallToolResult:
|
|
284
418
|
"""Handle tool calls."""
|
|
285
|
-
|
|
419
|
+
# Skip initialization for interpret_analysis (doesn't need project config)
|
|
420
|
+
if request.params.name != "interpret_analysis" and not self._initialized:
|
|
286
421
|
await self.initialize()
|
|
287
422
|
|
|
288
423
|
try:
|
|
@@ -296,6 +431,18 @@ class MCPVectorSearchServer:
|
|
|
296
431
|
return await self._get_project_status(request.params.arguments)
|
|
297
432
|
elif request.params.name == "index_project":
|
|
298
433
|
return await self._index_project(request.params.arguments)
|
|
434
|
+
elif request.params.name == "analyze_project":
|
|
435
|
+
return await self._analyze_project(request.params.arguments)
|
|
436
|
+
elif request.params.name == "analyze_file":
|
|
437
|
+
return await self._analyze_file(request.params.arguments)
|
|
438
|
+
elif request.params.name == "find_smells":
|
|
439
|
+
return await self._find_smells(request.params.arguments)
|
|
440
|
+
elif request.params.name == "get_complexity_hotspots":
|
|
441
|
+
return await self._get_complexity_hotspots(request.params.arguments)
|
|
442
|
+
elif request.params.name == "check_circular_dependencies":
|
|
443
|
+
return await self._check_circular_dependencies(request.params.arguments)
|
|
444
|
+
elif request.params.name == "interpret_analysis":
|
|
445
|
+
return await self._interpret_analysis(request.params.arguments)
|
|
299
446
|
else:
|
|
300
447
|
return CallToolResult(
|
|
301
448
|
content=[
|
|
@@ -397,9 +544,11 @@ class MCPVectorSearchServer:
|
|
|
397
544
|
"languages": config.languages,
|
|
398
545
|
"total_chunks": stats.total_chunks,
|
|
399
546
|
"total_files": stats.total_files,
|
|
400
|
-
"index_size":
|
|
401
|
-
|
|
402
|
-
|
|
547
|
+
"index_size": (
|
|
548
|
+
f"{stats.index_size_mb:.2f} MB"
|
|
549
|
+
if hasattr(stats, "index_size_mb")
|
|
550
|
+
else "Unknown"
|
|
551
|
+
),
|
|
403
552
|
}
|
|
404
553
|
else:
|
|
405
554
|
status_info = {
|
|
@@ -642,6 +791,667 @@ class MCPVectorSearchServer:
|
|
|
642
791
|
isError=True,
|
|
643
792
|
)
|
|
644
793
|
|
|
794
|
+
async def _analyze_project(self, args: dict[str, Any]) -> CallToolResult:
|
|
795
|
+
"""Handle analyze_project tool call."""
|
|
796
|
+
threshold_preset = args.get("threshold_preset", "standard")
|
|
797
|
+
output_format = args.get("output_format", "summary")
|
|
798
|
+
|
|
799
|
+
try:
|
|
800
|
+
# Load threshold configuration based on preset
|
|
801
|
+
threshold_config = self._get_threshold_config(threshold_preset)
|
|
802
|
+
|
|
803
|
+
# Run analysis using CLI analyze logic
|
|
804
|
+
from ..cli.commands.analyze import _analyze_file, _find_analyzable_files
|
|
805
|
+
|
|
806
|
+
parser_registry = ParserRegistry()
|
|
807
|
+
files_to_analyze = _find_analyzable_files(
|
|
808
|
+
self.project_root, None, None, parser_registry, None
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
if not files_to_analyze:
|
|
812
|
+
return CallToolResult(
|
|
813
|
+
content=[
|
|
814
|
+
TextContent(
|
|
815
|
+
type="text",
|
|
816
|
+
text="No analyzable files found in project",
|
|
817
|
+
)
|
|
818
|
+
],
|
|
819
|
+
isError=True,
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
# Analyze files
|
|
823
|
+
from ..analysis import (
|
|
824
|
+
CognitiveComplexityCollector,
|
|
825
|
+
CyclomaticComplexityCollector,
|
|
826
|
+
MethodCountCollector,
|
|
827
|
+
NestingDepthCollector,
|
|
828
|
+
ParameterCountCollector,
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
collectors = [
|
|
832
|
+
CognitiveComplexityCollector(),
|
|
833
|
+
CyclomaticComplexityCollector(),
|
|
834
|
+
NestingDepthCollector(),
|
|
835
|
+
ParameterCountCollector(),
|
|
836
|
+
MethodCountCollector(),
|
|
837
|
+
]
|
|
838
|
+
|
|
839
|
+
project_metrics = ProjectMetrics(project_root=str(self.project_root))
|
|
840
|
+
|
|
841
|
+
for file_path in files_to_analyze:
|
|
842
|
+
try:
|
|
843
|
+
file_metrics = await _analyze_file(
|
|
844
|
+
file_path, parser_registry, collectors
|
|
845
|
+
)
|
|
846
|
+
if file_metrics and file_metrics.chunks:
|
|
847
|
+
project_metrics.files[str(file_path)] = file_metrics
|
|
848
|
+
except Exception as e:
|
|
849
|
+
logger.debug(f"Failed to analyze {file_path}: {e}")
|
|
850
|
+
continue
|
|
851
|
+
|
|
852
|
+
project_metrics.compute_aggregates()
|
|
853
|
+
|
|
854
|
+
# Detect code smells
|
|
855
|
+
smell_detector = SmellDetector(thresholds=threshold_config)
|
|
856
|
+
all_smells = []
|
|
857
|
+
for file_path, file_metrics in project_metrics.files.items():
|
|
858
|
+
file_smells = smell_detector.detect_all(file_metrics, file_path)
|
|
859
|
+
all_smells.extend(file_smells)
|
|
860
|
+
|
|
861
|
+
# Format response
|
|
862
|
+
if output_format == "detailed":
|
|
863
|
+
# Return full JSON output
|
|
864
|
+
import json
|
|
865
|
+
|
|
866
|
+
output = project_metrics.to_summary()
|
|
867
|
+
output["smells"] = {
|
|
868
|
+
"total": len(all_smells),
|
|
869
|
+
"by_severity": {
|
|
870
|
+
"error": sum(
|
|
871
|
+
1 for s in all_smells if s.severity == SmellSeverity.ERROR
|
|
872
|
+
),
|
|
873
|
+
"warning": sum(
|
|
874
|
+
1 for s in all_smells if s.severity == SmellSeverity.WARNING
|
|
875
|
+
),
|
|
876
|
+
"info": sum(
|
|
877
|
+
1 for s in all_smells if s.severity == SmellSeverity.INFO
|
|
878
|
+
),
|
|
879
|
+
},
|
|
880
|
+
}
|
|
881
|
+
response_text = json.dumps(output, indent=2)
|
|
882
|
+
else:
|
|
883
|
+
# Return summary
|
|
884
|
+
summary = project_metrics.to_summary()
|
|
885
|
+
response_lines = [
|
|
886
|
+
"# Project Analysis Summary\n",
|
|
887
|
+
f"**Project Root:** {summary['project_root']}",
|
|
888
|
+
f"**Total Files:** {summary['total_files']}",
|
|
889
|
+
f"**Total Functions:** {summary['total_functions']}",
|
|
890
|
+
f"**Total Classes:** {summary['total_classes']}",
|
|
891
|
+
f"**Average File Complexity:** {summary['avg_file_complexity']}\n",
|
|
892
|
+
"## Complexity Distribution",
|
|
893
|
+
]
|
|
894
|
+
|
|
895
|
+
dist = summary["complexity_distribution"]
|
|
896
|
+
for grade in ["A", "B", "C", "D", "F"]:
|
|
897
|
+
response_lines.append(f"- Grade {grade}: {dist[grade]} chunks")
|
|
898
|
+
|
|
899
|
+
response_lines.extend(
|
|
900
|
+
[
|
|
901
|
+
"\n## Health Metrics",
|
|
902
|
+
f"- Average Health Score: {summary['health_metrics']['avg_health_score']:.2f}",
|
|
903
|
+
f"- Files Needing Attention: {summary['health_metrics']['files_needing_attention']}",
|
|
904
|
+
"\n## Code Smells",
|
|
905
|
+
f"- Total: {len(all_smells)}",
|
|
906
|
+
f"- Errors: {sum(1 for s in all_smells if s.severity == SmellSeverity.ERROR)}",
|
|
907
|
+
f"- Warnings: {sum(1 for s in all_smells if s.severity == SmellSeverity.WARNING)}",
|
|
908
|
+
f"- Info: {sum(1 for s in all_smells if s.severity == SmellSeverity.INFO)}",
|
|
909
|
+
]
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
response_text = "\n".join(response_lines)
|
|
913
|
+
|
|
914
|
+
return CallToolResult(
|
|
915
|
+
content=[TextContent(type="text", text=response_text)]
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
except Exception as e:
|
|
919
|
+
logger.error(f"Project analysis failed: {e}")
|
|
920
|
+
return CallToolResult(
|
|
921
|
+
content=[
|
|
922
|
+
TextContent(type="text", text=f"Project analysis failed: {str(e)}")
|
|
923
|
+
],
|
|
924
|
+
isError=True,
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
async def _analyze_file(self, args: dict[str, Any]) -> CallToolResult:
|
|
928
|
+
"""Handle analyze_file tool call."""
|
|
929
|
+
file_path_str = args.get("file_path", "")
|
|
930
|
+
|
|
931
|
+
if not file_path_str:
|
|
932
|
+
return CallToolResult(
|
|
933
|
+
content=[
|
|
934
|
+
TextContent(type="text", text="file_path parameter is required")
|
|
935
|
+
],
|
|
936
|
+
isError=True,
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
try:
|
|
940
|
+
file_path = Path(file_path_str)
|
|
941
|
+
if not file_path.is_absolute():
|
|
942
|
+
file_path = self.project_root / file_path
|
|
943
|
+
|
|
944
|
+
if not file_path.exists():
|
|
945
|
+
return CallToolResult(
|
|
946
|
+
content=[
|
|
947
|
+
TextContent(
|
|
948
|
+
type="text", text=f"File not found: {file_path_str}"
|
|
949
|
+
)
|
|
950
|
+
],
|
|
951
|
+
isError=True,
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
# Analyze single file
|
|
955
|
+
from ..analysis import (
|
|
956
|
+
CognitiveComplexityCollector,
|
|
957
|
+
CyclomaticComplexityCollector,
|
|
958
|
+
MethodCountCollector,
|
|
959
|
+
NestingDepthCollector,
|
|
960
|
+
ParameterCountCollector,
|
|
961
|
+
)
|
|
962
|
+
from ..cli.commands.analyze import _analyze_file
|
|
963
|
+
|
|
964
|
+
parser_registry = ParserRegistry()
|
|
965
|
+
collectors = [
|
|
966
|
+
CognitiveComplexityCollector(),
|
|
967
|
+
CyclomaticComplexityCollector(),
|
|
968
|
+
NestingDepthCollector(),
|
|
969
|
+
ParameterCountCollector(),
|
|
970
|
+
MethodCountCollector(),
|
|
971
|
+
]
|
|
972
|
+
|
|
973
|
+
file_metrics = await _analyze_file(file_path, parser_registry, collectors)
|
|
974
|
+
|
|
975
|
+
if not file_metrics:
|
|
976
|
+
return CallToolResult(
|
|
977
|
+
content=[
|
|
978
|
+
TextContent(
|
|
979
|
+
type="text",
|
|
980
|
+
text=f"Unable to analyze file: {file_path_str}",
|
|
981
|
+
)
|
|
982
|
+
],
|
|
983
|
+
isError=True,
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
# Detect smells
|
|
987
|
+
smell_detector = SmellDetector()
|
|
988
|
+
smells = smell_detector.detect_all(file_metrics, str(file_path))
|
|
989
|
+
|
|
990
|
+
# Format response
|
|
991
|
+
response_lines = [
|
|
992
|
+
f"# File Analysis: {file_path.name}\n",
|
|
993
|
+
f"**Path:** {file_path}",
|
|
994
|
+
f"**Total Lines:** {file_metrics.total_lines}",
|
|
995
|
+
f"**Code Lines:** {file_metrics.code_lines}",
|
|
996
|
+
f"**Comment Lines:** {file_metrics.comment_lines}",
|
|
997
|
+
f"**Functions:** {file_metrics.function_count}",
|
|
998
|
+
f"**Classes:** {file_metrics.class_count}",
|
|
999
|
+
f"**Methods:** {file_metrics.method_count}\n",
|
|
1000
|
+
"## Complexity Metrics",
|
|
1001
|
+
f"- Total Complexity: {file_metrics.total_complexity}",
|
|
1002
|
+
f"- Average Complexity: {file_metrics.avg_complexity:.2f}",
|
|
1003
|
+
f"- Max Complexity: {file_metrics.max_complexity}",
|
|
1004
|
+
f"- Health Score: {file_metrics.health_score:.2f}\n",
|
|
1005
|
+
]
|
|
1006
|
+
|
|
1007
|
+
if smells:
|
|
1008
|
+
response_lines.append(f"## Code Smells ({len(smells)})\n")
|
|
1009
|
+
for smell in smells[:10]: # Show top 10
|
|
1010
|
+
response_lines.append(
|
|
1011
|
+
f"- [{smell.severity.value.upper()}] {smell.name}: {smell.description}"
|
|
1012
|
+
)
|
|
1013
|
+
if len(smells) > 10:
|
|
1014
|
+
response_lines.append(f"\n... and {len(smells) - 10} more")
|
|
1015
|
+
else:
|
|
1016
|
+
response_lines.append("## Code Smells\n- None detected")
|
|
1017
|
+
|
|
1018
|
+
response_text = "\n".join(response_lines)
|
|
1019
|
+
|
|
1020
|
+
return CallToolResult(
|
|
1021
|
+
content=[TextContent(type="text", text=response_text)]
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
except Exception as e:
|
|
1025
|
+
logger.error(f"File analysis failed: {e}")
|
|
1026
|
+
return CallToolResult(
|
|
1027
|
+
content=[
|
|
1028
|
+
TextContent(type="text", text=f"File analysis failed: {str(e)}")
|
|
1029
|
+
],
|
|
1030
|
+
isError=True,
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
async def _find_smells(self, args: dict[str, Any]) -> CallToolResult:
|
|
1034
|
+
"""Handle find_smells tool call."""
|
|
1035
|
+
smell_type_filter = args.get("smell_type")
|
|
1036
|
+
severity_filter = args.get("severity")
|
|
1037
|
+
|
|
1038
|
+
try:
|
|
1039
|
+
# Run full project analysis
|
|
1040
|
+
from ..analysis import (
|
|
1041
|
+
CognitiveComplexityCollector,
|
|
1042
|
+
CyclomaticComplexityCollector,
|
|
1043
|
+
MethodCountCollector,
|
|
1044
|
+
NestingDepthCollector,
|
|
1045
|
+
ParameterCountCollector,
|
|
1046
|
+
)
|
|
1047
|
+
from ..cli.commands.analyze import _analyze_file, _find_analyzable_files
|
|
1048
|
+
|
|
1049
|
+
parser_registry = ParserRegistry()
|
|
1050
|
+
files_to_analyze = _find_analyzable_files(
|
|
1051
|
+
self.project_root, None, None, parser_registry, None
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
collectors = [
|
|
1055
|
+
CognitiveComplexityCollector(),
|
|
1056
|
+
CyclomaticComplexityCollector(),
|
|
1057
|
+
NestingDepthCollector(),
|
|
1058
|
+
ParameterCountCollector(),
|
|
1059
|
+
MethodCountCollector(),
|
|
1060
|
+
]
|
|
1061
|
+
|
|
1062
|
+
project_metrics = ProjectMetrics(project_root=str(self.project_root))
|
|
1063
|
+
|
|
1064
|
+
for file_path in files_to_analyze:
|
|
1065
|
+
try:
|
|
1066
|
+
file_metrics = await _analyze_file(
|
|
1067
|
+
file_path, parser_registry, collectors
|
|
1068
|
+
)
|
|
1069
|
+
if file_metrics and file_metrics.chunks:
|
|
1070
|
+
project_metrics.files[str(file_path)] = file_metrics
|
|
1071
|
+
except Exception: # nosec B112 - intentional skip of unparseable files
|
|
1072
|
+
continue
|
|
1073
|
+
|
|
1074
|
+
# Detect all smells
|
|
1075
|
+
smell_detector = SmellDetector()
|
|
1076
|
+
all_smells = []
|
|
1077
|
+
for file_path, file_metrics in project_metrics.files.items():
|
|
1078
|
+
file_smells = smell_detector.detect_all(file_metrics, file_path)
|
|
1079
|
+
all_smells.extend(file_smells)
|
|
1080
|
+
|
|
1081
|
+
# Apply filters
|
|
1082
|
+
filtered_smells = all_smells
|
|
1083
|
+
|
|
1084
|
+
if smell_type_filter:
|
|
1085
|
+
filtered_smells = [
|
|
1086
|
+
s for s in filtered_smells if s.name == smell_type_filter
|
|
1087
|
+
]
|
|
1088
|
+
|
|
1089
|
+
if severity_filter:
|
|
1090
|
+
severity_enum = SmellSeverity(severity_filter)
|
|
1091
|
+
filtered_smells = [
|
|
1092
|
+
s for s in filtered_smells if s.severity == severity_enum
|
|
1093
|
+
]
|
|
1094
|
+
|
|
1095
|
+
# Format response
|
|
1096
|
+
if not filtered_smells:
|
|
1097
|
+
filter_desc = []
|
|
1098
|
+
if smell_type_filter:
|
|
1099
|
+
filter_desc.append(f"type={smell_type_filter}")
|
|
1100
|
+
if severity_filter:
|
|
1101
|
+
filter_desc.append(f"severity={severity_filter}")
|
|
1102
|
+
filter_str = f" ({', '.join(filter_desc)})" if filter_desc else ""
|
|
1103
|
+
response_text = f"No code smells found{filter_str}"
|
|
1104
|
+
else:
|
|
1105
|
+
response_lines = [f"# Code Smells Found: {len(filtered_smells)}\n"]
|
|
1106
|
+
|
|
1107
|
+
# Group by severity
|
|
1108
|
+
by_severity = {
|
|
1109
|
+
"error": [
|
|
1110
|
+
s for s in filtered_smells if s.severity == SmellSeverity.ERROR
|
|
1111
|
+
],
|
|
1112
|
+
"warning": [
|
|
1113
|
+
s
|
|
1114
|
+
for s in filtered_smells
|
|
1115
|
+
if s.severity == SmellSeverity.WARNING
|
|
1116
|
+
],
|
|
1117
|
+
"info": [
|
|
1118
|
+
s for s in filtered_smells if s.severity == SmellSeverity.INFO
|
|
1119
|
+
],
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
for severity_level in ["error", "warning", "info"]:
|
|
1123
|
+
smells = by_severity[severity_level]
|
|
1124
|
+
if smells:
|
|
1125
|
+
response_lines.append(
|
|
1126
|
+
f"## {severity_level.upper()} ({len(smells)})\n"
|
|
1127
|
+
)
|
|
1128
|
+
for smell in smells[:20]: # Show top 20 per severity
|
|
1129
|
+
response_lines.append(
|
|
1130
|
+
f"- **{smell.name}** at `{smell.location}`"
|
|
1131
|
+
)
|
|
1132
|
+
response_lines.append(f" {smell.description}")
|
|
1133
|
+
if smell.suggestion:
|
|
1134
|
+
response_lines.append(
|
|
1135
|
+
f" *Suggestion: {smell.suggestion}*"
|
|
1136
|
+
)
|
|
1137
|
+
response_lines.append("")
|
|
1138
|
+
|
|
1139
|
+
response_text = "\n".join(response_lines)
|
|
1140
|
+
|
|
1141
|
+
return CallToolResult(
|
|
1142
|
+
content=[TextContent(type="text", text=response_text)]
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
except Exception as e:
|
|
1146
|
+
logger.error(f"Smell detection failed: {e}")
|
|
1147
|
+
return CallToolResult(
|
|
1148
|
+
content=[
|
|
1149
|
+
TextContent(type="text", text=f"Smell detection failed: {str(e)}")
|
|
1150
|
+
],
|
|
1151
|
+
isError=True,
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
async def _get_complexity_hotspots(self, args: dict[str, Any]) -> CallToolResult:
|
|
1155
|
+
"""Handle get_complexity_hotspots tool call."""
|
|
1156
|
+
limit = args.get("limit", 10)
|
|
1157
|
+
|
|
1158
|
+
try:
|
|
1159
|
+
# Run full project analysis
|
|
1160
|
+
from ..analysis import (
|
|
1161
|
+
CognitiveComplexityCollector,
|
|
1162
|
+
CyclomaticComplexityCollector,
|
|
1163
|
+
MethodCountCollector,
|
|
1164
|
+
NestingDepthCollector,
|
|
1165
|
+
ParameterCountCollector,
|
|
1166
|
+
)
|
|
1167
|
+
from ..cli.commands.analyze import _analyze_file, _find_analyzable_files
|
|
1168
|
+
|
|
1169
|
+
parser_registry = ParserRegistry()
|
|
1170
|
+
files_to_analyze = _find_analyzable_files(
|
|
1171
|
+
self.project_root, None, None, parser_registry, None
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
collectors = [
|
|
1175
|
+
CognitiveComplexityCollector(),
|
|
1176
|
+
CyclomaticComplexityCollector(),
|
|
1177
|
+
NestingDepthCollector(),
|
|
1178
|
+
ParameterCountCollector(),
|
|
1179
|
+
MethodCountCollector(),
|
|
1180
|
+
]
|
|
1181
|
+
|
|
1182
|
+
project_metrics = ProjectMetrics(project_root=str(self.project_root))
|
|
1183
|
+
|
|
1184
|
+
for file_path in files_to_analyze:
|
|
1185
|
+
try:
|
|
1186
|
+
file_metrics = await _analyze_file(
|
|
1187
|
+
file_path, parser_registry, collectors
|
|
1188
|
+
)
|
|
1189
|
+
if file_metrics and file_metrics.chunks:
|
|
1190
|
+
project_metrics.files[str(file_path)] = file_metrics
|
|
1191
|
+
except Exception: # nosec B112 - intentional skip of unparseable files
|
|
1192
|
+
continue
|
|
1193
|
+
|
|
1194
|
+
# Get top N complex files
|
|
1195
|
+
hotspots = project_metrics.get_hotspots(limit=limit)
|
|
1196
|
+
|
|
1197
|
+
# Format response
|
|
1198
|
+
if not hotspots:
|
|
1199
|
+
response_text = "No complexity hotspots found"
|
|
1200
|
+
else:
|
|
1201
|
+
response_lines = [f"# Top {len(hotspots)} Complexity Hotspots\n"]
|
|
1202
|
+
|
|
1203
|
+
for i, file_metrics in enumerate(hotspots, 1):
|
|
1204
|
+
response_lines.extend(
|
|
1205
|
+
[
|
|
1206
|
+
f"## {i}. {Path(file_metrics.file_path).name}",
|
|
1207
|
+
f"**Path:** `{file_metrics.file_path}`",
|
|
1208
|
+
f"**Average Complexity:** {file_metrics.avg_complexity:.2f}",
|
|
1209
|
+
f"**Max Complexity:** {file_metrics.max_complexity}",
|
|
1210
|
+
f"**Total Complexity:** {file_metrics.total_complexity}",
|
|
1211
|
+
f"**Functions:** {file_metrics.function_count}",
|
|
1212
|
+
f"**Health Score:** {file_metrics.health_score:.2f}\n",
|
|
1213
|
+
]
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
response_text = "\n".join(response_lines)
|
|
1217
|
+
|
|
1218
|
+
return CallToolResult(
|
|
1219
|
+
content=[TextContent(type="text", text=response_text)]
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
except Exception as e:
|
|
1223
|
+
logger.error(f"Hotspot detection failed: {e}")
|
|
1224
|
+
return CallToolResult(
|
|
1225
|
+
content=[
|
|
1226
|
+
TextContent(type="text", text=f"Hotspot detection failed: {str(e)}")
|
|
1227
|
+
],
|
|
1228
|
+
isError=True,
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
async def _check_circular_dependencies(
|
|
1232
|
+
self, args: dict[str, Any]
|
|
1233
|
+
) -> CallToolResult:
|
|
1234
|
+
"""Handle check_circular_dependencies tool call."""
|
|
1235
|
+
try:
|
|
1236
|
+
# Find analyzable files to build import graph
|
|
1237
|
+
from ..cli.commands.analyze import _find_analyzable_files
|
|
1238
|
+
|
|
1239
|
+
parser_registry = ParserRegistry()
|
|
1240
|
+
files_to_analyze = _find_analyzable_files(
|
|
1241
|
+
self.project_root, None, None, parser_registry, None
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
if not files_to_analyze:
|
|
1245
|
+
return CallToolResult(
|
|
1246
|
+
content=[
|
|
1247
|
+
TextContent(
|
|
1248
|
+
type="text",
|
|
1249
|
+
text="No analyzable files found in project",
|
|
1250
|
+
)
|
|
1251
|
+
],
|
|
1252
|
+
isError=True,
|
|
1253
|
+
)
|
|
1254
|
+
|
|
1255
|
+
# Import circular dependency detection
|
|
1256
|
+
from ..analysis.collectors.coupling import build_import_graph
|
|
1257
|
+
|
|
1258
|
+
# Build import graph for the project (reverse dependency graph)
|
|
1259
|
+
import_graph = build_import_graph(
|
|
1260
|
+
self.project_root, files_to_analyze, language="python"
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
# Convert to forward dependency graph for cycle detection
|
|
1264
|
+
# import_graph maps: module -> set of files that import it (reverse)
|
|
1265
|
+
# We need: file -> list of files it imports (forward)
|
|
1266
|
+
forward_graph: dict[str, list[str]] = {}
|
|
1267
|
+
|
|
1268
|
+
# Build forward graph by reading imports from files
|
|
1269
|
+
for file_path in files_to_analyze:
|
|
1270
|
+
file_str = str(file_path.relative_to(self.project_root))
|
|
1271
|
+
if file_str not in forward_graph:
|
|
1272
|
+
forward_graph[file_str] = []
|
|
1273
|
+
|
|
1274
|
+
# For each module in import_graph, if this file imports it, add edge
|
|
1275
|
+
for module, importers in import_graph.items():
|
|
1276
|
+
for importer in importers:
|
|
1277
|
+
importer_str = str(
|
|
1278
|
+
Path(importer).relative_to(self.project_root)
|
|
1279
|
+
if Path(importer).is_absolute()
|
|
1280
|
+
else importer
|
|
1281
|
+
)
|
|
1282
|
+
if importer_str == file_str:
|
|
1283
|
+
# This file imports the module, add forward edge
|
|
1284
|
+
if module not in forward_graph[file_str]:
|
|
1285
|
+
forward_graph[file_str].append(module)
|
|
1286
|
+
|
|
1287
|
+
# Detect circular dependencies using DFS
|
|
1288
|
+
def find_cycles(graph: dict[str, list[str]]) -> list[list[str]]:
|
|
1289
|
+
"""Find all cycles in the import graph using DFS."""
|
|
1290
|
+
cycles = []
|
|
1291
|
+
visited = set()
|
|
1292
|
+
rec_stack = set()
|
|
1293
|
+
|
|
1294
|
+
def dfs(node: str, path: list[str]) -> None:
|
|
1295
|
+
visited.add(node)
|
|
1296
|
+
rec_stack.add(node)
|
|
1297
|
+
path.append(node)
|
|
1298
|
+
|
|
1299
|
+
for neighbor in graph.get(node, []):
|
|
1300
|
+
if neighbor not in visited:
|
|
1301
|
+
dfs(neighbor, path.copy())
|
|
1302
|
+
elif neighbor in rec_stack:
|
|
1303
|
+
# Found a cycle
|
|
1304
|
+
try:
|
|
1305
|
+
cycle_start = path.index(neighbor)
|
|
1306
|
+
cycle = path[cycle_start:] + [neighbor]
|
|
1307
|
+
# Normalize cycle representation to avoid duplicates
|
|
1308
|
+
cycle_tuple = tuple(sorted(cycle))
|
|
1309
|
+
if not any(
|
|
1310
|
+
tuple(sorted(c)) == cycle_tuple for c in cycles
|
|
1311
|
+
):
|
|
1312
|
+
cycles.append(cycle)
|
|
1313
|
+
except ValueError:
|
|
1314
|
+
pass
|
|
1315
|
+
|
|
1316
|
+
rec_stack.remove(node)
|
|
1317
|
+
|
|
1318
|
+
for node in graph:
|
|
1319
|
+
if node not in visited:
|
|
1320
|
+
dfs(node, [])
|
|
1321
|
+
|
|
1322
|
+
return cycles
|
|
1323
|
+
|
|
1324
|
+
cycles = find_cycles(forward_graph)
|
|
1325
|
+
|
|
1326
|
+
# Format response
|
|
1327
|
+
if not cycles:
|
|
1328
|
+
response_text = "No circular dependencies detected"
|
|
1329
|
+
else:
|
|
1330
|
+
response_lines = [f"# Circular Dependencies Found: {len(cycles)}\n"]
|
|
1331
|
+
|
|
1332
|
+
for i, cycle in enumerate(cycles, 1):
|
|
1333
|
+
response_lines.append(f"## Cycle {i}")
|
|
1334
|
+
response_lines.append("```")
|
|
1335
|
+
for j, node in enumerate(cycle):
|
|
1336
|
+
if j < len(cycle) - 1:
|
|
1337
|
+
response_lines.append(f"{node}")
|
|
1338
|
+
response_lines.append(" ↓")
|
|
1339
|
+
else:
|
|
1340
|
+
response_lines.append(f"{node} (back to {cycle[0]})")
|
|
1341
|
+
response_lines.append("```\n")
|
|
1342
|
+
|
|
1343
|
+
response_text = "\n".join(response_lines)
|
|
1344
|
+
|
|
1345
|
+
return CallToolResult(
|
|
1346
|
+
content=[TextContent(type="text", text=response_text)]
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1349
|
+
except Exception as e:
|
|
1350
|
+
logger.error(f"Circular dependency check failed: {e}")
|
|
1351
|
+
return CallToolResult(
|
|
1352
|
+
content=[
|
|
1353
|
+
TextContent(
|
|
1354
|
+
type="text",
|
|
1355
|
+
text=f"Circular dependency check failed: {str(e)}",
|
|
1356
|
+
)
|
|
1357
|
+
],
|
|
1358
|
+
isError=True,
|
|
1359
|
+
)
|
|
1360
|
+
|
|
1361
|
+
async def _interpret_analysis(self, args: dict[str, Any]) -> CallToolResult:
|
|
1362
|
+
"""Handle interpret_analysis tool call."""
|
|
1363
|
+
analysis_json_str = args.get("analysis_json", "")
|
|
1364
|
+
focus = args.get("focus", "summary")
|
|
1365
|
+
verbosity = args.get("verbosity", "normal")
|
|
1366
|
+
|
|
1367
|
+
if not analysis_json_str:
|
|
1368
|
+
return CallToolResult(
|
|
1369
|
+
content=[
|
|
1370
|
+
TextContent(type="text", text="analysis_json parameter is required")
|
|
1371
|
+
],
|
|
1372
|
+
isError=True,
|
|
1373
|
+
)
|
|
1374
|
+
|
|
1375
|
+
try:
|
|
1376
|
+
import json
|
|
1377
|
+
|
|
1378
|
+
from ..analysis.interpretation import AnalysisInterpreter, LLMContextExport
|
|
1379
|
+
|
|
1380
|
+
# Parse JSON input
|
|
1381
|
+
analysis_data = json.loads(analysis_json_str)
|
|
1382
|
+
|
|
1383
|
+
# Convert to LLMContextExport
|
|
1384
|
+
export = LLMContextExport(**analysis_data)
|
|
1385
|
+
|
|
1386
|
+
# Create interpreter and generate interpretation
|
|
1387
|
+
interpreter = AnalysisInterpreter()
|
|
1388
|
+
interpretation = interpreter.interpret(
|
|
1389
|
+
export, focus=focus, verbosity=verbosity
|
|
1390
|
+
)
|
|
1391
|
+
|
|
1392
|
+
return CallToolResult(
|
|
1393
|
+
content=[TextContent(type="text", text=interpretation)]
|
|
1394
|
+
)
|
|
1395
|
+
|
|
1396
|
+
except json.JSONDecodeError as e:
|
|
1397
|
+
return CallToolResult(
|
|
1398
|
+
content=[
|
|
1399
|
+
TextContent(
|
|
1400
|
+
type="text",
|
|
1401
|
+
text=f"Invalid JSON input: {str(e)}",
|
|
1402
|
+
)
|
|
1403
|
+
],
|
|
1404
|
+
isError=True,
|
|
1405
|
+
)
|
|
1406
|
+
except Exception as e:
|
|
1407
|
+
logger.error(f"Analysis interpretation failed: {e}")
|
|
1408
|
+
return CallToolResult(
|
|
1409
|
+
content=[
|
|
1410
|
+
TextContent(
|
|
1411
|
+
type="text",
|
|
1412
|
+
text=f"Interpretation failed: {str(e)}",
|
|
1413
|
+
)
|
|
1414
|
+
],
|
|
1415
|
+
isError=True,
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
def _get_threshold_config(self, preset: str) -> ThresholdConfig:
|
|
1419
|
+
"""Get threshold configuration based on preset.
|
|
1420
|
+
|
|
1421
|
+
Args:
|
|
1422
|
+
preset: Threshold preset ('strict', 'standard', or 'relaxed')
|
|
1423
|
+
|
|
1424
|
+
Returns:
|
|
1425
|
+
ThresholdConfig instance
|
|
1426
|
+
"""
|
|
1427
|
+
if preset == "strict":
|
|
1428
|
+
# Stricter thresholds
|
|
1429
|
+
config = ThresholdConfig()
|
|
1430
|
+
config.complexity.cognitive_a = 3
|
|
1431
|
+
config.complexity.cognitive_b = 7
|
|
1432
|
+
config.complexity.cognitive_c = 15
|
|
1433
|
+
config.complexity.cognitive_d = 20
|
|
1434
|
+
config.smells.long_method_lines = 30
|
|
1435
|
+
config.smells.high_complexity = 10
|
|
1436
|
+
config.smells.too_many_parameters = 3
|
|
1437
|
+
config.smells.deep_nesting_depth = 3
|
|
1438
|
+
return config
|
|
1439
|
+
elif preset == "relaxed":
|
|
1440
|
+
# More relaxed thresholds
|
|
1441
|
+
config = ThresholdConfig()
|
|
1442
|
+
config.complexity.cognitive_a = 7
|
|
1443
|
+
config.complexity.cognitive_b = 15
|
|
1444
|
+
config.complexity.cognitive_c = 25
|
|
1445
|
+
config.complexity.cognitive_d = 40
|
|
1446
|
+
config.smells.long_method_lines = 75
|
|
1447
|
+
config.smells.high_complexity = 20
|
|
1448
|
+
config.smells.too_many_parameters = 7
|
|
1449
|
+
config.smells.deep_nesting_depth = 5
|
|
1450
|
+
return config
|
|
1451
|
+
else:
|
|
1452
|
+
# Standard (default)
|
|
1453
|
+
return ThresholdConfig()
|
|
1454
|
+
|
|
645
1455
|
|
|
646
1456
|
def create_mcp_server(
|
|
647
1457
|
project_root: Path | None = None, enable_file_watching: bool | None = None
|