claude-mpm 5.6.23__py3-none-any.whl → 5.6.72__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (80) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/auth/__init__.py +35 -0
  3. claude_mpm/auth/callback_server.py +328 -0
  4. claude_mpm/auth/models.py +104 -0
  5. claude_mpm/auth/oauth_manager.py +266 -0
  6. claude_mpm/auth/providers/__init__.py +12 -0
  7. claude_mpm/auth/providers/base.py +165 -0
  8. claude_mpm/auth/providers/google.py +261 -0
  9. claude_mpm/auth/token_storage.py +252 -0
  10. claude_mpm/cli/commands/commander.py +6 -6
  11. claude_mpm/cli/commands/mcp.py +29 -17
  12. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  13. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  14. claude_mpm/cli/commands/oauth.py +481 -0
  15. claude_mpm/cli/executor.py +9 -0
  16. claude_mpm/cli/helpers.py +1 -1
  17. claude_mpm/cli/parsers/base_parser.py +13 -0
  18. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  19. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  20. claude_mpm/cli/startup.py +150 -33
  21. claude_mpm/cli/startup_display.py +3 -2
  22. claude_mpm/commander/chat/cli.py +5 -2
  23. claude_mpm/commander/chat/commands.py +42 -16
  24. claude_mpm/commander/chat/repl.py +1581 -70
  25. claude_mpm/commander/events/manager.py +61 -1
  26. claude_mpm/commander/frameworks/base.py +87 -0
  27. claude_mpm/commander/frameworks/mpm.py +9 -14
  28. claude_mpm/commander/git/__init__.py +5 -0
  29. claude_mpm/commander/git/worktree_manager.py +212 -0
  30. claude_mpm/commander/instance_manager.py +428 -13
  31. claude_mpm/commander/models/events.py +6 -0
  32. claude_mpm/commander/persistence/state_store.py +95 -1
  33. claude_mpm/commander/tmux_orchestrator.py +3 -2
  34. claude_mpm/constants.py +5 -0
  35. claude_mpm/core/hook_manager.py +2 -1
  36. claude_mpm/core/logging_utils.py +4 -2
  37. claude_mpm/core/output_style_manager.py +5 -2
  38. claude_mpm/core/socketio_pool.py +34 -10
  39. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  40. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  41. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  42. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  43. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  44. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
  45. claude_mpm/hooks/claude_hooks/event_handlers.py +206 -94
  46. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  47. claude_mpm/hooks/claude_hooks/installer.py +175 -51
  48. claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
  49. claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
  50. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  51. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  52. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  53. claude_mpm/hooks/claude_hooks/services/__pycache__/container.cpython-311.pyc +0 -0
  54. claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.cpython-311.pyc +0 -0
  55. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  56. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  57. claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
  58. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
  59. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  60. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  61. claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
  62. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
  63. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  64. claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
  65. claude_mpm/init.py +21 -14
  66. claude_mpm/mcp/__init__.py +9 -0
  67. claude_mpm/mcp/google_workspace_server.py +610 -0
  68. claude_mpm/scripts/claude-hook-handler.sh +3 -3
  69. claude_mpm/services/command_deployment_service.py +44 -26
  70. claude_mpm/services/hook_installer_service.py +77 -8
  71. claude_mpm/services/mcp_config_manager.py +99 -19
  72. claude_mpm/services/mcp_service_registry.py +294 -0
  73. claude_mpm/services/monitor/server.py +6 -1
  74. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/METADATA +24 -1
  75. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/RECORD +80 -60
  76. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/WHEEL +1 -1
  77. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/entry_points.txt +2 -0
  78. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/licenses/LICENSE +0 -0
  79. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  80. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,14 @@
1
- """Service for deploying MPM slash commands to user's Claude configuration.
1
+ """Service for managing MPM slash commands in user's Claude configuration.
2
2
 
