mcp-vector-search 1.0.3__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.
Files changed (63) hide show
  1. mcp_vector_search/__init__.py +3 -3
  2. mcp_vector_search/analysis/__init__.py +48 -1
  3. mcp_vector_search/analysis/baseline/__init__.py +68 -0
  4. mcp_vector_search/analysis/baseline/comparator.py +462 -0
  5. mcp_vector_search/analysis/baseline/manager.py +621 -0
  6. mcp_vector_search/analysis/collectors/__init__.py +35 -0
  7. mcp_vector_search/analysis/collectors/cohesion.py +463 -0
  8. mcp_vector_search/analysis/collectors/coupling.py +1162 -0
  9. mcp_vector_search/analysis/collectors/halstead.py +514 -0
  10. mcp_vector_search/analysis/collectors/smells.py +325 -0
  11. mcp_vector_search/analysis/debt.py +516 -0
  12. mcp_vector_search/analysis/interpretation.py +685 -0
  13. mcp_vector_search/analysis/metrics.py +74 -1
  14. mcp_vector_search/analysis/reporters/__init__.py +3 -1
  15. mcp_vector_search/analysis/reporters/console.py +424 -0
  16. mcp_vector_search/analysis/reporters/markdown.py +480 -0
  17. mcp_vector_search/analysis/reporters/sarif.py +377 -0
  18. mcp_vector_search/analysis/storage/__init__.py +93 -0
  19. mcp_vector_search/analysis/storage/metrics_store.py +762 -0
  20. mcp_vector_search/analysis/storage/schema.py +245 -0
  21. mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
  22. mcp_vector_search/analysis/trends.py +308 -0
  23. mcp_vector_search/analysis/visualizer/__init__.py +90 -0
  24. mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
  25. mcp_vector_search/analysis/visualizer/exporter.py +484 -0
  26. mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
  27. mcp_vector_search/analysis/visualizer/schemas.py +525 -0
  28. mcp_vector_search/cli/commands/analyze.py +665 -11
  29. mcp_vector_search/cli/commands/chat.py +193 -0
  30. mcp_vector_search/cli/commands/index.py +600 -2
  31. mcp_vector_search/cli/commands/index_background.py +467 -0
  32. mcp_vector_search/cli/commands/search.py +194 -1
  33. mcp_vector_search/cli/commands/setup.py +64 -13
  34. mcp_vector_search/cli/commands/status.py +302 -3
  35. mcp_vector_search/cli/commands/visualize/cli.py +26 -10
  36. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +8 -4
  37. mcp_vector_search/cli/commands/visualize/graph_builder.py +167 -234
  38. mcp_vector_search/cli/commands/visualize/server.py +304 -15
  39. mcp_vector_search/cli/commands/visualize/templates/base.py +60 -6
  40. mcp_vector_search/cli/commands/visualize/templates/scripts.py +2100 -65
  41. mcp_vector_search/cli/commands/visualize/templates/styles.py +1297 -88
  42. mcp_vector_search/cli/didyoumean.py +5 -0
  43. mcp_vector_search/cli/main.py +16 -5
  44. mcp_vector_search/cli/output.py +134 -5
  45. mcp_vector_search/config/thresholds.py +89 -1
  46. mcp_vector_search/core/__init__.py +16 -0
  47. mcp_vector_search/core/database.py +39 -2
  48. mcp_vector_search/core/embeddings.py +24 -0
  49. mcp_vector_search/core/git.py +380 -0
  50. mcp_vector_search/core/indexer.py +445 -84
  51. mcp_vector_search/core/llm_client.py +9 -4
  52. mcp_vector_search/core/models.py +88 -1
  53. mcp_vector_search/core/relationships.py +473 -0
  54. mcp_vector_search/core/search.py +1 -1
  55. mcp_vector_search/mcp/server.py +795 -4
  56. mcp_vector_search/parsers/python.py +285 -5
  57. mcp_vector_search/utils/gitignore.py +0 -3
  58. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +3 -2
  59. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/RECORD +62 -39
  60. mcp_vector_search/cli/commands/visualize.py.original +0 -2536
  61. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +0 -0
  62. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +0 -0
  63. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
@@ -136,6 +136,41 @@ def search_main(
136
136
  help="Custom export file path",
137
137
  rich_help_panel="💾 Export Options",
138
138
  ),
