claude-mpm 4.3.11__py3-none-any.whl → 4.3.12__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 (31) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/research.json +20 -8
  3. claude_mpm/agents/templates/web_qa.json +25 -10
  4. claude_mpm/cli/__init__.py +1 -0
  5. claude_mpm/cli/commands/mcp_command_router.py +11 -0
  6. claude_mpm/cli/commands/mcp_config.py +157 -0
  7. claude_mpm/cli/commands/mcp_external_commands.py +241 -0
  8. claude_mpm/cli/commands/mcp_install_commands.py +64 -23
  9. claude_mpm/cli/commands/mcp_setup_external.py +829 -0
  10. claude_mpm/cli/commands/run.py +70 -0
  11. claude_mpm/cli/commands/search.py +285 -0
  12. claude_mpm/cli/parsers/base_parser.py +13 -0
  13. claude_mpm/cli/parsers/mcp_parser.py +17 -0
  14. claude_mpm/cli/parsers/run_parser.py +5 -0
  15. claude_mpm/cli/parsers/search_parser.py +239 -0
  16. claude_mpm/constants.py +1 -0
  17. claude_mpm/core/unified_agent_registry.py +7 -0
  18. claude_mpm/services/agents/deployment/agent_deployment.py +28 -13
  19. claude_mpm/services/agents/deployment/agent_discovery_service.py +16 -6
  20. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +6 -4
  21. claude_mpm/services/cli/agent_cleanup_service.py +5 -0
  22. claude_mpm/services/mcp_config_manager.py +294 -0
  23. claude_mpm/services/mcp_gateway/config/configuration.py +17 -0
  24. claude_mpm/services/mcp_gateway/main.py +38 -0
  25. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +390 -0
  26. {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.12.dist-info}/METADATA +4 -1
  27. {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.12.dist-info}/RECORD +31 -24
  28. {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.12.dist-info}/WHEEL +0 -0
  29. {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.12.dist-info}/entry_points.txt +0 -0
  30. {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.12.dist-info}/licenses/LICENSE +0 -0
  31. {claude_mpm-4.3.11.dist-info → claude_mpm-4.3.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,829 @@
1
+ """MCP external services setup module.
2
+
3
+ This module handles the registration of external MCP services
4
+ (mcp-vector-search, mcp-browser) as separate MCP servers in Claude Desktop.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Dict, Optional, List, Tuple
13
+
14
+
15
+ class MCPExternalServicesSetup:
16
+ """Handles setup of external MCP services in Claude Desktop configuration."""
17
+
18
+ def get_project_services(self, project_path: Path) -> Dict:
19
+ """Get external services configuration for the current project.
20
+
21
+ Args:
22
+ project_path: Path to the project directory
23
+
24
+ Returns:
25
+ Dict: Configuration for external services
26
+ """
27
+ # Detect best command paths for services
28
+ mcp_browser_config = self._get_best_service_config("mcp-browser", project_path)
29
+ mcp_vector_search_config = self._get_best_service_config("mcp-vector-search", project_path)
30
+
31
+ return {
32
+ "mcp-vector-search": {
33
+ "package_name": "mcp-vector-search",
34
+ "module_name": "mcp_vector_search",
35
+ "description": "Semantic code search with vector embeddings",
36
+ "config": mcp_vector_search_config
37
+ },
38
+ "mcp-browser": {
39
+ "package_name": "mcp-browser",
40
+ "module_name": "mcp_browser",
41
+ "description": "Web browsing and content extraction",
42
+ "config": mcp_browser_config
43
+ }
44
+ }
45
+
46
+ def _get_best_service_config(self, service_name: str, project_path: Path) -> Dict:
47
+ """Get the best configuration for a service.
48
+
49
+ Priority order:
50
+ 1. Pipx installation (preferred for isolation and reliability)
51
+ 2. Local development installation (e.g., ~/Projects/managed/)
52
+ 3. Local project venv
53
+ 4. System Python
54
+
55
+ Args:
56
+ service_name: Name of the service
57
+ project_path: Path to the project directory
58
+
59
+ Returns:
60
+ Dict: Service configuration
61
+ """
62
+ # First try pipx (preferred for isolation and reliability)
63
+ pipx_config = self._get_pipx_config(service_name, project_path)
64
+ if pipx_config:
65
+ # Verify the executable actually exists before using it
66
+ command = pipx_config.get("command", "")
67
+ if Path(command).exists():
68
+ return pipx_config
69
+
70
+ # Then try local development installations
71
+ local_dev_config = self._get_local_dev_config(service_name, project_path)
72
+ if local_dev_config:
73
+ # Verify the command exists
74
+ command = local_dev_config.get("command", "")
75
+ if Path(command).exists():
76
+ return local_dev_config
77
+
78
+ # Then try local venv if exists
79
+ venv_config = self._get_venv_config(service_name, project_path)
80
+ if venv_config:
81
+ command = venv_config.get("command", "")
82
+ if Path(command).exists():
83
+ return venv_config
84
+
85
+ # Fall back to system Python
86
+ return self._get_system_config(service_name, project_path)
87
+
88
+ def _get_local_dev_config(self, service_name: str, project_path: Path) -> Optional[Dict]:
89
+ """Get configuration for a locally developed service.
90
+
91
+ Checks common development locations like ~/Projects/managed/
92
+
93
+ Args:
94
+ service_name: Name of the service
95
+ project_path: Path to the project directory
96
+
97
+ Returns:
98
+ Configuration dict or None if not available
99
+ """
100
+ # Check common local development locations
101
+ dev_locations = [
102
+ Path.home() / "Projects" / "managed" / service_name,
103
+ Path.home() / "Projects" / service_name,
104
+ Path.home() / "Development" / service_name,
105
+ Path.home() / "dev" / service_name,
106
+ ]
107
+
108
+ for dev_path in dev_locations:
109
+ if not dev_path.exists():
110
+ continue
111
+
112
+ # Check for venv in the development location
113
+ venv_paths = [
114
+ dev_path / ".venv" / "bin" / "python",
115
+ dev_path / "venv" / "bin" / "python",
116
+ dev_path / "env" / "bin" / "python",
117
+ ]
118
+
119
+ for venv_python in venv_paths:
120
+ if venv_python.exists():
121
+ # Special handling for mcp-browser
122
+ if service_name == "mcp-browser":
123
+ # First check for mcp-browser binary in the same directory as python
124
+ mcp_browser_binary = venv_python.parent / "mcp-browser"
125
+ if mcp_browser_binary.exists():
126
+ return {
127
+ "type": "stdio",
128
+ "command": str(mcp_browser_binary),
129
+ "args": ["mcp"],
130
+ "env": {
131
+ "MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser")
132
+ }
133
+ }
134
+
135
+ # Then check for mcp-server.py
136
+ mcp_server = dev_path / "mcp-server.py"
137
+ if mcp_server.exists():
138
+ return {
139
+ "type": "stdio",
140
+ "command": str(venv_python),
141
+ "args": [str(mcp_server)],
142
+ "env": {
143
+ "MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser"),
144
+ "PYTHONPATH": str(dev_path)
145
+ }
146
+ }
147
+
148
+ # Check if the package is installed in this venv
149
+ module_name = service_name.replace("-", "_")
150
+ try:
151
+ result = subprocess.run(
152
+ [str(venv_python), "-c", f"import {module_name}"],
153
+ capture_output=True,
154
+ timeout=5
155
+ )
156
+ if result.returncode == 0:
157
+ # Use special configuration for local dev
158
+ if service_name == "mcp-vector-search":
159
+ return {
160
+ "type": "stdio",
161
+ "command": str(venv_python),
162
+ "args": ["-m", "mcp_vector_search.mcp.server", str(project_path)],
163
+ "env": {}
164
+ }
165
+ elif service_name == "mcp-browser":
166
+ # Fallback for mcp-browser without mcp-server.py
167
+ return {
168
+ "type": "stdio",
169
+ "command": str(venv_python),
170
+ "args": ["-m", "mcp_browser", "mcp"],
171
+ "env": {
172
+ "MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser"),
173
+ "PYTHONPATH": str(dev_path)
174
+ }
175
+ }
176
+ except:
177
+ continue
178
+
179
+ return None
180
+
181
+ def _get_venv_config(self, service_name: str, project_path: Path) -> Optional[Dict]:
182
+ """Get configuration for a service in the local virtual environment.
183
+
184
+ Args:
185
+ service_name: Name of the service
186
+ project_path: Path to the project directory
187
+
188
+ Returns:
189
+ Configuration dict or None if not available
190
+ """
191
+ # Check common venv locations
192
+ venv_paths = [
193
+ project_path / "venv" / "bin" / "python",
194
+ project_path / ".venv" / "bin" / "python",
195
+ project_path / "env" / "bin" / "python",
196
+ ]
197
+
198
+ for venv_python in venv_paths:
199
+ if venv_python.exists():
200
+ # Check if the package is installed in this venv
201
+ module_name = service_name.replace("-", "_")
202
+ try:
203
+ result = subprocess.run(
204
+ [str(venv_python), "-c", f"import {module_name}"],
205
+ capture_output=True,
206
+ timeout=5
207
+ )
208
+ if result.returncode == 0:
209
+ return self._create_service_config(service_name, str(venv_python), project_path)
210
+ except:
211
+ continue
212
+
213
+ return None
214
+
215
+ def _get_system_config(self, service_name: str, project_path: Path) -> Dict:
216
+ """Get configuration using system Python.
217
+
218
+ Args:
219
+ service_name: Name of the service
220
+ project_path: Path to the project directory
221
+
222
+ Returns:
223
+ Configuration dict
224
+ """
225
+ return self._create_service_config(service_name, sys.executable, project_path)
226
+
227
+ def _create_service_config(self, service_name: str, python_path: str, project_path: Path) -> Dict:
228
+ """Create service configuration for the given Python executable.
229
+
230
+ Args:
231
+ service_name: Name of the service
232
+ python_path: Path to Python executable
233
+ project_path: Path to the project directory
234
+
235
+ Returns:
236
+ Configuration dict
237
+ """
238
+ if service_name == "mcp-browser":
239
+ # Check if mcp-browser binary exists (for pipx installations)
240
+ binary_path = Path(python_path).parent / "mcp-browser"
241
+ if binary_path.exists():
242
+ return {
243
+ "type": "stdio",
244
+ "command": str(binary_path),
245
+ "args": ["mcp"],
246
+ "env": {"MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser")}
247
+ }
248
+ else:
249
+ # Use Python module invocation
250
+ return {
251
+ "type": "stdio",
252
+ "command": python_path,
253
+ "args": ["-m", "mcp_browser", "mcp"],
254
+ "env": {"MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser")}
255
+ }
256
+ elif service_name == "mcp-vector-search":
257
+ return {
258
+ "type": "stdio",
259
+ "command": python_path,
260
+ "args": ["-m", "mcp_vector_search.mcp.server", str(project_path)],
261
+ "env": {}
262
+ }
263
+ else:
264
+ # Generic configuration for other services
265
+ module_name = service_name.replace("-", "_")
266
+ return {
267
+ "type": "stdio",
268
+ "command": python_path,
269
+ "args": ["-m", module_name],
270
+ "env": {}
271
+ }
272
+
273
+ def detect_mcp_installations(self) -> Dict[str, Dict]:
274
+ """Detect all MCP service installations and their locations.
275
+
276
+ Returns:
277
+ Dict mapping service name to installation info:
278
+ {
279
+ "service-name": {
280
+ "type": "local_dev" | "pipx" | "venv" | "system" | "not_installed",
281
+ "path": "/path/to/installation",
282
+ "config": {...} # Ready-to-use configuration
283
+ }
284
+ }
285
+ """
286
+ installations = {}
287
+ project_path = Path.cwd()
288
+
289
+ for service_name in ["mcp-browser", "mcp-vector-search"]:
290
+ # Try each detection method in priority order
291
+ local_dev_config = self._get_local_dev_config(service_name, project_path)
292
+ if local_dev_config:
293
+ installations[service_name] = {
294
+ "type": "local_dev",
295
+ "path": local_dev_config["command"],
296
+ "config": local_dev_config
297
+ }
298
+ continue
299
+
300
+ pipx_config = self._get_pipx_config(service_name, project_path)
301
+ if pipx_config:
302
+ # Verify the command actually exists before reporting it as available
303
+ command_path = Path(pipx_config["command"])
304
+ if command_path.exists():
305
+ installations[service_name] = {
306
+ "type": "pipx",
307
+ "path": pipx_config["command"],
308
+ "config": pipx_config
309
+ }
310
+ continue
311
+
312
+ venv_config = self._get_venv_config(service_name, project_path)
313
+ if venv_config:
314
+ installations[service_name] = {
315
+ "type": "venv",
316
+ "path": venv_config["command"],
317
+ "config": venv_config
318
+ }
319
+ continue
320
+
321
+ # Check if available in system Python
322
+ module_name = service_name.replace("-", "_")
323
+ if self._check_python_package(module_name):
324
+ system_config = self._get_system_config(service_name, project_path)
325
+ installations[service_name] = {
326
+ "type": "system",
327
+ "path": system_config["command"],
328
+ "config": system_config
329
+ }
330
+ else:
331
+ installations[service_name] = {
332
+ "type": "not_installed",
333
+ "path": None,
334
+ "config": None
335
+ }
336
+
337
+ return installations
338
+
339
+ def update_mcp_json_with_detected(self, force: bool = False) -> bool:
340
+ """Update .mcp.json with auto-detected service configurations.
341
+
342
+ Args:
343
+ force: Whether to overwrite existing configurations
344
+
345
+ Returns:
346
+ bool: True if configuration was updated successfully
347
+ """
348
+ print("\n🔍 Auto-detecting MCP service installations...")
349
+ print("=" * 50)
350
+
351
+ installations = self.detect_mcp_installations()
352
+
353
+ # Display detected installations
354
+ for service_name, info in installations.items():
355
+ print(f"\n{service_name}:")
356
+ if info["type"] == "not_installed":
357
+ print(f" ❌ Not installed")
358
+ else:
359
+ type_emoji = {
360
+ "local_dev": "🔧",
361
+ "pipx": "📦",
362
+ "venv": "🐍",
363
+ "system": "💻"
364
+ }.get(info["type"], "❓")
365
+ print(f" {type_emoji} Type: {info['type']}")
366
+ print(f" 📍 Path: {info['path']}")
367
+
368
+ # Load current configuration
369
+ config_path = Path.cwd() / ".mcp.json"
370
+ config = self._load_config(config_path)
371
+ if not config:
372
+ print("\n❌ Failed to load configuration")
373
+ return False
374
+
375
+ # Update configurations
376
+ updated = False
377
+ for service_name, info in installations.items():
378
+ if info["type"] == "not_installed":
379
+ continue
380
+
381
+ # Check if already configured
382
+ if service_name in config.get("mcpServers", {}) and not force:
383
+ print(f"\n⚠️ {service_name} already configured, skipping (use --force to override)")
384
+ continue
385
+
386
+ # Update configuration
387
+ if "mcpServers" not in config:
388
+ config["mcpServers"] = {}
389
+
390
+ config["mcpServers"][service_name] = info["config"]
391
+ print(f"\n✅ Updated {service_name} configuration")
392
+ updated = True
393
+
394
+ # Save configuration if updated
395
+ if updated:
396
+ if self._save_config(config, config_path):
397
+ print("\n✅ Successfully updated .mcp.json with detected configurations")
398
+ return True
399
+ else:
400
+ print("\n❌ Failed to save configuration")
401
+ return False
402
+ else:
403
+ print("\n📌 No updates needed")
404
+ return True
405
+
406
+ def __init__(self, logger):
407
+ """Initialize the external services setup handler."""
408
+ self.logger = logger
409
+ self._pipx_path = Path.home() / ".local" / "pipx" / "venvs"
410
+
411
+ def setup_external_services(self, force: bool = False) -> bool:
412
+ """Setup external MCP services in project .mcp.json file.
413
+
414
+ Args:
415
+ force: Whether to overwrite existing configurations
416
+
417
+ Returns:
418
+ bool: True if all services were set up successfully
419
+ """
420
+ print("\n📦 Setting up External MCP Services")
421
+ print("=" * 50)
422
+
423
+ # Use project-level .mcp.json file
424
+ project_path = Path.cwd()
425
+ config_path = project_path / ".mcp.json"
426
+
427
+ print(f"📁 Project directory: {project_path}")
428
+ print(f"📄 Using config: {config_path}")
429
+
430
+ # Load existing configuration
431
+ config = self._load_config(config_path)
432
+ if config is None:
433
+ print("❌ Failed to load configuration")
434
+ return False
435
+
436
+ # Ensure mcpServers section exists
437
+ if "mcpServers" not in config:
438
+ config["mcpServers"] = {}
439
+
440
+ # Setup each external service
441
+ success_count = 0
442
+ for service_name, service_info in self.get_project_services(project_path).items():
443
+ if self._setup_service(config, service_name, service_info, force):
444
+ success_count += 1
445
+
446
+ # Save the updated configuration
447
+ if success_count > 0:
448
+ if self._save_config(config, config_path):
449
+ print(f"\n✅ Successfully configured {success_count} external services in .mcp.json")
450
+ print(f"\n📌 Note: Claude Desktop will automatically load these services")
451
+ print(f" when you open this project directory in Claude Desktop.")
452
+ return True
453
+ else:
454
+ print("❌ Failed to save configuration")
455
+ return False
456
+ else:
457
+ print("\n⚠️ No external services were configured")
458
+ return False
459
+
460
+ def _setup_service(
461
+ self,
462
+ config: Dict,
463
+ service_name: str,
464
+ service_info: Dict,
465
+ force: bool
466
+ ) -> bool:
467
+ """Setup a single external MCP service.
468
+
469
+ Args:
470
+ config: The Claude Desktop configuration
471
+ service_name: Name of the service to setup
472
+ service_info: Service configuration information
473
+ force: Whether to overwrite existing configuration
474
+
475
+ Returns:
476
+ bool: True if service was set up successfully
477
+ """
478
+ print(f"\n📦 Setting up {service_name}...")
479
+
480
+ # Check if already configured
481
+ if service_name in config["mcpServers"] and not force:
482
+ existing_config = config["mcpServers"][service_name]
483
+ print(f" ⚠️ {service_name} already configured")
484
+ print(f" Current command: {existing_config.get('command')}")
485
+ print(f" Current args: {existing_config.get('args')}")
486
+
487
+ # Check if it's using a local development path
488
+ command = str(existing_config.get('command', ''))
489
+ if any(path in command for path in ["/Projects/managed/", "/Projects/", "/Development/"]):
490
+ print(" 📍 Using local development version")
491
+ response = input(" Keep local development version? (Y/n): ").strip().lower()
492
+ if response not in ["n", "no"]:
493
+ print(f" ✅ Keeping existing local configuration for {service_name}")
494
+ return True # Consider it successfully configured
495
+ else:
496
+ response = input(" Overwrite? (y/N): ").strip().lower()
497
+ if response not in ["y", "yes"]:
498
+ print(f" ⏭️ Skipping {service_name}")
499
+ return False
500
+
501
+ # Check if Python package is available
502
+ module_name = service_info.get("module_name", service_name.replace("-", "_"))
503
+ if not self._check_python_package(module_name):
504
+ print(f" ⚠️ Python package {service_info['package_name']} not installed")
505
+ print(f" ℹ️ Installing {service_info['package_name']}...")
506
+ if not self._install_python_package(service_info['package_name']):
507
+ print(f" ❌ Failed to install {service_info['package_name']}")
508
+ print(f" ℹ️ Install manually with: pip install {service_info['package_name']}")
509
+ return False
510
+
511
+ # Add service configuration
512
+ config["mcpServers"][service_name] = service_info["config"]
513
+ print(f" ✅ Configured {service_name}")
514
+ print(f" Command: {service_info['config']['command']}")
515
+ print(f" Args: {service_info['config']['args']}")
516
+ if "env" in service_info["config"]:
517
+ print(f" Environment: {list(service_info['config']['env'].keys())}")
518
+
519
+ return True
520
+
521
+
522
+ def check_and_install_pip_packages(self) -> bool:
523
+ """Check and install Python packages for external services.
524
+
525
+ Returns:
526
+ bool: True if all packages are available
527
+ """
528
+ print("\n🐍 Checking Python packages for external services...")
529
+
530
+ packages_to_check = [
531
+ ("mcp-vector-search", "mcp_vector_search"),
532
+ ("mcp-browser", "mcp_browser")
533
+ ]
534
+
535
+ all_installed = True
536
+ for package_name, module_name in packages_to_check:
537
+ if self._check_python_package(module_name):
538
+ print(f" ✅ {package_name} is installed")
539
+ else:
540
+ print(f" 📦 Installing {package_name}...")
541
+ if self._install_python_package(package_name):
542
+ print(f" ✅ Successfully installed {package_name}")
543
+ else:
544
+ print(f" ❌ Failed to install {package_name}")
545
+ all_installed = False
546
+
547
+ return all_installed
548
+
549
+ def _check_python_package(self, module_name: str) -> bool:
550
+ """Check if a Python package is installed.
551
+
552
+ Args:
553
+ module_name: Name of the module to import
554
+
555
+ Returns:
556
+ bool: True if package is installed
557
+ """
558
+ try:
559
+ import importlib.util
560
+ spec = importlib.util.find_spec(module_name)
561
+ return spec is not None
562
+ except (ImportError, ModuleNotFoundError):
563
+ return False
564
+
565
+ def _install_python_package(self, package_name: str) -> bool:
566
+ """Install a Python package using pip.
567
+
568
+ Args:
569
+ package_name: Name of the package to install
570
+
571
+ Returns:
572
+ bool: True if installation was successful
573
+ """
574
+ try:
575
+ result = subprocess.run(
576
+ [sys.executable, "-m", "pip", "install", package_name],
577
+ capture_output=True,
578
+ text=True,
579
+ timeout=60
580
+ )
581
+ return result.returncode == 0
582
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
583
+ return False
584
+
585
+ def _load_config(self, config_path: Path) -> Optional[Dict]:
586
+ """Load MCP configuration.
587
+
588
+ Args:
589
+ config_path: Path to the configuration file
590
+
591
+ Returns:
592
+ Optional[Dict]: Configuration dictionary or None if failed
593
+ """
594
+ try:
595
+ if config_path.exists():
596
+ with open(config_path) as f:
597
+ config = json.load(f)
598
+ # Ensure mcpServers key exists
599
+ if "mcpServers" not in config:
600
+ config["mcpServers"] = {}
601
+ return config
602
+ else:
603
+ # Create new configuration
604
+ print(f" 📝 Creating new .mcp.json file")
605
+ return {"mcpServers": {}}
606
+ except (OSError, json.JSONDecodeError) as e:
607
+ print(f"❌ Error loading config: {e}")
608
+ # Try to return empty config instead of None
609
+ return {"mcpServers": {}}
610
+
611
+ def _save_config(self, config: Dict, config_path: Path) -> bool:
612
+ """Save MCP configuration.
613
+
614
+ Args:
615
+ config: Configuration dictionary
616
+ config_path: Path to save the configuration
617
+
618
+ Returns:
619
+ bool: True if save was successful
620
+ """
621
+ try:
622
+ # Ensure directory exists
623
+ config_path.parent.mkdir(parents=True, exist_ok=True)
624
+
625
+ # Create backup if file exists
626
+ if config_path.exists():
627
+ from datetime import datetime
628
+ backup_path = config_path.parent / f".mcp.backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
629
+ import shutil
630
+ shutil.copy2(config_path, backup_path)
631
+ print(f" 📁 Created backup: {backup_path}")
632
+
633
+ # Write configuration with proper formatting
634
+ with open(config_path, "w") as f:
635
+ json.dump(config, f, indent=2)
636
+ f.write("\n") # Add newline at end of file
637
+
638
+ print(f" 💾 Saved configuration to {config_path}")
639
+ return True
640
+
641
+ except Exception as e:
642
+ print(f"❌ Error saving config: {e}")
643
+ return False
644
+
645
+ def _get_pipx_config(self, package_name: str, project_path: Path) -> Optional[Dict]:
646
+ """Get configuration for a pipx-installed package.
647
+
648
+ Args:
649
+ package_name: Name of the package (e.g., "mcp-browser")
650
+ project_path: Path to the project directory
651
+
652
+ Returns:
653
+ Configuration dict for the service or None if not found
654
+ """
655
+ pipx_venv = self._pipx_path / package_name
656
+ if not pipx_venv.exists():
657
+ return None
658
+
659
+ if package_name == "mcp-browser":
660
+ # mcp-browser uses Python module invocation for MCP mode
661
+ python_path = pipx_venv / "bin" / "python"
662
+ if python_path.exists():
663
+ # Check if module is importable
664
+ try:
665
+ result = subprocess.run(
666
+ [str(python_path), "-c", "import mcp_browser.cli.main"],
667
+ capture_output=True,
668
+ timeout=5
669
+ )
670
+ if result.returncode == 0:
671
+ return {
672
+ "type": "stdio",
673
+ "command": str(python_path),
674
+ "args": ["-m", "mcp_browser.cli.main", "mcp"],
675
+ "env": {"MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser")}
676
+ }
677
+ except:
678
+ pass
679
+ elif package_name == "mcp-vector-search":
680
+ # mcp-vector-search uses Python module invocation
681
+ python_path = pipx_venv / "bin" / "python"
682
+ if python_path.exists():
683
+ # Check if module is importable
684
+ try:
685
+ result = subprocess.run(
686
+ [str(python_path), "-c", "import mcp_vector_search"],
687
+ capture_output=True,
688
+ timeout=5
689
+ )
690
+ if result.returncode == 0:
691
+ return {
692
+ "type": "stdio",
693
+ "command": str(python_path),
694
+ "args": ["-m", "mcp_vector_search.mcp.server", str(project_path)],
695
+ "env": {}
696
+ }
697
+ except:
698
+ pass
699
+
700
+ return None
701
+
702
+ def _check_pipx_installation(self, package_name: str) -> Tuple[bool, str]:
703
+ """Check if a package is installed via pipx.
704
+
705
+ Args:
706
+ package_name: Name of the package to check
707
+
708
+ Returns:
709
+ Tuple of (is_installed, installation_type)
710
+ """
711
+ pipx_venv = self._pipx_path / package_name
712
+ if pipx_venv.exists():
713
+ return True, "pipx"
714
+
715
+ # Check if available as Python module
716
+ module_name = package_name.replace("-", "_")
717
+ if self._check_python_package(module_name):
718
+ return True, "pip"
719
+
720
+ return False, "none"
721
+
722
+ def fix_browser_configuration(self) -> bool:
723
+ """Quick fix for mcp-browser configuration in project .mcp.json.
724
+
725
+ Updates only the mcp-browser configuration in the project's .mcp.json
726
+ to use the best available installation (pipx preferred).
727
+
728
+ Returns:
729
+ bool: True if configuration was updated successfully
730
+ """
731
+ print("\n🔧 Fixing mcp-browser Configuration")
732
+ print("=" * 50)
733
+
734
+ project_path = Path.cwd()
735
+ config_path = project_path / ".mcp.json"
736
+
737
+ print(f"📁 Project directory: {project_path}")
738
+ print(f"📄 Using config: {config_path}")
739
+
740
+ # Check if mcp-browser is installed
741
+ is_installed, install_type = self._check_pipx_installation("mcp-browser")
742
+ if not is_installed:
743
+ print("❌ mcp-browser is not installed")
744
+ print(" Install with: pipx install mcp-browser")
745
+ return False
746
+
747
+ if install_type != "pipx":
748
+ print("⚠️ mcp-browser is not installed via pipx")
749
+ print(" For best results, install with: pipx install mcp-browser")
750
+
751
+ # Get best configuration for mcp-browser
752
+ browser_config = self._get_best_service_config("mcp-browser", project_path)
753
+ if not browser_config:
754
+ print("❌ Could not determine mcp-browser configuration")
755
+ return False
756
+
757
+ # Load project configuration
758
+ config = self._load_config(config_path)
759
+ if not config:
760
+ print("❌ Failed to load configuration")
761
+ return False
762
+
763
+ # Update mcp-browser configuration
764
+ if "mcpServers" not in config:
765
+ config["mcpServers"] = {}
766
+
767
+ config["mcpServers"]["mcp-browser"] = browser_config
768
+
769
+ # Save configuration
770
+ if self._save_config(config, config_path):
771
+ print("✅ Successfully updated mcp-browser configuration in .mcp.json")
772
+ print(f" Command: {browser_config['command']}")
773
+ print(f" Args: {browser_config['args']}")
774
+ print("\n📌 Note: Claude Desktop will automatically use this configuration")
775
+ print(" when you open this project directory.")
776
+ return True
777
+ else:
778
+ print("❌ Failed to save configuration")
779
+ return False
780
+
781
+
782
+ def list_external_services(self) -> None:
783
+ """List all available external MCP services and their status."""
784
+ print("\n📋 Available External MCP Services")
785
+ print("=" * 50)
786
+
787
+ # Check project-level .mcp.json
788
+ project_path = Path.cwd()
789
+ mcp_config_path = project_path / ".mcp.json"
790
+ mcp_config = {}
791
+
792
+ if mcp_config_path.exists():
793
+ try:
794
+ with open(mcp_config_path) as f:
795
+ mcp_config = json.load(f)
796
+ print(f"\n📁 Project MCP config: {mcp_config_path}")
797
+ except:
798
+ print(f"\n⚠️ Could not read project .mcp.json")
799
+ else:
800
+ print(f"\n📝 No .mcp.json found in project directory")
801
+
802
+ # Get service configurations for this project
803
+ services = self.get_project_services(project_path)
804
+
805
+ for service_name, service_info in services.items():
806
+ print(f"\n{service_name}:")
807
+ print(f" Description: {service_info['description']}")
808
+ print(f" Python Package: {service_info['package_name']}")
809
+
810
+ # Check if configured in .mcp.json
811
+ if mcp_config.get("mcpServers", {}).get(service_name):
812
+ print(f" Project Status: ✅ Configured in .mcp.json")
813
+ service_config = mcp_config["mcpServers"][service_name]
814
+ print(f" Command: {service_config.get('command')}")
815
+ if service_config.get('args'):
816
+ print(f" Args: {service_config.get('args')}")
817
+ else:
818
+ print(f" Project Status: ❌ Not configured in .mcp.json")
819
+
820
+ # Check installation type
821
+ is_installed, install_type = self._check_pipx_installation(service_name)
822
+ if is_installed:
823
+ if install_type == "pipx":
824
+ print(f" Installation: ✅ Installed via pipx (recommended)")
825
+ else:
826
+ print(f" Installation: ✅ Installed via pip")
827
+ else:
828
+ print(f" Installation: ❌ Not installed")
829
+ print(f" Install with: pipx install {service_info['package_name']}")