3
- This service handles:
4
- 1. Copying command markdown files from source to user's ~/.claude/commands directory
5
- 2. Creating the commands directory if it doesn't exist
6
- 3. Overwriting existing commands to ensure they're up-to-date
7
- 4. Parsing and validating YAML frontmatter for namespace metadata (Phase 1 - 1M-400)
3
+ DEPRECATED: User-level commands in ~/.claude/commands/ are deprecated.
4
+ Project-level skills (.claude/skills/) are now the only source for commands.
5
+
6
+ This service now only handles:
7
+ 1. Cleanup of deprecated commands from previous versions
8
+ 2. Cleanup of stale commands that no longer exist in source
9
+ 3. Parsing and validating YAML frontmatter (for internal use)
10
+
11
+ New command deployment is intentionally disabled - see deploy_commands_on_startup().
8
12
  """
9
13
 
10
14
  from pathlib import Path
@@ -17,20 +21,34 @@ from claude_mpm.core.logger import get_logger
17
21
 
18
22
 
19
23
  class CommandDeploymentService(BaseService):
20
- """Service for deploying MPM slash commands."""
24
+ """Service for managing MPM slash commands (cleanup only - deployment deprecated)."""
21
25
 
22
- # Deprecated commands that have been replaced (cleanup on startup)
26
+ # Deprecated commands that should be removed from ~/.claude/commands/
27
+ # ALL user-level commands are now deprecated - project-level skills are the only source
23
28
  DEPRECATED_COMMANDS = [
29
+ # Legacy deprecated commands (historical)
24
30
  "mpm-agents.md", # Replaced by mpm-agents-list.md
25
31
  "mpm-auto-configure.md", # Replaced by mpm-agents-auto-configure.md
26
32
  "mpm-config-view.md", # Replaced by mpm-config.md
27
33
  "mpm-resume.md", # Replaced by mpm-session-resume.md
28
34
  "mpm-ticket.md", # Replaced by mpm-ticket-view.md
29
- # Removed - consolidated into /mpm-configure
30
35
  "mpm-agents-list.md", # Consolidated into /mpm-configure
31
36
  "mpm-agents-detect.md", # Consolidated into /mpm-configure
32
37
  "mpm-agents-auto-configure.md", # Consolidated into /mpm-configure
33
38
  "mpm-agents-recommend.md", # Consolidated into /mpm-configure
39
+ # ALL user-level commands are now deprecated (use project-level skills)
40
+ "mpm.md",
41
+ "mpm-config.md",
42
+ "mpm-doctor.md",
43
+ "mpm-help.md",
44
+ "mpm-init.md",
45
+ "mpm-monitor.md",
46
+ "mpm-organize.md",
47
+ "mpm-postmortem.md",
48
+ "mpm-session-resume.md",
49
+ "mpm-status.md",
50
+ "mpm-ticket-view.md",
51
+ "mpm-version.md",
34
52
  ]
35
53
 
36
54
  def __init__(self):
@@ -414,33 +432,33 @@ class CommandDeploymentService(BaseService):
414
432
  def deploy_commands_on_startup(force: bool = False) -> None:
415
433
  """Convenience function to deploy commands during startup.
416
434
 
417
- This function:
418
- 1. Removes deprecated commands that have been replaced
419
- 2. Removes stale commands that no longer exist in source
420
- 3. Deploys current command files
435
+ DEPRECATED: User-level commands in ~/.claude/commands/ are deprecated.
436
+ Project-level skills should be the only source for commands.
437
+
438
+ This function now only cleans up any existing deprecated/stale commands
439
+ without deploying new ones.
421
440
 
422
441
  Args:
423
- force: Force deployment even if files exist
442
+ force: Force deployment even if files exist (ignored - deployment disabled)
424
443
  """
425
- service = CommandDeploymentService()
426
444
  logger = get_logger("startup")
445
+ logger.debug(
446
+ "User-level command deployment is deprecated - "
447
+ "project-level skills are the only command source"
448
+ )
427
449
 
428
- # Clean up deprecated commands FIRST (known old commands)
450
+ # Still clean up any lingering deprecated/stale commands from previous versions
451
+ service = CommandDeploymentService()
452
+
453
+ # Clean up deprecated commands
429
454
  deprecated_count = service.remove_deprecated_commands()
430
455
  if deprecated_count > 0:
431
456
  logger.info(f"Cleaned up {deprecated_count} deprecated command(s)")
432
457
 
433
- # Clean up stale commands SECOND (deployed but not in source anymore)
458
+ # Clean up stale commands
434
459
  stale_count = service.remove_stale_commands()
435
460
  if stale_count > 0:
436
461
  logger.info(f"Cleaned up {stale_count} stale command(s)")
437
462
 
438
- # Deploy current commands LAST
439
- result = service.deploy_commands(force=force)
440
-
441
- if result["deployed"]:
442
- logger.info(f"MPM commands deployed: {', '.join(result['deployed'])}")
443
-
444
- if result["errors"]:
445
- for error in result["errors"]:
446
- logger.warning(f"Command deployment issue: {error}")
463
+ # NOTE: Deployment of new commands is intentionally disabled.
464
+ # Project-level skills (.claude/skills/) are the only source for commands.
@@ -20,8 +20,13 @@ class HookInstallerService:
20
20
  def __init__(self):
21
21
  """Initialize the hook installer service."""
22
22
  self.logger = get_logger(__name__)
23
- self.claude_dir = Path.home() / ".claude"
24
- self.settings_file = self.claude_dir / "settings.json"
23
+ # Use project-level paths, NEVER global ~/.claude/settings.json
24
+ # This ensures hooks are scoped to the current project only
25
+ self.project_root = Path.cwd()
26
+ self.claude_dir = self.project_root / ".claude"
27
+ # Use settings.local.json for project-level hook settings
28
+ # Claude Code reads project-level settings from .claude/settings.local.json
29
+ self.settings_file = self.claude_dir / "settings.local.json"
25
30
 
26
31
  def is_hooks_configured(self) -> bool:
27
32
  """Check if hooks are configured in Claude settings.
@@ -299,16 +304,77 @@ class HookInstallerService:
299
304
  self.logger.debug("Creating new Claude settings")
300
305
 
301
306
  # Configure hooks
302
- hook_config = {
303
- "matcher": "*",
304
- "hooks": [{"type": "command", "command": hook_script_path}],
305
- }
307
+ new_hook_command = {"type": "command", "command": hook_script_path}
306
308
 
307
309
  # Update settings
308
310
  if "hooks" not in settings:
309
311
  settings["hooks"] = {}
310
312
 
311
- # Add hooks for all event types
313
+ def is_our_hook(cmd: Dict[str, Any]) -> bool:
314
+ """Check if a hook command belongs to claude-mpm."""
315
+ if cmd.get("type") != "command":
316
+ return False
317
+ command = cmd.get("command", "")
318
+ return (
319
+ "hook_wrapper.sh" in command
320
+ or "claude-hook-handler.sh" in command
321
+ or "claude-mpm" in command
322
+ )
323
+
324
+ def merge_hooks_for_event(
325
+ existing_hooks: list, hook_command: Dict[str, Any]
326
+ ) -> list:
327
+ """Merge new hook command into existing hooks without duplication.
328
+
329
+ Args:
330
+ existing_hooks: Current hooks configuration for an event type
331
+ hook_command: The claude-mpm hook command to add
332
+
333
+ Returns:
334
+ Updated hooks list with our hook merged in
335
+ """
336
+ # Check if our hook already exists in any existing hook config
337
+ our_hook_exists = False
338
+
339
+ for hook_config in existing_hooks:
340
+ if "hooks" in hook_config and isinstance(
341
+ hook_config["hooks"], list
342
+ ):
343
+ for hook in hook_config["hooks"]:
344
+ if is_our_hook(hook):
345
+ # Update existing hook command path (in case it changed)
346
+ hook["command"] = hook_command["command"]
347
+ our_hook_exists = True
348
+ break
349
+ if our_hook_exists:
350
+ break
351
+
352
+ if our_hook_exists:
353
+ # Our hook already exists, just return the updated list
354
+ return existing_hooks
355
+
356
+ # Our hook doesn't exist - need to add it
357
+ # Strategy: Add our hook to the first "*" matcher config, or create new
358
+ added = False
359
+
360
+ for hook_config in existing_hooks:
361
+ # Check if this config has matcher: "*"
362
+ if hook_config.get("matcher") == "*":
363
+ # Add our hook to this config's hooks array
364
+ if "hooks" not in hook_config:
365
+ hook_config["hooks"] = []
366
+ hook_config["hooks"].append(hook_command)
367
+ added = True
368
+ break
369
+
370
+ if not added:
371
+ # No suitable config found, create a new one
372
+ new_config = {"matcher": "*", "hooks": [hook_command]}
373
+ existing_hooks.append(new_config)
374
+
375
+ return existing_hooks
376
+
377
+ # Add hooks for all event types - MERGE instead of overwrite
312
378
  for event_type in [
313
379
  "UserPromptSubmit",
314
380
  "PreToolUse",
@@ -316,7 +382,10 @@ class HookInstallerService:
316
382
  "Stop",
317
383
  "SubagentStop",
318
384
  ]:
319
- settings["hooks"][event_type] = [hook_config]
385
+ existing = settings["hooks"].get(event_type, [])
386
+ settings["hooks"][event_type] = merge_hooks_for_event(
387
+ existing, new_hook_command
388
+ )
320
389
 
321
390
  # Write settings
322
391
  with self.settings_file.open("w") as f:
@@ -10,7 +10,7 @@ MCP service installations.
10
10
  """
11
11
 
12
12
  import json
13
- import subprocess
13
+ import subprocess # nosec B404 - Required for MCP service management
14
14
  import sys
15
15
  from enum import Enum
16
16
  from pathlib import Path
@@ -150,6 +150,86 @@ class MCPConfigManager:
150
150
 
151
151
  return is_enabled
152
152
 
153
+ def get_registry_service_config(
154
+ self, service_name: str, env_overrides: Optional[Dict[str, str]] = None
155
+ ) -> Optional[Dict]:
156
+ """
157
+ Get configuration for a service from the MCP Service Registry.
158
+
159
+ Args:
160
+ service_name: Name of the service
161
+ env_overrides: Optional environment variable overrides
162
+
163
+ Returns:
164
+ Service configuration dict or None if service not in registry
165
+ """
166
+ try:
167
+ from .mcp_service_registry import MCPServiceRegistry
168
+
169
+ service = MCPServiceRegistry.get(service_name)
170
+ if not service:
171
+ return None
172
+
173
+ return MCPServiceRegistry.generate_config(service, env_overrides)
174
+ except ImportError:
175
+ self.logger.debug("MCP Service Registry not available")
176
+ return None
177
+
178
+ def filter_services_by_mcp_flag(
179
+ self, mcp_flag: Optional[str], all_services: Dict[str, Dict]
180
+ ) -> Dict[str, Dict]:
181
+ """
182
+ Filter MCP services based on the --mcp command line flag.
183
+
184
+ Args:
185
+ mcp_flag: Comma-separated list of service names, or None for all
186
+ all_services: Dict of all available service configurations
187
+
188
+ Returns:
189
+ Filtered dict of service configurations
190
+ """
191
+ if not mcp_flag:
192
+ return all_services
193
+
194
+ # Parse comma-separated service names
195
+ requested_services = {s.strip() for s in mcp_flag.split(",") if s.strip()}
196
+
197
+ # Filter services
198
+ filtered = {}
199
+ for name, config in all_services.items():
200
+ if name in requested_services:
201
+ filtered[name] = config
202
+ else:
203
+ self.logger.debug(f"MCP service '{name}' excluded by --mcp flag")
204
+
205
+ # Warn about requested services that don't exist
206
+ available = set(all_services.keys())
207
+ missing = requested_services - available
208
+ if missing:
209
+ self.logger.warning(
210
+ f"Requested MCP services not available: {', '.join(missing)}"
211
+ )
212
+
213
+ return filtered
214
+
215
+ def list_available_services(self) -> list[str]:
216
+ """
217
+ List all available MCP services from registry and static configs.
218
+
219
+ Returns:
220
+ List of service names
221
+ """
222
+ services = set(self.STATIC_MCP_CONFIGS.keys())
223
+
224
+ try:
225
+ from .mcp_service_registry import MCPServiceRegistry
226
+
227
+ services.update(MCPServiceRegistry.list_names())
228
+ except ImportError:
229
+ pass
230
+
231
+ return sorted(services)
232
+
153
233
  def detect_service_path(self, service_name: str) -> Optional[str]:
154
234
  """
155
235
  Detect the best path for an MCP service.
@@ -185,7 +265,7 @@ class MCPConfigManager:
185
265
  # Choose the best candidate (prefer v1.1.0+ with MCP support)
186
266
  for path in candidates:
187
267
  try:
188
- result = subprocess.run(
268
+ result = subprocess.run( # nosec B603 B607 - Controlled service help check
189
269
  [path, "--help"],
190
270
  capture_output=True,
191
271
  text=True,
@@ -258,7 +338,7 @@ class MCPConfigManager:
258
338
  def _check_system_path(self, service_name: str) -> Optional[str]:
259
339
  """Check if service is available in system PATH."""
260
340
  try:
261
- result = subprocess.run(
341
+ result = subprocess.run( # nosec B603 B607 - Controlled which command
262
342
  ["which", service_name],
263
343
  capture_output=True,
264
344
  text=True,
@@ -356,7 +436,7 @@ class MCPConfigManager:
356
436
  cmd.append("--help")
357
437
 
358
438
  # Run test command with timeout
359
- result = subprocess.run(
439
+ result = subprocess.run( # nosec B603 - Controlled service test command
360
440
  cmd,
361
441
  capture_output=True,
362
442
  text=True,
@@ -560,7 +640,7 @@ class MCPConfigManager:
560
640
  # Try pipx run test
561
641
  if shutil.which("pipx"):
562
642
  try:
563
- result = subprocess.run(
643
+ result = subprocess.run( # nosec B603 B607 - Controlled pipx run command
564
644
  ["pipx", "run", service_name, "--version"],
565
645
  capture_output=True,
566
646
  text=True,
@@ -576,7 +656,7 @@ class MCPConfigManager:
576
656
  # Try uvx if pipx run not available
577
657
  if not use_pipx_run and shutil.which("uvx"):
578
658
  try:
579
- result = subprocess.run(
659
+ result = subprocess.run( # nosec B603 B607 - Controlled uvx command
580
660
  ["uvx", service_name, "--version"],
581
661
  capture_output=True,
582
662
  text=True,
@@ -876,7 +956,7 @@ class MCPConfigManager:
876
956
  command_path = service_config.get("command", "")
877
957
  results[service_name] = Path(command_path).exists()
878
958
  return results
879
- except Exception:
959
+ except Exception: # nosec B110 - Graceful fallback to empty dict
880
960
  pass
881
961
  return {}
882
962
 
@@ -946,7 +1026,7 @@ class MCPConfigManager:
946
1026
  if shutil.which("pipx"):
947
1027
  try:
948
1028
  self.logger.debug(f"Attempting to install {service_name} via pipx...")
949
- result = subprocess.run(
1029
+ result = subprocess.run( # nosec B603 B607 - Controlled pipx install
950
1030
  ["pipx", "install", service_name],
951
1031
  capture_output=True,
952
1032
  text=True,
@@ -980,7 +1060,7 @@ class MCPConfigManager:
980
1060
  if shutil.which("uvx"):
981
1061
  try:
982
1062
  self.logger.debug(f"Attempting to install {service_name} via uvx...")
983
- result = subprocess.run(
1063
+ result = subprocess.run( # nosec B603 B607 - Controlled uvx install
984
1064
  ["uvx", "install", service_name],
985
1065
  capture_output=True,
986
1066
  text=True,
@@ -997,7 +1077,7 @@ class MCPConfigManager:
997
1077
  # Method 3: Try pip install --user
998
1078
  try:
999
1079
  self.logger.debug(f"Attempting to install {service_name} via pip --user...")
1000
- result = subprocess.run(
1080
+ result = subprocess.run( # nosec B603 B607 - Controlled pip install
1001
1081
  [sys.executable, "-m", "pip", "install", "--user", service_name],
1002
1082
  capture_output=True,
1003
1083
  text=True,
@@ -1093,7 +1173,7 @@ class MCPConfigManager:
1093
1173
  self.logger.debug(
1094
1174
  f" Testing {service_name} from installed pipx venv: {pipx_venv_bin}"
1095
1175
  )
1096
- result = subprocess.run(
1176
+ result = subprocess.run( # nosec B603 - Controlled service help check
1097
1177
  [str(pipx_venv_bin), "--help"],
1098
1178
  capture_output=True,
1099
1179
  text=True,
@@ -1145,7 +1225,7 @@ class MCPConfigManager:
1145
1225
  self.logger.debug(
1146
1226
  f" Testing {service_name} via pipx run (not installed in venv)"
1147
1227
  )
1148
- result = subprocess.run(
1228
+ result = subprocess.run( # nosec B603 B607 - Controlled pipx run command
1149
1229
  ["pipx", "run", service_name, "--help"],
1150
1230
  capture_output=True,
1151
1231
  text=True,
@@ -1216,7 +1296,7 @@ class MCPConfigManager:
1216
1296
  self.logger.debug(f"Uninstalling {service_name}...")
1217
1297
 
1218
1298
  # First uninstall the corrupted version
1219
- uninstall_result = subprocess.run(
1299
+ uninstall_result = subprocess.run( # nosec B603 B607 - Controlled pipx uninstall
1220
1300
  ["pipx", "uninstall", service_name],
1221
1301
  capture_output=True,
1222
1302
  text=True,
@@ -1229,7 +1309,7 @@ class MCPConfigManager:
1229
1309
 
1230
1310
  # Now reinstall
1231
1311
  self.logger.debug(f"Installing fresh {service_name}...")
1232
- install_result = subprocess.run(
1312
+ install_result = subprocess.run( # nosec B603 B607 - Controlled pipx install
1233
1313
  ["pipx", "install", service_name],
1234
1314
  capture_output=True,
1235
1315
  text=True,
@@ -1294,7 +1374,7 @@ class MCPConfigManager:
1294
1374
  for dep in missing_deps:
1295
1375
  try:
1296
1376
  self.logger.debug(f" Injecting {dep} into {service_name}...")
1297
- result = subprocess.run(
1377
+ result = subprocess.run( # nosec B603 B607 - Controlled pipx inject
1298
1378
  ["pipx", "inject", service_name, dep],
1299
1379
  capture_output=True,
1300
1380
  text=True,
@@ -1348,7 +1428,7 @@ class MCPConfigManager:
1348
1428
  return False
1349
1429
 
1350
1430
  self.logger.info(f" → Uninstalling {service_name}...")
1351
- uninstall_result = subprocess.run(
1431
+ uninstall_result = subprocess.run( # nosec B603 B607 - Controlled pipx uninstall
1352
1432
  ["pipx", "uninstall", service_name],
1353
1433
  capture_output=True,
1354
1434
  text=True,
@@ -1363,7 +1443,7 @@ class MCPConfigManager:
1363
1443
  )
1364
1444
 
1365
1445
  self.logger.info(f" → Installing fresh {service_name}...")
1366
- install_result = subprocess.run(
1446
+ install_result = subprocess.run( # nosec B603 B607 - Controlled pipx install
1367
1447
  ["pipx", "install", service_name],
1368
1448
  capture_output=True,
1369
1449
  text=True,
@@ -1439,7 +1519,7 @@ class MCPConfigManager:
1439
1519
  # Try pipx run as fallback for pipx installations
1440
1520
  if method == "pipx":
1441
1521
  try:
1442
- result = subprocess.run(
1522
+ result = subprocess.run( # nosec B603 B607 - Controlled pipx command
1443
1523
  ["pipx", "run", service_name, "--version"],
1444
1524
  capture_output=True,
1445
1525
  text=True,
@@ -1462,7 +1542,7 @@ class MCPConfigManager:
1462
1542
  ]
1463
1543
 
1464
1544
  for cmd in test_commands:
1465
- result = subprocess.run(
1545
+ result = subprocess.run( # nosec B603 - Controlled service verification
1466
1546
  cmd,
1467
1547
  capture_output=True,
1468
1548
  text=True,