139
+ max_complexity: int | None = typer.Option(
140
+ None,
141
+ "--max-complexity",
142
+ help="Filter results with cognitive complexity greater than N",
143
+ min=1,
144
+ rich_help_panel="🎯 Quality Filters",
145
+ ),
146
+ no_smells: bool = typer.Option(
147
+ False,
148
+ "--no-smells",
149
+ help="Exclude results with code smells",
150
+ rich_help_panel="🎯 Quality Filters",
151
+ ),
152
+ grade: str | None = typer.Option(
153
+ None,
154
+ "--grade",
155
+ help="Filter by complexity grade (e.g., 'A,B,C' or 'A-C')",
156
+ rich_help_panel="🎯 Quality Filters",
157
+ ),
158
+ min_quality: int | None = typer.Option(
159
+ None,
160
+ "--min-quality",
161
+ help="Filter by minimum quality score (0-100)",
162
+ min=0,
163
+ max=100,
164
+ rich_help_panel="🎯 Quality Filters",
165
+ ),
166
+ quality_weight: float = typer.Option(
167
+ 0.3,
168
+ "--quality-weight",
169
+ help="Weight for quality ranking (0.0=pure relevance, 1.0=pure quality, default=0.3)",
170
+ min=0.0,
171
+ max=1.0,
172
+ rich_help_panel="🎯 Quality Filters",
173
+ ),
139
174
  ) -> None:
