claude-mpm 5.6.1__py3-none-any.whl → 5.6.76__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 (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
  3. claude_mpm/auth/__init__.py +35 -0
  4. claude_mpm/auth/callback_server.py +328 -0
  5. claude_mpm/auth/models.py +104 -0
  6. claude_mpm/auth/oauth_manager.py +266 -0
  7. claude_mpm/auth/providers/__init__.py +12 -0
  8. claude_mpm/auth/providers/base.py +165 -0
  9. claude_mpm/auth/providers/google.py +261 -0
  10. claude_mpm/auth/token_storage.py +252 -0
  11. claude_mpm/cli/commands/commander.py +174 -4
  12. claude_mpm/cli/commands/mcp.py +29 -17
  13. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  14. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  15. claude_mpm/cli/commands/oauth.py +481 -0
  16. claude_mpm/cli/commands/skill_source.py +51 -2
  17. claude_mpm/cli/commands/skills.py +5 -3
  18. claude_mpm/cli/executor.py +9 -0
  19. claude_mpm/cli/helpers.py +1 -1
  20. claude_mpm/cli/parsers/base_parser.py +13 -0
  21. claude_mpm/cli/parsers/commander_parser.py +43 -10
  22. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  23. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  24. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  25. claude_mpm/cli/parsers/skills_parser.py +5 -0
  26. claude_mpm/cli/startup.py +300 -33
  27. claude_mpm/cli/startup_display.py +4 -2
  28. claude_mpm/cli/startup_migrations.py +236 -0
  29. claude_mpm/commander/__init__.py +6 -0
  30. claude_mpm/commander/adapters/__init__.py +32 -3
  31. claude_mpm/commander/adapters/auggie.py +260 -0
  32. claude_mpm/commander/adapters/base.py +98 -1
  33. claude_mpm/commander/adapters/claude_code.py +32 -1
  34. claude_mpm/commander/adapters/codex.py +237 -0
  35. claude_mpm/commander/adapters/example_usage.py +310 -0
  36. claude_mpm/commander/adapters/mpm.py +389 -0
  37. claude_mpm/commander/adapters/registry.py +204 -0
  38. claude_mpm/commander/api/app.py +32 -16
  39. claude_mpm/commander/api/errors.py +21 -0
  40. claude_mpm/commander/api/routes/messages.py +11 -11
  41. claude_mpm/commander/api/routes/projects.py +20 -20
  42. claude_mpm/commander/api/routes/sessions.py +37 -26
  43. claude_mpm/commander/api/routes/work.py +86 -50
  44. claude_mpm/commander/api/schemas.py +4 -0
  45. claude_mpm/commander/chat/cli.py +47 -5
  46. claude_mpm/commander/chat/commands.py +44 -16
  47. claude_mpm/commander/chat/repl.py +1729 -82
  48. claude_mpm/commander/config.py +5 -3
  49. claude_mpm/commander/core/__init__.py +10 -0
  50. claude_mpm/commander/core/block_manager.py +325 -0
  51. claude_mpm/commander/core/response_manager.py +323 -0
  52. claude_mpm/commander/daemon.py +215 -10
  53. claude_mpm/commander/env_loader.py +59 -0
  54. claude_mpm/commander/events/manager.py +61 -1
  55. claude_mpm/commander/frameworks/base.py +91 -1
  56. claude_mpm/commander/frameworks/mpm.py +9 -14
  57. claude_mpm/commander/git/__init__.py +5 -0
  58. claude_mpm/commander/git/worktree_manager.py +212 -0
  59. claude_mpm/commander/instance_manager.py +546 -15
  60. claude_mpm/commander/memory/__init__.py +45 -0
  61. claude_mpm/commander/memory/compression.py +347 -0
  62. claude_mpm/commander/memory/embeddings.py +230 -0
  63. claude_mpm/commander/memory/entities.py +310 -0
  64. claude_mpm/commander/memory/example_usage.py +290 -0
  65. claude_mpm/commander/memory/integration.py +325 -0
  66. claude_mpm/commander/memory/search.py +381 -0
  67. claude_mpm/commander/memory/store.py +657 -0
  68. claude_mpm/commander/models/events.py +6 -0
  69. claude_mpm/commander/persistence/state_store.py +95 -1
  70. claude_mpm/commander/registry.py +10 -4
  71. claude_mpm/commander/runtime/monitor.py +32 -2
  72. claude_mpm/commander/tmux_orchestrator.py +3 -2
  73. claude_mpm/commander/work/executor.py +38 -20
  74. claude_mpm/commander/workflow/event_handler.py +25 -3
  75. claude_mpm/config/skill_sources.py +16 -0
  76. claude_mpm/constants.py +5 -0
  77. claude_mpm/core/claude_runner.py +152 -0
  78. claude_mpm/core/config.py +30 -22
  79. claude_mpm/core/config_constants.py +74 -9
  80. claude_mpm/core/constants.py +56 -12
  81. claude_mpm/core/hook_manager.py +2 -1
  82. claude_mpm/core/interactive_session.py +5 -4
  83. claude_mpm/core/logger.py +16 -2
  84. claude_mpm/core/logging_utils.py +40 -16
  85. claude_mpm/core/network_config.py +148 -0
  86. claude_mpm/core/oneshot_session.py +7 -6
  87. claude_mpm/core/output_style_manager.py +37 -7
  88. claude_mpm/core/socketio_pool.py +47 -15
  89. claude_mpm/core/unified_paths.py +68 -80
  90. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
  91. claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
  92. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  93. claude_mpm/hooks/claude_hooks/installer.py +222 -54
  94. claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
  95. claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
  96. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  97. claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
  98. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
  99. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  100. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  101. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  102. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
  103. claude_mpm/hooks/session_resume_hook.py +22 -18
  104. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  105. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  106. claude_mpm/init.py +21 -14
  107. claude_mpm/mcp/__init__.py +9 -0
  108. claude_mpm/mcp/google_workspace_server.py +610 -0
  109. claude_mpm/scripts/claude-hook-handler.sh +10 -9
  110. claude_mpm/services/agents/agent_selection_service.py +2 -2
  111. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  112. claude_mpm/services/command_deployment_service.py +44 -26
  113. claude_mpm/services/hook_installer_service.py +77 -8
  114. claude_mpm/services/mcp_config_manager.py +99 -19
  115. claude_mpm/services/mcp_service_registry.py +294 -0
  116. claude_mpm/services/monitor/server.py +6 -1
  117. claude_mpm/services/pm_skills_deployer.py +5 -3
  118. claude_mpm/services/skills/git_skill_source_manager.py +79 -8
  119. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  120. claude_mpm/services/skills/skill_discovery_service.py +17 -1
  121. claude_mpm/services/skills_deployer.py +31 -5
  122. claude_mpm/skills/__init__.py +2 -1
  123. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  124. claude_mpm/skills/registry.py +295 -90
  125. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
  126. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
  127. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
  128. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
  129. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
  130. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  131. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,252 @@
1
+ """Secure OAuth token storage with Fernet encryption and keyring.
2
+
3
+ This module provides encrypted persistence for OAuth tokens, using
4
+ the system keyring for encryption key storage and Fernet symmetric
5
+ encryption for the actual token data.
6
+
7
+ Security Features:
8
+ - Encryption keys stored in system keyring (not on disk)
9
+ - Fernet symmetric encryption for token data
10
+ - File permissions restricted to owner only (600)
11
+ - Credentials stored in user-specific directory
12
+ """
13
+
14
+ import json
15
+ import stat
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ import keyring
20
+ from cryptography.fernet import Fernet, InvalidToken
21
+
22
+ from claude_mpm.auth.models import OAuthToken, StoredToken, TokenMetadata, TokenStatus
23
+
24
+ # Keyring service identifier for encryption keys
25
+ KEYRING_SERVICE = "claude-mpm-oauth"
26
+
27
+ # Default credentials directory
28
+ CREDENTIALS_DIR = Path.home() / ".claude-mpm" / "credentials"
29
+
30
+
31
+ class TokenStorage:
32
+ """Secure storage for OAuth tokens using Fernet encryption.
33
+
34
+ Tokens are encrypted using Fernet symmetric encryption with keys
35
+ stored securely in the system keyring. Token files are stored
36
+ with restricted permissions (600) in the credentials directory.
37
+
38
+ Attributes:
39
+ credentials_dir: Directory where encrypted tokens are stored.
40
+
41
+ Example:
42
+ ```python
43
+ storage = TokenStorage()
44
+
45
+ # Store a token
46
+ token = OAuthToken(
47
+ access_token="abc123",
48
+ expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
49
+ scopes=["read", "write"]
50
+ )
51
+ metadata = TokenMetadata(service_name="github-mcp", provider="github")
52
+ storage.store("github-mcp", token, metadata)
53
+
54
+ # Retrieve the token
55
+ stored = storage.retrieve("github-mcp")
56
+ if stored:
57
+ print(f"Token expires at: {stored.token.expires_at}")
58
+ ```
59
+ """
60
+
61
+ def __init__(self, credentials_dir: Optional[Path] = None) -> None:
62
+ """Initialize token storage.
63
+
64
+ Args:
65
+ credentials_dir: Custom directory for storing encrypted tokens.
66
+ Defaults to ~/.claude-mpm/credentials/
67
+ """
68
+ self.credentials_dir = credentials_dir or CREDENTIALS_DIR
69
+ self._ensure_credentials_dir()
70
+
71
+ def _ensure_credentials_dir(self) -> None:
72
+ """Create credentials directory with secure permissions if needed."""
73
+ if not self.credentials_dir.exists():
74
+ self.credentials_dir.mkdir(parents=True, mode=0o700)
75
+ else:
76
+ # Ensure directory has correct permissions
77
+ self.credentials_dir.chmod(0o700)
78
+
79
+ def _get_encryption_key(self, service_name: str) -> bytes:
80
+ """Get or create the Fernet encryption key for a service.
81
+
82
+ Keys are stored in the system keyring, not on disk.
83
+
84
+ Args:
85
+ service_name: Name of the service to get/create key for.
86
+
87
+ Returns:
88
+ Fernet encryption key as bytes.
89
+ """
90
+ key_name = f"{service_name}-key"
91
+ existing_key = keyring.get_password(KEYRING_SERVICE, key_name)
92
+
93
+ if existing_key:
94
+ return existing_key.encode()
95
+
96
+ # Generate new key and store in keyring
97
+ new_key = Fernet.generate_key()
98
+ keyring.set_password(KEYRING_SERVICE, key_name, new_key.decode())
99
+ return new_key
100
+
101
+ def _delete_encryption_key(self, service_name: str) -> None:
102
+ """Delete the encryption key for a service from keyring.
103
+
104
+ Args:
105
+ service_name: Name of the service to delete key for.
106
+ """
107
+ key_name = f"{service_name}-key"
108
+ try:
109
+ keyring.delete_password(KEYRING_SERVICE, key_name)
110
+ except keyring.errors.PasswordDeleteError:
111
+ pass # Key doesn't exist, nothing to delete
112
+
113
+ def _get_token_path(self, service_name: str) -> Path:
114
+ """Get the file path for a service's encrypted token.
115
+
116
+ Args:
117
+ service_name: Name of the service.
118
+
119
+ Returns:
120
+ Path to the encrypted token file.
121
+ """
122
+ # Sanitize service name for filesystem
123
+ safe_name = "".join(
124
+ c if c.isalnum() or c in "-_" else "_" for c in service_name
125
+ )
126
+ return self.credentials_dir / f"{safe_name}.enc"
127
+
128
+ def store(
129
+ self,
130
+ service_name: str,
131
+ token: OAuthToken,
132
+ metadata: TokenMetadata,
133
+ ) -> None:
134
+ """Store an OAuth token securely.
135
+
136
+ The token is encrypted using Fernet symmetric encryption with
137
+ a key stored in the system keyring. The encrypted data is
138
+ written to a file with 600 permissions.
139
+
140
+ Args:
141
+ service_name: Unique identifier for the service.
142
+ token: OAuth token data to store.
143
+ metadata: Token metadata including provider info.
144
+ """
145
+ stored_token = StoredToken(
146
+ version=1,
147
+ metadata=metadata,
148
+ token=token,
149
+ )
150
+
151
+ # Serialize to JSON
152
+ token_json = stored_token.model_dump_json()
153
+
154
+ # Encrypt the token data
155
+ key = self._get_encryption_key(service_name)
156
+ fernet = Fernet(key)
157
+ encrypted_data = fernet.encrypt(token_json.encode())
158
+
159
+ # Write encrypted data with secure permissions
160
+ token_path = self._get_token_path(service_name)
161
+ token_path.write_bytes(encrypted_data)
162
+
163
+ # Set file permissions to owner read/write only (600)
164
+ token_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
165
+
166
+ def retrieve(self, service_name: str) -> Optional[StoredToken]:
167
+ """Retrieve a stored OAuth token.
168
+
169
+ Args:
170
+ service_name: Unique identifier for the service.
171
+
172
+ Returns:
173
+ StoredToken if found and valid, None otherwise.
174
+ """
175
+ token_path = self._get_token_path(service_name)
176
+
177
+ if not token_path.exists():
178
+ return None
179
+
180
+ try:
181
+ # Read encrypted data
182
+ encrypted_data = token_path.read_bytes()
183
+
184
+ # Decrypt
185
+ key = self._get_encryption_key(service_name)
186
+ fernet = Fernet(key)
187
+ decrypted_data = fernet.decrypt(encrypted_data)
188
+
189
+ # Deserialize
190
+ return StoredToken.model_validate_json(decrypted_data)
191
+
192
+ except (InvalidToken, json.JSONDecodeError, ValueError):
193
+ # Token is corrupted or invalid
194
+ return None
195
+
196
+ def delete(self, service_name: str) -> bool:
197
+ """Delete a stored token and its encryption key.
198
+
199
+ Args:
200
+ service_name: Unique identifier for the service.
201
+
202
+ Returns:
203
+ True if token was deleted, False if it didn't exist.
204
+ """
205
+ token_path = self._get_token_path(service_name)
206
+
207
+ if not token_path.exists():
208
+ return False
209
+
210
+ # Delete the token file
211
+ token_path.unlink()
212
+
213
+ # Delete the encryption key from keyring
214
+ self._delete_encryption_key(service_name)
215
+
216
+ return True
217
+
218
+ def list_services(self) -> list[str]:
219
+ """List all services with stored tokens.
220
+
221
+ Returns:
222
+ List of service names that have stored tokens.
223
+ """
224
+ services = []
225
+ for path in self.credentials_dir.glob("*.enc"):
226
+ # Extract service name from filename
227
+ service_name = path.stem
228
+ services.append(service_name)
229
+ return sorted(services)
230
+
231
+ def get_status(self, service_name: str) -> TokenStatus:
232
+ """Get the status of a stored token.
233
+
234
+ Args:
235
+ service_name: Unique identifier for the service.
236
+
237
+ Returns:
238
+ TokenStatus indicating the token's current state.
239
+ """
240
+ stored = self.retrieve(service_name)
241
+
242
+ if stored is None:
243
+ token_path = self._get_token_path(service_name)
244
+ if token_path.exists():
245
+ # File exists but couldn't be decrypted
246
+ return TokenStatus.INVALID
247
+ return TokenStatus.MISSING
248
+
249
+ if stored.token.is_expired():
250
+ return TokenStatus.EXPIRED
251
+
252
+ return TokenStatus.VALID
@@ -2,25 +2,129 @@
2
2
 
3
3
  import asyncio
4
4
  import logging
5
+ import shutil
6
+ import threading
7
+ import time
8
+ from pathlib import Path
5
9
 
6
10
  logger = logging.getLogger(__name__)
7
11
 
12
+ # ANSI colors
13
+ CYAN = "\033[36m"
14
+ DIM = "\033[2m"
15
+ BOLD = "\033[1m"
16
+ YELLOW = "\033[33m"
17
+ GREEN = "\033[32m"
18
+ RED = "\033[31m"
19
+ RESET = "\033[0m"
20
+
21
+
22
+ def _get_terminal_width() -> int:
23
+ """Get terminal width with reasonable bounds."""
24
+ try:
25
+ width = shutil.get_terminal_size().columns
26
+ return max(80, min(width, 120))
27
+ except Exception:
28
+ return 100
29
+
30
+
31
+ def _get_version() -> str:
32
+ """Get Commander version."""
33
+ version_file = Path(__file__).parent.parent.parent / "VERSION"
34
+ if version_file.exists():
35
+ return version_file.read_text().strip()
36
+ return "unknown"
37
+
38
+
39
+ def display_commander_banner():
40
+ """Display Commander-specific startup banner."""
41
+ width = _get_terminal_width()
42
+ version = _get_version()
43
+
44
+ # Commander ASCII art banner
45
+ banner = f"""
46
+ {CYAN}╭{"─" * (width - 2)}╮{RESET}
47
+ {CYAN}│{RESET}{BOLD} ⚡ MPM Commander {RESET}{DIM}v{version}{RESET}{" " * (width - 24 - len(version))}│
48
+ {CYAN}│{RESET}{DIM} Multi-Project AI Orchestration{RESET}{" " * (width - 36)}│
49
+ {CYAN}├{"─" * (width - 2)}┤{RESET}
50
+ {CYAN}│{RESET} {YELLOW}ALPHA{RESET} - APIs may change {" " * (width - 55)}│
51
+ {CYAN}╰{"─" * (width - 2)}╯{RESET}
52
+ """
53
+ print(banner)
54
+
55
+
56
+ def _count_cached_agents() -> int:
57
+ """Count cached agents from ~/.claude-mpm/cache/agents/."""
58
+ try:
59
+ cache_agents_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
60
+ if not cache_agents_dir.exists():
61
+ return 0
62
+ # Recursively find all .md files excluding base/README files
63
+ agent_files = [
64
+ f
65
+ for f in cache_agents_dir.rglob("*.md")
66
+ if f.is_file()
67
+ and not f.name.startswith(".")
68
+ and f.name not in ("README.md", "BASE-AGENT.md", "INSTRUCTIONS.md")
69
+ ]
70
+ return len(agent_files)
71
+ except Exception:
72
+ return 0
73
+
74
+
75
+ def _count_cached_skills() -> int:
76
+ """Count cached skills from ~/.claude-mpm/cache/skills/."""
77
+ try:
78
+ cache_skills_dir = Path.home() / ".claude-mpm" / "cache" / "skills"
79
+ if not cache_skills_dir.exists():
80
+ return 0
81
+ # Recursively find all directories containing SKILL.md
82
+ skill_files = list(cache_skills_dir.rglob("SKILL.md"))
83
+ return len(skill_files)
84
+ except Exception:
85
+ return 0
86
+
87
+
88
+ def load_agents_and_skills():
89
+ """Load agents and skills for Commander sessions."""
90
+ try:
91
+ print(f"{DIM}Loading agents...{RESET}", end=" ", flush=True)
92
+ agent_count = _count_cached_agents()
93
+ print(f"{GREEN}✓{RESET} {agent_count} agents")
94
+
95
+ print(f"{DIM}Loading skills...{RESET}", end=" ", flush=True)
96
+ skill_count = _count_cached_skills()
97
+ print(f"{GREEN}✓{RESET} {skill_count} skills")
98
+
99
+ return agent_count, skill_count
100
+ except Exception as e:
101
+ logger.warning(f"Could not load agents/skills: {e}")
102
+ print(f"{YELLOW}⚠{RESET} Could not load agents/skills")
103
+ return 0, 0
104
+
8
105
 
9
106
  def handle_commander_command(args) -> int:
10
- """Handle the commander command.
107
+ """Handle the commander command with auto-starting daemon.
11
108
 
12
109
  Args:
13
110
  args: Parsed command line arguments with:
14
- - port: Port for internal services (default: 8765)
111
+ - port: Port for daemon (default: 8765)
112
+ - host: Host for daemon (default: 127.0.0.1)
15
113
  - state_dir: Optional state directory path
16
114
  - debug: Enable debug logging
115
+ - no_chat: Start daemon only without interactive chat
116
+ - daemon_only: Alias for no_chat
17
117
 
18
118
  Returns:
19
119
  Exit code (0 for success, 1 for error)
20
120
  """
21
121
  try:
22
122
  # Import here to avoid circular dependencies
123
+ import requests
124
+
23
125
  from claude_mpm.commander.chat.cli import run_commander
126
+ from claude_mpm.commander.config import DaemonConfig
127
+ from claude_mpm.commander.daemon import main as daemon_main
24
128
 
25
129
  # Setup debug logging if requested
26
130
  if getattr(args, "debug", False):
@@ -29,11 +133,76 @@ def handle_commander_command(args) -> int:
29
133
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
30
134
  )
31
135
 
136
+ # Display Commander banner
137
+ display_commander_banner()
138
+
139
+ # Load agents and skills
140
+ load_agents_and_skills()
141
+
142
+ print() # Blank line after loading
143
+
32
144
  # Get arguments
33
- port = getattr(args, "port", 8765)
145
+ port = getattr(args, "port", 8766) # NetworkPorts.COMMANDER_DEFAULT
146
+ host = getattr(args, "host", "127.0.0.1")
34
147
  state_dir = getattr(args, "state_dir", None)
148
+ no_chat = getattr(args, "no_chat", False) or getattr(args, "daemon_only", False)
149
+
150
+ # Check if daemon already running
151
+ daemon_running = False
152
+ try:
153
+ resp = requests.get(f"http://{host}:{port}/api/health", timeout=1)
154
+ if resp.status_code == 200:
155
+ print(f"{GREEN}✓{RESET} Daemon already running on {host}:{port}")
156
+ daemon_running = True
157
+ except (requests.RequestException, requests.ConnectionError):
158
+ pass
159
+
160
+ # Start daemon if not running
161
+ if not daemon_running:
162
+ print(
163
+ f"{DIM}Starting daemon on {host}:{port}...{RESET}", end=" ", flush=True
164
+ )
165
+
166
+ # Create daemon config
167
+ config_kwargs = {"host": host, "port": port}
168
+ if state_dir:
169
+ config_kwargs["state_dir"] = state_dir
170
+ config = DaemonConfig(**config_kwargs)
171
+
172
+ # Start daemon in background thread
173
+ daemon_thread = threading.Thread(
174
+ target=lambda: asyncio.run(daemon_main(config)), daemon=True
175
+ )
176
+ daemon_thread.start()
177
+
178
+ # Wait for daemon to be ready (max 3 seconds)
179
+ for _ in range(30):
180
+ time.sleep(0.1)
181
+ try:
182
+ resp = requests.get(f"http://{host}:{port}/api/health", timeout=1)
183
+ if resp.status_code == 200:
184
+ print(f"{GREEN}✓{RESET}")
185
+ daemon_running = True
186
+ break
187
+ except (requests.RequestException, requests.ConnectionError):
188
+ pass
189
+ else:
190
+ print(f"{RED}✗{RESET} Failed (timeout)")
191
+ return 1
192
+
193
+ # If daemon-only mode, keep running until interrupted
194
+ if no_chat:
195
+ print(f"\n{CYAN}Daemon running.{RESET} API at http://{host}:{port}")
196
+ print(f"{DIM}Press Ctrl+C to stop{RESET}\n")
197
+ try:
198
+ while True:
199
+ time.sleep(1)
200
+ except KeyboardInterrupt:
201
+ print(f"\n{DIM}Shutting down...{RESET}")
202
+ return 0
35
203
 
36
- # Run commander
204
+ # Launch interactive chat
205
+ print(f"\n{CYAN}Entering Commander chat...{RESET}\n")
37
206
  asyncio.run(run_commander(port=port, state_dir=state_dir))
38
207
 
39
208
  return 0
@@ -43,4 +212,5 @@ def handle_commander_command(args) -> int:
43
212
  return 0
44
213
  except Exception as e:
45
214
  logger.error(f"Commander error: {e}", exc_info=True)
215
+ print(f"{RED}Error:{RESET} {e}")
46
216
  return 1
@@ -33,7 +33,27 @@ def manage_mcp(args):
33
33
  """
34
34
  logger = get_logger("cli.mcp")
35
35
 
36
- # First check if MCP package is installed for any command
36
+ # Commands that don't require full MCP Gateway or mcp package
37
+ # These only need mcp_service_registry which doesn't require the mcp package
38
+ service_mgmt_commands = {
39
+ MCPCommands.ENABLE.value,
40
+ MCPCommands.DISABLE.value,
41
+ MCPCommands.LIST.value,
42
+ }
43
+
44
+ # Route service management commands directly without any MCP dependencies
45
+ if args.mcp_command in service_mgmt_commands:
46
+ try:
47
+ from .mcp_command_router import MCPCommandRouter
48
+
49
+ router = MCPCommandRouter(logger)
50
+ return router.route_command(args)
51
+ except Exception as e:
52
+ logger.error(f"Error running service command: {e}", exc_info=True)
53
+ print(f"Error: {e}")
54
+ return 1
55
+
56
+ # Now check for mcp package for other commands
37
57
  import importlib.util
38
58
 
39
59
  mcp_spec = importlib.util.find_spec("mcp")
@@ -58,22 +78,14 @@ def manage_mcp(args):
58
78
  except ImportError as e:
59
79
  # Provide minimal fallbacks for basic commands
60
80
  logger.warning(f"Some MCP Gateway services not available: {e}")
61
-
62
- # Allow install command to proceed
63
- if args.mcp_command == MCPCommands.INSTALL.value:
64
- MCPConfiguration = None
65
- MCPServiceRegistry = None
66
- ToolRegistry = None
67
- MCPGateway = None
68
- else:
69
- print(
70
- "\nError: MCP Gateway services not fully available",
71
- file=sys.stderr,
72
- )
73
- print(f"Details: {e}", file=sys.stderr)
74
- print("\nTry running:", file=sys.stderr)
75
- print(" claude-mpm mcp install", file=sys.stderr)
76
- return 1
81
+ print(
82
+ "\nError: MCP Gateway services not fully available",
83
+ file=sys.stderr,
84
+ )
85
+ print(f"Details: {e}", file=sys.stderr)
86
+ print("\nTry running:", file=sys.stderr)
87
+ print(" claude-mpm mcp install", file=sys.stderr)
88
+ return 1
77
89
 
78
90
  if not args.mcp_command:
79
91
  # No subcommand - show status by default
@@ -48,6 +48,15 @@ class MCPCommandRouter:
48
48
  if args.mcp_command == MCPCommands.EXTERNAL.value:
49
49
  return self._manage_external(args)
50
50
 
51
+ if args.mcp_command == MCPCommands.ENABLE.value:
52
+ return self._enable_service(args)
53
+
54
+ if args.mcp_command == MCPCommands.DISABLE.value:
55
+ return self._disable_service(args)
56
+
57
+ if args.mcp_command == MCPCommands.LIST.value:
58
+ return self._list_services(args)
59
+
51
60
  if args.mcp_command == "cleanup":
52
61
  return self._cleanup_locks(args)
53
62
 
@@ -134,6 +143,27 @@ class MCPCommandRouter:
134
143
  handler = MCPExternalCommands(self.logger)
135
144
  return handler.manage_external(args)
136
145
 
146
+ def _enable_service(self, args) -> int:
147
+ """Enable MCP service command handler."""
148
+ from .mcp_service_commands import MCPServiceCommands
149
+
150
+ handler = MCPServiceCommands(self.logger)
151
+ return handler.enable_service(args)
152
+
153
+ def _disable_service(self, args) -> int:
154
+ """Disable MCP service command handler."""
155
+ from .mcp_service_commands import MCPServiceCommands
156
+
157
+ handler = MCPServiceCommands(self.logger)
158
+ return handler.disable_service(args)
159
+
160
+ def _list_services(self, args) -> int:
161
+ """List MCP services command handler."""
162
+ from .mcp_service_commands import MCPServiceCommands
163
+
164
+ handler = MCPServiceCommands(self.logger)
165
+ return handler.list_services(args)
166
+
137
167
  def _show_help(self):
138
168
  """Show available MCP commands."""
139
169
  print("\nAvailable MCP commands:")
@@ -148,6 +178,10 @@ class MCPCommandRouter:
148
178
  print(" config - View and manage configuration")
149
179
  print(" external - Manage external MCP services")
150
180
  print(" cleanup - Clean up legacy files")
181
+ print("\nService management:")
182
+ print(" enable - Enable an MCP service in configuration")
183
+ print(" disable - Disable an MCP service from configuration")
184
+ print(" list - List available and enabled MCP services")
151
185
  print("\nFor help with a specific command:")
152
186
  print(" claude-mpm mcp <command> --help")
153
187
  print("\nExamples:")
@@ -159,3 +193,8 @@ class MCPCommandRouter:
159
193
  print(" claude-mpm mcp tools")
160
194
  print(" claude-mpm mcp register my-tool")
161
195
  print(" claude-mpm mcp test my-tool")
196
+ print("\nService management examples:")
197
+ print(" claude-mpm mcp list --available # List all available services")
198
+ print(" claude-mpm mcp enable kuzu-memory # Enable a service")
199
+ print(" claude-mpm mcp enable mcp-github --interactive # Enable with prompts")
200
+ print(" claude-mpm mcp disable mcp-github # Disable a service")