chunkhound 0.1.0__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 (65) hide show
  1. chunkhound/__init__.py +25 -0
  2. chunkhound/api/__init__.py +5 -0
  3. chunkhound/api/cli/__init__.py +13 -0
  4. chunkhound/api/cli/commands/__init__.py +11 -0
  5. chunkhound/api/cli/commands/config.py +488 -0
  6. chunkhound/api/cli/commands/mcp.py +57 -0
  7. chunkhound/api/cli/commands/run.py +366 -0
  8. chunkhound/api/cli/main.py +172 -0
  9. chunkhound/api/cli/parsers/__init__.py +14 -0
  10. chunkhound/api/cli/parsers/config_parser.py +393 -0
  11. chunkhound/api/cli/parsers/main_parser.py +149 -0
  12. chunkhound/api/cli/parsers/mcp_parser.py +64 -0
  13. chunkhound/api/cli/parsers/run_parser.py +91 -0
  14. chunkhound/api/cli/utils/__init__.py +13 -0
  15. chunkhound/api/cli/utils/output.py +246 -0
  16. chunkhound/api/cli/utils/validation.py +274 -0
  17. chunkhound/chunker.py +377 -0
  18. chunkhound/config.py +777 -0
  19. chunkhound/database.py +314 -0
  20. chunkhound/embeddings.py +1255 -0
  21. chunkhound/file_discovery_cache.py +260 -0
  22. chunkhound/file_watcher.py +480 -0
  23. chunkhound/mcp_entry.py +57 -0
  24. chunkhound/mcp_server.py +463 -0
  25. chunkhound/parser.py +4013 -0
  26. chunkhound/process_detection.py +276 -0
  27. chunkhound/py.typed +0 -0
  28. chunkhound/signal_coordinator.py +464 -0
  29. chunkhound/tree_cache.py +339 -0
  30. chunkhound-0.1.0.dist-info/METADATA +168 -0
  31. chunkhound-0.1.0.dist-info/RECORD +65 -0
  32. chunkhound-0.1.0.dist-info/WHEEL +4 -0
  33. chunkhound-0.1.0.dist-info/entry_points.txt +3 -0
  34. chunkhound-0.1.0.dist-info/licenses/LICENSE +21 -0
  35. core/__init__.py +44 -0
  36. core/exceptions/__init__.py +37 -0
  37. core/exceptions/core.py +301 -0
  38. core/models/__init__.py +24 -0
  39. core/models/chunk.py +400 -0
  40. core/models/embedding.py +422 -0
  41. core/models/file.py +281 -0
  42. core/types/__init__.py +51 -0
  43. core/types/common.py +156 -0
  44. interfaces/__init__.py +11 -0
  45. interfaces/database_provider.py +197 -0
  46. interfaces/embedding_provider.py +271 -0
  47. interfaces/language_parser.py +305 -0
  48. providers/__init__.py +16 -0
  49. providers/database/__init__.py +7 -0
  50. providers/database/duckdb_provider.py +2119 -0
  51. providers/embeddings/__init__.py +7 -0
  52. providers/embeddings/openai_provider.py +469 -0
  53. providers/parsing/__init__.py +7 -0
  54. providers/parsing/csharp_parser.py +790 -0
  55. providers/parsing/java_parser.py +771 -0
  56. providers/parsing/javascript_parser.py +507 -0
  57. providers/parsing/markdown_parser.py +402 -0
  58. providers/parsing/python_parser.py +397 -0
  59. providers/parsing/typescript_parser.py +736 -0
  60. registry/__init__.py +403 -0
  61. services/__init__.py +13 -0
  62. services/base_service.py +24 -0
  63. services/embedding_service.py +478 -0
  64. services/indexing_coordinator.py +504 -0
  65. services/search_service.py +374 -0
chunkhound/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """ChunkHound - Local-first semantic code search with vector and regex capabilities."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "ChunkHound Team"
5
+ __description__ = "Local-first semantic code search with vector and regex capabilities"
6
+
7
+ # Import modules only when needed to avoid dependency issues during setup
8
+ __all__ = [
9
+ "Database",
10
+ "CodeParser",
11
+ "Chunker",
12
+ ]
13
+
14
+ def __getattr__(name: str):
15
+ """Lazy import to avoid dependency issues during setup."""
16
+ if name == "Database":
17
+ from .database import Database
18
+ return Database
19
+ elif name == "CodeParser":
20
+ from .parser import CodeParser
21
+ return CodeParser
22
+ elif name == "Chunker":
23
+ from .chunker import Chunker
24
+ return Chunker
25
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,5 @@
1
+ """ChunkHound API package - provides CLI and server interfaces."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = []
@@ -0,0 +1,13 @@
1
+ """ChunkHound CLI API package - modular command-line interface."""
2
+
3
+ from .commands import (
4
+ run_command,
5
+ mcp_command,
6
+ config_command,
7
+ )
8
+
9
+ __all__ = [
10
+ "run_command",
11
+ "mcp_command",
12
+ "config_command",
13
+ ]
@@ -0,0 +1,11 @@
1
+ """ChunkHound CLI commands package - modular command implementations."""
2
+
3
+ from .run import run_command
4
+ from .mcp import mcp_command
5
+ from .config import config_command
6
+
7
+ __all__ = [
8
+ "run_command",
9
+ "mcp_command",
10
+ "config_command",
11
+ ]
@@ -0,0 +1,488 @@
1
+ """Config command module - handles embedding server configuration operations."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Dict, Any, List, Optional
8
+
9
+ from loguru import logger
10
+
11
+ from chunkhound.config import get_config_manager, reset_config_manager, ServerConfig
12
+ from ..utils.output import OutputFormatter, format_health_status, format_server_info, print_section
13
+ from ..utils.validation import validate_config_args, validate_server_name
14
+
15
+
16
+ async def config_command(args: argparse.Namespace) -> None:
17
+ """Execute the config command with appropriate subcommand.
18
+
19
+ Args:
20
+ args: Parsed command-line arguments
21
+ """
22
+ # Route to appropriate subcommand
23
+ subcommand_handlers = {
24
+ "list": config_list_command,
25
+ "add": config_add_command,
26
+ "remove": config_remove_command,
27
+ "test": config_test_command,
28
+ "health": config_health_command,
29
+ "enable": config_enable_command,
30
+ "disable": config_disable_command,
31
+ "set-default": config_set_default_command,
32
+ "validate": config_validate_command,
33
+ "benchmark": config_benchmark_command,
34
+ "switch": config_switch_command,
35
+ "discover": config_discover_command,
36
+ "export": config_export_command,
37
+ "import": config_import_command,
38
+ "template": config_template_command,
39
+ "batch-test": config_batch_test_command,
40
+ }
41
+
42
+ handler = subcommand_handlers.get(args.config_command)
43
+ if handler:
44
+ await handler(args)
45
+ else:
46
+ logger.error(f"Unknown config command: {args.config_command}")
47
+ sys.exit(1)
48
+
49
+
50
+ async def config_list_command(args: argparse.Namespace) -> None:
51
+ """Handle config list command."""
52
+ formatter = OutputFormatter(verbose=getattr(args, 'verbose', False))
53
+
54
+ try:
55
+ config_manager = get_config_manager(str(args.config) if args.config else None)
56
+ servers = config_manager.registry.list_servers()
57
+
58
+ if not servers:
59
+ formatter.info("No servers configured.")
60
+ return
61
+
62
+ print(f"Configured servers ({len(servers)}):")
63
+ print()
64
+
65
+ for name in servers:
66
+ server = config_manager.registry.get_server(name)
67
+ default_marker = " (default)" if config_manager.registry._default_server == name else ""
68
+ enabled_marker = "" if server.enabled else " (disabled)"
69
+
70
+ print(f" {name}{default_marker}{enabled_marker}")
71
+ print(f" Type: {server.type}")
72
+ print(f" URL: {server.base_url}")
73
+ print(f" Model: {server.model or 'auto-detected'}")
74
+
75
+ if getattr(args, 'show_health', False):
76
+ try:
77
+ health = await config_manager.registry.check_server_health(name)
78
+ status = format_health_status({
79
+ 'healthy': health.is_healthy,
80
+ 'response_time_ms': health.response_time_ms,
81
+ 'error': health.error_message
82
+ })
83
+ print(f" Health: {status}")
84
+ except Exception as e:
85
+ print(f" Health: ❓ unknown ({e})")
86
+
87
+ print()
88
+
89
+ except Exception as e:
90
+ formatter.error(f"Failed to list servers: {e}")
91
+ sys.exit(1)
92
+
93
+
94
+ async def config_add_command(args: argparse.Namespace) -> None:
95
+ """Handle config add command."""
96
+ formatter = OutputFormatter(verbose=getattr(args, 'verbose', False))
97
+
98
+ try:
99
+ config_manager = get_config_manager(str(args.config) if args.config else None)
100
+
101
+ # Validate arguments
102
+ if not validate_config_args(args.type, args.base_url, args.model, getattr(args, 'api_key', None)):
103
+ sys.exit(1)
104
+
105
+ # Check if server name already exists
106
+ existing_servers = config_manager.registry.list_servers()
107
+ if not validate_server_name(args.name, existing_servers):
108
+ sys.exit(1)
109
+
110
+ # Prepare metadata for BGE-IN-ICL specific options
111
+ metadata = {}
112
+ if args.type == 'bge-in-icl':
113
+ if hasattr(args, 'batch_size') and args.batch_size:
114
+ metadata['batch_size'] = args.batch_size
115
+
116
+ # Create server configuration
117
+ server_config = ServerConfig(
118
+ name=args.name,
119
+ type=args.type,
120
+ base_url=args.base_url,
121
+ model=args.model or ('bge-in-icl' if args.type == 'bge-in-icl' else None),
122
+ api_key=getattr(args, 'api_key', None),
123
+ enabled=True,
124
+ metadata=metadata
125
+ )
126
+
127
+ # Add server to configuration
128
+ config_manager.add_server(server_config)
129
+
130
+ # Set as default if requested
131
+ if getattr(args, 'default', False):
132
+ config_manager.registry.set_default_server(args.name)
133
+
134
+ # Save configuration
135
+ config_manager.save_config()
136
+
137
+ formatter.success(f"Added server '{args.name}' successfully")
138
+
139
+ if getattr(args, 'default', False):
140
+ formatter.info(f"Set '{args.name}' as default server")
141
+
142
+ except Exception as e:
143
+ formatter.error(f"Failed to add server: {e}")
144
+ sys.exit(1)
145
+
146
+
147
+ async def config_remove_command(args: argparse.Namespace) -> None:
148
+ """Handle config remove command."""
149
+ formatter = OutputFormatter(verbose=getattr(args, 'verbose', False))
150
+
151
+ try:
152
+ config_manager = get_config_manager(str(args.config) if args.config else None)
153
+
154
+ if args.name not in config_manager.registry.list_servers():
155
+ formatter.error(f"Server '{args.name}' not found")
156
+ sys.exit(1)
157
+
158
+ config_manager.remove_server(args.name)
159
+ config_manager.save_config()
160
+
161
+ formatter.success(f"Removed server '{args.name}' successfully")
162
+
163
+ except Exception as e:
164
+ formatter.error(f"Failed to remove server: {e}")
165
+ sys.exit(1)
166
+
167
+
168
+ async def config_test_command(args: argparse.Namespace) -> None:
169
+ """Handle config test command."""
170
+ formatter = OutputFormatter(verbose=getattr(args, 'verbose', False))
171
+
172
+ try:
173
+ config_manager = get_config_manager(str(args.config) if args.config else None)
174
+
175
+ server_name = getattr(args, 'name', None)
176
+ if not server_name:
177
+ server_name = config_manager.registry._default_server
178
+ if not server_name:
179
+ formatter.error("No default server configured and no server specified")
180
+ sys.exit(1)
181
+
182
+ if server_name not in config_manager.registry.list_servers():
183
+ formatter.error(f"Server '{server_name}' not found")
184
+ sys.exit(1)
185
+
186
+ formatter.info(f"Testing server '{server_name}'...")
187
+
188
+ # Perform health check
189
+ health = await config_manager.registry.check_server_health(server_name)
190
+
191
+ if health.is_healthy:
192
+ formatter.success(f"Server '{server_name}' is healthy ({health.response_time_ms:.1f}ms)")
193
+ else:
194
+ formatter.error(f"Server '{server_name}' is unhealthy: {health.error_message}")
195
+ sys.exit(1)
196
+
197
+ except Exception as e:
198
+ formatter.error(f"Failed to test server: {e}")
199
+ sys.exit(1)
200
+
201
+
202
+ async def config_health_command(args: argparse.Namespace) -> None:
203
+ """Handle config health command."""
204
+ formatter = OutputFormatter(verbose=getattr(args, 'verbose', False))
205
+
206
+ try:
207
+ config_manager = get_config_manager(str(args.config) if args.config else None)
208
+
209
+ if getattr(args, 'monitor', False):
210
+ formatter.info("Starting health monitoring... (Press Ctrl+C to stop)")
211
+ await config_manager.start_monitoring()
212
+ try:
213
+ while True:
214
+ await asyncio.sleep(10)
215
+ except KeyboardInterrupt:
216
+ formatter.info("Health monitoring stopped")
217
+ await config_manager.stop_monitoring()
218
+ else:
219
+ # Single health check for all servers
220
+ servers = config_manager.registry.list_servers()
221
+ if not servers:
222
+ formatter.info("No servers configured")
223
+ return
224
+
225
+ formatter.info("Checking server health...")
226
+
227
+ for name in servers:
228
+ try:
229
+ health = await config_manager.registry.check_server_health(name)
230
+ status = format_health_status({
231
+ 'healthy': health.is_healthy,
232
+ 'response_time_ms': health.response_time_ms,
233
+ 'error': health.error_message
234
+ })
235
+ print(f" {name}: {status}")
236
+ except Exception as e:
237
+ print(f" {name}: ❓ Error checking health: {e}")
238
+
239
+ except Exception as e:
240
+ formatter.error(f"Failed to check health: {e}")
241
+ sys.exit(1)
242
+
243
+
244
+ async def config_enable_command(args: argparse.Namespace) -> None:
245
+ """Handle config enable command."""
246
+ formatter = OutputFormatter(verbose=getattr(args, 'verbose', False))
247
+
248
+ try:
249
+ config_manager = get_config_manager(str(args.config) if args.config else None)
250
+
251
+ if args.name not in config_manager.registry.list_servers():
252
+ formatter.error(f"Server '{args.name}' not found")
253
+ sys.exit(1)
254
+
255
+ server = config_manager.registry.get_server(args.name)
256
+ if server.enabled:
257
+ formatter.info(f"Server '{args.name}' is already enabled")
258
+ return
259
+
260
+ config_manager.registry.enable_server(args.name)
261
+ config_manager.save_config()
262
+
263
+ formatter.success(f"Enabled server '{args.name}'")
264
+
265
+ except Exception as e:
266
+ formatter.error(f"Failed to enable server: {e}")
267
+ sys.exit(1)
268
+
269
+
270
+ async def config_disable_command(args: argparse.Namespace) -> None:
271
+ """Handle config disable command."""
272
+ formatter = OutputFormatter(verbose=getattr(args, 'verbose', False))
273
+
274
+ try:
275
+ config_manager = get_config_manager(str(args.config) if args.config else None)
276
+
277
+ if args.name not in config_manager.registry.list_servers():
278
+ formatter.error(f"Server '{args.name}' not found")
279
+ sys.exit(1)
280
+
281
+ server = config_manager.registry.get_server(args.name)
282
+ if not server.enabled:
283
+ formatter.info(f"Server '{args.name}' is already disabled")
284
+ return
285
+
286
+ # Check if this is the default server
287
+ if config_manager.registry._default_server == args.name:
288
+ formatter.warning(f"Disabling default server '{args.name}' - you may want to set a new default")
289
+
290
+ config_manager.registry.disable_server(args.name)
291
+ config_manager.save_config()
292
+
293
+ formatter.success(f"Disabled server '{args.name}'")
294
+
295
+ except Exception as e:
296
+ formatter.error(f"Failed to disable server: {e}")
297
+ sys.exit(1)
298
+
299
+
300
+ async def config_set_default_command(args: argparse.Namespace) -> None:
301
+ """Handle config set-default command."""
302
+ formatter = OutputFormatter(verbose=getattr(args, 'verbose', False))
303
+
304
+ try:
305
+ config_manager = get_config_manager(str(args.config) if args.config else None)
306
+
307
+ if args.name not in config_manager.registry.list_servers():
308
+ formatter.error(f"Server '{args.name}' not found")
309
+ sys.exit(1)
310
+
311
+ server = config_manager.registry.get_server(args.name)
312
+ if not server.enabled:
313
+ formatter.error(f"Cannot set disabled server '{args.name}' as default")
314
+ sys.exit(1)
315
+
316
+ config_manager.registry.set_default_server(args.name)
317
+ config_manager.save_config()
318
+
319
+ formatter.success(f"Set '{args.name}' as default server")
320
+
321
+ except Exception as e:
322
+ formatter.error(f"Failed to set default server: {e}")
323
+ sys.exit(1)
324
+
325
+
326
+ async def config_validate_command(args: argparse.Namespace) -> None:
327
+ """Handle config validate command."""
328
+ formatter = OutputFormatter(verbose=getattr(args, 'verbose', False))
329
+
330
+ try:
331
+ config_manager = get_config_manager(str(args.config) if args.config else None)
332
+
333
+ # Use enhanced validation
334
+ validation_result = config_manager.validate_config_file(args.config)
335
+
336
+ formatter.info(f"Validating configuration: {validation_result.get('config_path', 'default')}")
337
+ print()
338
+
339
+ if validation_result.get('valid', False):
340
+ formatter.success("Configuration is valid")
341
+
342
+ # Show summary
343
+ servers = validation_result.get('servers', [])
344
+ formatter.info(f"Found {len(servers)} server(s)")
345
+
346
+ for server_info in servers:
347
+ status = "✅" if server_info.get('valid', False) else "❌"
348
+ print(f" {status} {server_info.get('name', 'unknown')}")
349
+
350
+ if not server_info.get('valid', False):
351
+ errors = server_info.get('errors', [])
352
+ for error in errors:
353
+ print(f" Error: {error}")
354
+ else:
355
+ formatter.error("Configuration has errors:")
356
+ errors = validation_result.get('errors', [])
357
+ for error in errors:
358
+ print(f" • {error}")
359
+ sys.exit(1)
360
+
361
+ except Exception as e:
362
+ formatter.error(f"Failed to validate configuration: {e}")
363
+ sys.exit(1)
364
+
365
+
366
+ # Simplified implementations for remaining commands
367
+ async def config_benchmark_command(args: argparse.Namespace) -> None:
368
+ """Handle config benchmark command."""
369
+ formatter = OutputFormatter()
370
+ formatter.info("Benchmark command not yet implemented in modular CLI")
371
+
372
+
373
+ async def config_switch_command(args: argparse.Namespace) -> None:
374
+ """Handle config switch command."""
375
+ formatter = OutputFormatter()
376
+ formatter.info("Switch command not yet implemented in modular CLI")
377
+
378
+
379
+ async def config_discover_command(args: argparse.Namespace) -> None:
380
+ """Handle config discover command."""
381
+ formatter = OutputFormatter()
382
+ formatter.info("Discover command not yet implemented in modular CLI")
383
+
384
+
385
+ async def config_export_command(args: argparse.Namespace) -> None:
386
+ """Handle config export command."""
387
+ formatter = OutputFormatter()
388
+ formatter.info("Export command not yet implemented in modular CLI")
389
+
390
+
391
+ async def config_import_command(args: argparse.Namespace) -> None:
392
+ """Handle config import command."""
393
+ formatter = OutputFormatter()
394
+ formatter.info("Import command not yet implemented in modular CLI")
395
+
396
+
397
+ async def config_template_command(args: argparse.Namespace) -> None:
398
+ """Handle config template command."""
399
+ formatter = OutputFormatter()
400
+ formatter.info("Template command not yet implemented in modular CLI")
401
+
402
+
403
+ async def config_batch_test_command(args: argparse.Namespace) -> None:
404
+ """Handle config batch-test command."""
405
+ formatter = OutputFormatter()
406
+ formatter.info("Batch-test command not yet implemented in modular CLI")
407
+
408
+
409
+ def add_config_subparser(subparsers) -> argparse.ArgumentParser:
410
+ """Add config command subparser to the main parser.
411
+
412
+ Args:
413
+ subparsers: Subparsers object from the main argument parser
414
+
415
+ Returns:
416
+ The configured config subparser
417
+ """
418
+ config_parser = subparsers.add_parser(
419
+ "config",
420
+ help="Manage embedding server configurations",
421
+ description="Configure and manage embedding server connections"
422
+ )
423
+
424
+ config_subparsers = config_parser.add_subparsers(dest="config_command", help="Configuration commands")
425
+
426
+ # Config list command
427
+ list_parser = config_subparsers.add_parser("list", help="List all configured servers")
428
+ list_parser.add_argument("--config", type=Path, help="Configuration file path")
429
+ list_parser.add_argument("--show-health", action="store_true", help="Show health status for each server")
430
+
431
+ # Config add command
432
+ add_parser = config_subparsers.add_parser("add", help="Add a new embedding server")
433
+ add_parser.add_argument("name", help="Server name")
434
+ add_parser.add_argument("--type", required=True, choices=["openai", "openai-compatible", "tei", "bge-in-icl"], help="Server type")
435
+ add_parser.add_argument("--base-url", required=True, help="Server base URL")
436
+ add_parser.add_argument("--model", help="Model name (auto-detected for TEI, defaults to 'bge-in-icl' for BGE-IN-ICL)")
437
+ add_parser.add_argument("--api-key", help="API key for authentication")
438
+ add_parser.add_argument("--default", action="store_true", help="Set as default server")
439
+ add_parser.add_argument("--config", type=Path, help="Configuration file path")
440
+ add_parser.add_argument("--batch-size", type=int, help="Batch size for embeddings")
441
+
442
+ # Config remove command
443
+ remove_parser = config_subparsers.add_parser("remove", help="Remove a server")
444
+ remove_parser.add_argument("name", help="Server name to remove")
445
+ remove_parser.add_argument("--config", type=Path, help="Configuration file path")
446
+
447
+ # Config test command
448
+ test_parser = config_subparsers.add_parser("test", help="Test server connectivity")
449
+ test_parser.add_argument("name", nargs="?", help="Server name to test (uses default if not specified)")
450
+ test_parser.add_argument("--config", type=Path, help="Configuration file path")
451
+
452
+ # Config health command
453
+ health_parser = config_subparsers.add_parser("health", help="Check server health")
454
+ health_parser.add_argument("--monitor", action="store_true", help="Start continuous monitoring")
455
+ health_parser.add_argument("--config", type=Path, help="Configuration file path")
456
+
457
+ # Config enable command
458
+ enable_parser = config_subparsers.add_parser("enable", help="Enable a server")
459
+ enable_parser.add_argument("name", help="Server name to enable")
460
+ enable_parser.add_argument("--config", type=Path, help="Configuration file path")
461
+
462
+ # Config disable command
463
+ disable_parser = config_subparsers.add_parser("disable", help="Disable a server")
464
+ disable_parser.add_argument("name", help="Server name to disable")
465
+ disable_parser.add_argument("--config", type=Path, help="Configuration file path")
466
+
467
+ # Config set-default command
468
+ default_parser = config_subparsers.add_parser("set-default", help="Set default server")
469
+ default_parser.add_argument("name", help="Server name to set as default")
470
+ default_parser.add_argument("--config", type=Path, help="Configuration file path")
471
+
472
+ # Config validate command
473
+ validate_parser = config_subparsers.add_parser("validate", help="Validate configuration")
474
+ validate_parser.add_argument("--config", type=Path, help="Configuration file path")
475
+
476
+ # Add simplified parsers for remaining commands
477
+ config_subparsers.add_parser("benchmark", help="Benchmark server performance")
478
+ config_subparsers.add_parser("switch", help="Switch between server configurations")
479
+ config_subparsers.add_parser("discover", help="Discover configuration files")
480
+ config_subparsers.add_parser("export", help="Export configuration")
481
+ config_subparsers.add_parser("import", help="Import configuration")
482
+ config_subparsers.add_parser("template", help="Generate configuration templates")
483
+ config_subparsers.add_parser("batch-test", help="Test all enabled servers")
484
+
485
+ return config_parser
486
+
487
+
488
+ __all__ = ["config_command", "add_config_subparser"]
@@ -0,0 +1,57 @@
1
+ """MCP command module - handles Model Context Protocol server operations."""
2
+
3
+ import argparse
4
+ import os
5
+ from pathlib import Path
6
+
7
+
8
+ def mcp_command(args: argparse.Namespace) -> None:
9
+ """Execute the MCP server command.
10
+
11
+ Args:
12
+ args: Parsed command-line arguments containing database path
13
+ """
14
+ import subprocess
15
+ import sys
16
+
17
+ # Use the standalone MCP launcher that sets environment before any imports
18
+ mcp_launcher_path = Path(__file__).parent.parent.parent.parent.parent / "mcp_launcher.py"
19
+ cmd = [sys.executable, str(mcp_launcher_path), "--db", str(args.db)]
20
+
21
+ process = subprocess.run(
22
+ cmd,
23
+ stdin=sys.stdin,
24
+ stdout=sys.stdout,
25
+ stderr=sys.stderr # Allow stderr through for proper error handling
26
+ )
27
+
28
+ # Exit with the same code as the subprocess
29
+ sys.exit(process.returncode)
30
+
31
+
32
+ def add_mcp_subparser(subparsers) -> argparse.ArgumentParser:
33
+ """Add MCP command subparser to the main parser.
34
+
35
+ Args:
36
+ subparsers: Subparsers object from the main argument parser
37
+
38
+ Returns:
39
+ The configured MCP subparser
40
+ """
41
+ mcp_parser = subparsers.add_parser(
42
+ "mcp",
43
+ help="Run Model Context Protocol server",
44
+ description="Start the MCP server for integration with MCP-compatible clients"
45
+ )
46
+
47
+ mcp_parser.add_argument(
48
+ "--db",
49
+ type=Path,
50
+ default=Path.home() / ".cache" / "chunkhound" / "chunks.duckdb",
51
+ help="DuckDB database file path (default: ~/.cache/chunkhound/chunks.duckdb)",
52
+ )
53
+
54
+ return mcp_parser
55
+
56
+
57
+ __all__ = ["mcp_command", "add_mcp_subparser"]