140
175
  """🔍 Search your codebase semantically.
141
176
 
@@ -166,6 +201,25 @@ def search_main(
166
201
  [green]Context-based search:[/green]
167
202
  $ mcp-vector-search search "implement rate limiting" --context --focus security
168
203
 
204
+ [bold cyan]Quality Filters:[/bold cyan]
205
+
206
+ [green]Filter by complexity:[/green]
207
+ $ mcp-vector-search search "authentication" --max-complexity 15
208
+
209
+ [green]Exclude code smells:[/green]
210
+ $ mcp-vector-search search "login" --no-smells
211
+
212
+ [green]Filter by grade:[/green]
213
+ $ mcp-vector-search search "api" --grade A,B
214
+
215
+ [green]Minimum quality score:[/green]
216
+ $ mcp-vector-search search "handler" --min-quality 80
217
+
218
+ [green]Quality-aware ranking:[/green]
219
+ $ mcp-vector-search search "auth" --quality-weight 0.5 # Balance relevance and quality
220
+ $ mcp-vector-search search "api" --quality-weight 0.0 # Pure semantic search
221
+ $ mcp-vector-search search "util" --quality-weight 1.0 # Pure quality ranking
222
+
169
223
  [bold cyan]Export Results:[/bold cyan]
170
224
 
171
225
  [green]Export to JSON:[/green]
@@ -242,6 +296,11 @@ def search_main(
242
296
  json_output=json_output,
243
297
  export_format=export_format,
244
298
  export_path=export_path,
299
+ max_complexity=max_complexity,
300
+ no_smells=no_smells,
301
+ grade=grade,
302
+ min_quality=min_quality,
303
+ quality_weight=quality_weight,
245
304
  )
246
305
  )
247
306
 
@@ -251,6 +310,52 @@ def search_main(
251
310
  raise typer.Exit(1)
252
311
 
253
312
 
313
+ def _parse_grade_filter(grade_str: str) -> set[str]:
314
+ """Parse grade filter string into set of allowed grades.
315
+
316
+ Supports formats:
317
+ - Comma-separated: "A,B,C"
318
+ - Range: "A-C" (expands to A, B, C)
319
+ - Mixed: "A,C-D" (expands to A, C, D)
320
+
321
+ Args:
322
+ grade_str: Grade filter string
323
+
324
+ Returns:
325
+ Set of allowed grade letters
326
+ """
327
+ allowed_grades = set()
328
+ grade_order = ["A", "B", "C", "D", "F"]
329
+
330
+ # Split by comma
331
+ parts = [part.strip().upper() for part in grade_str.split(",")]
332
+
333
+ for part in parts:
334
+ if "-" in part:
335
+ # Range format (e.g., "A-C")
336
+ start, end = part.split("-", 1)
337
+ start = start.strip()
338
+ end = end.strip()
339
+
340
+ if start in grade_order and end in grade_order:
341
+ start_idx = grade_order.index(start)
342
+ end_idx = grade_order.index(end)
343
+
344
+ # Handle reverse ranges (C-A becomes A-C)
345
+ if start_idx > end_idx:
346
+ start_idx, end_idx = end_idx, start_idx
347
+
348
+ # Add all grades in range
349
+ for grade in grade_order[start_idx : end_idx + 1]:
350
+ allowed_grades.add(grade)
351
+ else:
352
+ # Single grade
353
+ if part in grade_order:
354
+ allowed_grades.add(part)
355
+
356
+ return allowed_grades
357
+
358
+
254
359
  async def run_search(
255
360
  project_root: Path,
256
361
  query: str,
@@ -264,8 +369,13 @@ async def run_search(
264
369
  json_output: bool = False,
265
370
  export_format: str | None = None,
266
371
  export_path: Path | None = None,
372
+ max_complexity: int | None = None,
373
+ no_smells: bool = False,
374
+ grade: str | None = None,
375
+ min_quality: int | None = None,
376
+ quality_weight: float = 0.3,
267
377
  ) -> None:
268
- """Run semantic search."""
378
+ """Run semantic search with optional quality filters and quality-aware ranking."""
269
379
  # Load project configuration
270
380
  project_manager = ProjectManager(project_root)
271
381
 
@@ -371,6 +481,88 @@ async def run_search(
371
481
  f"File pattern '{files}' filtered results to {len(results)} matches"
372
482
  )
373
483
 
484
+ # Apply quality filters if specified
485
+ if any([max_complexity, no_smells, grade, min_quality]) and results:
486
+ filtered_results = []
487
+ for result in results:
488
+ # Parse quality metrics from result metadata
489
+ cognitive_complexity = getattr(result, "cognitive_complexity", None)
490
+ complexity_grade = getattr(result, "complexity_grade", None)
491
+ smell_count = getattr(result, "smell_count", None)
492
+ quality_score = getattr(result, "quality_score", None)
493
+
494
+ # Filter by max complexity
495
+ if max_complexity is not None and cognitive_complexity is not None:
496
+ if cognitive_complexity > max_complexity:
497
+ continue
498
+
499
+ # Filter by code smells
500
+ if no_smells and smell_count is not None:
501
+ if smell_count > 0:
502
+ continue
503
+
504
+ # Filter by grade
505
+ if grade and complexity_grade:
506
+ allowed_grades = _parse_grade_filter(grade)
507
+ if complexity_grade not in allowed_grades:
508
+ continue
509
+
510
+ # Filter by minimum quality score
511
+ if min_quality is not None and quality_score is not None:
512
+ if quality_score < min_quality:
513
+ continue
514
+
515
+ filtered_results.append(result)
516
+
517
+ initial_count = len(results)
518
+ results = filtered_results
519
+ logger.debug(
520
+ f"Quality filters reduced results from {initial_count} to {len(results)}"
521
+ )
522
+
523
+ # Apply quality-aware ranking if quality_weight > 0 and results have quality metrics
524
+ if quality_weight > 0.0 and results:
525
+ # Calculate quality scores for results that don't have them
526
+ for result in results:
527
+ if result.quality_score is None:
528
+ # Calculate quality score using the formula
529
+ calculated_score = result.calculate_quality_score()
530
+ if calculated_score is not None:
531
+ result.quality_score = calculated_score
532
+
533
+ # Re-rank results based on combined score
534
+ # Store original similarity score for display
535
+ for result in results:
536
+ # Store original relevance score
537
+ if not hasattr(result, "_original_similarity"):
538
+ result._original_similarity = result.similarity_score
539
+
540
+ # Calculate combined score
541
+ if result.quality_score is not None:
542
+ # Normalize quality score to 0-1 range (it's 0-100)
543
+ normalized_quality = result.quality_score / 100.0
544
+
545
+ # Combined score: (1-W) × relevance + W × quality
546
+ combined_score = (
547
+ (1.0 - quality_weight) * result.similarity_score
548
+ + quality_weight * normalized_quality
549
+ )
550
+
551
+ # Update similarity_score with combined score for sorting
552
+ result.similarity_score = combined_score
553
+ # If no quality score, keep original similarity_score
554
+
555
+ # Re-sort by combined score
556
+ results.sort(key=lambda r: r.similarity_score, reverse=True)
557
+
558
+ # Update ranks
559
+ for i, result in enumerate(results):
560
+ result.rank = i + 1
561
+
562
+ logger.debug(
563
+ f"Quality-aware ranking applied with weight {quality_weight:.2f}"
564
+ )
565
+
374
566
  # Handle export if requested
375
567
  if export_format:
376
568
  from ..export import SearchResultExporter, get_export_path
@@ -424,6 +616,7 @@ async def run_search(
424
616
  results=results,
425
617
  query=query,
426
618
  show_content=show_content,
619
+ quality_weight=quality_weight,
427
620
  )
428
621
 
429
622
  # Add contextual tips based on results
@@ -31,6 +31,9 @@ from pathlib import Path
31
31
 
32
32
  import typer
33
33
  from loguru import logger
34
+
35
+ # Import Platform enum to filter excluded platforms
36
+ from py_mcp_installer import Platform
34
37
  from rich.console import Console
35
38
  from rich.panel import Panel
36
39
 
@@ -53,6 +56,9 @@ from ..output import (
53
56
  # Import functions from refactored install module
54
57
  from .install import _install_to_platform, detect_all_platforms
55
58
 
59
+ # Platforms to exclude from auto-setup (user can still manually install)
60
+ EXCLUDED_PLATFORMS_FROM_SETUP = {Platform.CLAUDE_DESKTOP}
61
+
56
62
  # Create console for rich output
57
63
  console = Console()
58
64
 
@@ -148,9 +154,10 @@ def register_with_claude_cli(
148
154
 
149
155
  # Build the add command using mcp-vector-search CLI
150
156
  # This works for all installation methods: pipx, homebrew, and uv
157
+ # Claude Code sets CWD to the project directory, so no path needed
151
158
  # claude mcp add --transport stdio mcp-vector-search \
152
159
  # --env MCP_ENABLE_FILE_WATCHING=true \
153
- # -- mcp-vector-search mcp /project/root
160
+ # -- mcp-vector-search mcp
154
161
  cmd = [
155
162
  "claude",
156
163
  "mcp",
@@ -163,7 +170,6 @@ def register_with_claude_cli(
163
170
  "--",
164
171
  "mcp-vector-search",
165
172
  "mcp",
166
- str(project_root.absolute()),
167
173
  ]
168
174
 
169
175
  if verbose:
@@ -959,15 +965,35 @@ async def _run_smart_setup(
959
965
  detected_platforms_list = detect_all_platforms()
960
966
 
961
967
  if detected_platforms_list:
962
- platform_names = [p.platform.value for p in detected_platforms_list]
963
- print_success(
964
- f" ✅ Found {len(platform_names)} platform(s): {', '.join(platform_names)}"
965
- )
966
- if verbose:
967
- for platform_info in detected_platforms_list:
968
- print_info(
969
- f" {platform_info.platform.value}: {platform_info.config_path}"
970
- )
968
+ # Filter out excluded platforms for display
969
+ configurable_platforms = [
970
+ p
971
+ for p in detected_platforms_list
972
+ if p.platform not in EXCLUDED_PLATFORMS_FROM_SETUP
973
+ ]
974
+ excluded_platforms = [
975
+ p
976
+ for p in detected_platforms_list
977
+ if p.platform in EXCLUDED_PLATFORMS_FROM_SETUP
978
+ ]
979
+
980
+ if configurable_platforms:
981
+ platform_names = [p.platform.value for p in configurable_platforms]
982
+ print_success(
983
+ f" ✅ Found {len(platform_names)} platform(s): {', '.join(platform_names)}"
984
+ )
985
+ if verbose:
986
+ for platform_info in configurable_platforms:
987
+ print_info(
988
+ f" {platform_info.platform.value}: {platform_info.config_path}"
989
+ )
990
+
991
+ # Note excluded platforms
992
+ if excluded_platforms:
993
+ excluded_names = [p.platform.value for p in excluded_platforms]
994
+ print_info(
995
+ f" ℹ️ Skipping: {', '.join(excluded_names)} (use 'install mcp --platform' for manual install)"
996
+ )
971
997
  else:
972
998
  print_info(" No MCP platforms detected (will configure Claude Code)")
973
999
 
@@ -1012,7 +1038,27 @@ async def _run_smart_setup(
1012
1038
  # ===========================================================================
1013
1039
  # Phase 4: Indexing
1014
1040
  # ===========================================================================
1015
- if not already_initialized or force:
1041
+ # Determine if indexing is needed:
1042
+ # 1. Not already initialized (new setup)
1043
+ # 2. Force flag is set
1044
+ # 3. Index database doesn't exist
1045
+ # 4. Index exists but is empty
1046
+ # 5. Files have changed (incremental indexing will handle this)
1047
+ needs_indexing = not already_initialized or force
1048
+
1049
+ if already_initialized and not force:
1050
+ # Check if index exists and has content
1051
+ index_db_path = project_root / ".mcp-vector-search" / "chroma.sqlite3"
1052
+ if not index_db_path.exists():
1053
+ print_info(" Index database not found, will create...")
1054
+ needs_indexing = True
1055
+ else:
1056
+ # Check if index is empty or files have changed
1057
+ # Run incremental indexing to catch any changes
1058
+ print_info(" Checking for file changes...")
1059
+ needs_indexing = True # Always run incremental to catch changes
1060
+
1061
+ if needs_indexing:
1016
1062
  console.print("\n[bold blue]🔍 Indexing codebase...[/bold blue]")
1017
1063
 
1018
1064
  from .index import run_indexing
@@ -1045,7 +1091,12 @@ async def _run_smart_setup(
1045
1091
  print_info(" ✅ Claude CLI detected, using native integration")
1046
1092
 
1047
1093
  # Use detected platforms or default to empty list
1048
- platforms_to_configure = detected_platforms_list if detected_platforms_list else []
1094
+ # Filter out excluded platforms (e.g., Claude Desktop) - exclusion already noted in Phase 1
1095
+ platforms_to_configure = [
1096
+ p
1097
+ for p in (detected_platforms_list or [])
1098
+ if p.platform not in EXCLUDED_PLATFORMS_FROM_SETUP
1099
+ ]
1049
1100
 
1050
1101
  # Configure all detected platforms using new library
1051
1102
  for platform_info in platforms_to_configure: