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
@@ -0,0 +1,481 @@
1
+ """
2
+ OAuth management commands for claude-mpm CLI.
3
+
4
+ WHY: Users need a way to manage OAuth authentication for MCP services
5
+ that require OAuth2 flows (e.g., Google Workspace) directly from the terminal.
6
+
7
+ DESIGN DECISIONS:
8
+ - Use BaseCommand for consistent CLI patterns
9
+ - Reuse OAuth logic from commander/chat/repl.py
10
+ - Support multiple credential sources: .env.local, .env, environment variables
11
+ - Provide clear feedback during OAuth flow
12
+ """
13
+
14
+ import asyncio
15
+ import json
16
+ import os
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from rich.console import Console
21
+ from rich.panel import Panel
22
+ from rich.table import Table
23
+
24
+ from ..shared import BaseCommand, CommandResult
25
+
26
+ console = Console()
27
+
28
+
29
+ def _load_oauth_credentials_from_env_files() -> tuple[str | None, str | None]:
30
+ """Load OAuth credentials from .env files.
31
+
32
+ Checks .env.local first (user overrides), then .env.
33
+ Returns tuple of (client_id, client_secret), either may be None.
34
+ """
35
+ client_id = None
36
+ client_secret = None
37
+
38
+ # Priority order: .env.local first (user overrides), then .env
39
+ env_files = [".env.local", ".env"]
40
+
41
+ for env_file in env_files:
42
+ env_path = Path.cwd() / env_file
43
+ if env_path.exists():
44
+ try:
45
+ with open(env_path) as f:
46
+ for line in f:
47
+ line = line.strip()
48
+ # Skip empty lines and comments
49
+ if not line or line.startswith("#"):
50
+ continue
51
+ if "=" in line:
52
+ key, _, value = line.partition("=")
53
+ key = key.strip()
54
+ value = value.strip().strip('"').strip("'")
55
+
56
+ if key == "GOOGLE_OAUTH_CLIENT_ID" and not client_id:
57
+ client_id = value
58
+ elif (
59
+ key == "GOOGLE_OAUTH_CLIENT_SECRET"
60
+ and not client_secret
61
+ ):
62
+ client_secret = value
63
+
64
+ # If we found both, no need to check more files
65
+ if client_id and client_secret:
66
+ break
67
+ except Exception: # nosec B110 - intentionally ignore .env file read errors
68
+ pass
69
+
70
+ return client_id, client_secret
71
+
72
+
73
+ class OAuthCommand(BaseCommand):
74
+ """OAuth management command for MCP services."""
75
+
76
+ def __init__(self):
77
+ super().__init__("oauth")
78
+
79
+ def validate_args(self, args) -> str | None:
80
+ """Validate command arguments."""
81
+ # If no oauth_command specified, default to 'list'
82
+ if not hasattr(args, "oauth_command") or not args.oauth_command:
83
+ args.oauth_command = None # Will show help
84
+ return None
85
+
86
+ valid_commands = ["list", "setup", "status", "revoke", "refresh"]
87
+ if args.oauth_command not in valid_commands:
88
+ return f"Unknown oauth command: {args.oauth_command}. Valid commands: {', '.join(valid_commands)}"
89
+
90
+ # Validate service_name for commands that require it
91
+ if args.oauth_command in ["setup", "status", "revoke", "refresh"]:
92
+ if not hasattr(args, "service_name") or not args.service_name:
93
+ return f"oauth {args.oauth_command} requires a service name"
94
+
95
+ return None
96
+
97
+ def run(self, args) -> CommandResult:
98
+ """Execute the OAuth command."""
99
+ # If no subcommand, show help
100
+ if not hasattr(args, "oauth_command") or not args.oauth_command:
101
+ self._show_help()
102
+ return CommandResult.success_result("Help displayed")
103
+
104
+ if args.oauth_command == "list":
105
+ return self._list_services(args)
106
+ if args.oauth_command == "setup":
107
+ return self._setup_oauth(args)
108
+ if args.oauth_command == "status":
109
+ return self._show_status(args)
110
+ if args.oauth_command == "revoke":
111
+ return self._revoke_tokens(args)
112
+ if args.oauth_command == "refresh":
113
+ return self._refresh_tokens(args)
114
+
115
+ return CommandResult.error_result(
116
+ f"Unknown oauth command: {args.oauth_command}"
117
+ )
118
+
119
+ def _show_help(self) -> None:
120
+ """Display OAuth command help."""
121
+ help_text = """
122
+ [bold]OAuth Commands:[/bold]
123
+ oauth list List OAuth-capable MCP services
124
+ oauth setup <service> Set up OAuth authentication for a service
125
+ oauth status <service> Show OAuth token status for a service
126
+ oauth revoke <service> Revoke OAuth tokens for a service
127
+ oauth refresh <service> Refresh OAuth tokens for a service
128
+
129
+ [bold]Examples:[/bold]
130
+ claude-mpm oauth list
131
+ claude-mpm oauth setup workspace-mcp
132
+ claude-mpm oauth status workspace-mcp
133
+ """
134
+ console.print(help_text)
135
+
136
+ def _list_services(self, args) -> CommandResult:
137
+ """List OAuth-capable MCP services."""
138
+ try:
139
+ from claude_mpm.services.mcp_service_registry import MCPServiceRegistry
140
+
141
+ services = MCPServiceRegistry.list_all()
142
+ oauth_services = [s for s in services if s.oauth_provider]
143
+
144
+ if not oauth_services:
145
+ console.print("[yellow]No OAuth-capable services found.[/yellow]")
146
+ return CommandResult.success_result("No OAuth services found")
147
+
148
+ # Check output format
149
+ output_format = getattr(args, "format", "table")
150
+
151
+ if output_format == "json":
152
+ data = [
153
+ {
154
+ "name": s.name,
155
+ "description": s.description,
156
+ "oauth_provider": s.oauth_provider,
157
+ "oauth_scopes": s.oauth_scopes,
158
+ "required_env": s.required_env,
159
+ }
160
+ for s in oauth_services
161
+ ]
162
+ console.print(json.dumps(data, indent=2))
163
+ return CommandResult.success_result(
164
+ "Services listed", data={"services": data}
165
+ )
166
+
167
+ # Table format
168
+ table = Table(title="OAuth-Capable MCP Services")
169
+ table.add_column("Service", style="cyan")
170
+ table.add_column("Provider", style="green")
171
+ table.add_column("Description", style="white")
172
+
173
+ for service in oauth_services:
174
+ table.add_row(
175
+ service.name,
176
+ service.oauth_provider or "",
177
+ service.description,
178
+ )
179
+
180
+ console.print(table)
181
+ return CommandResult.success_result(
182
+ f"Found {len(oauth_services)} OAuth-capable service(s)"
183
+ )
184
+
185
+ except ImportError:
186
+ return CommandResult.error_result("MCP Service Registry not available")
187
+ except Exception as e:
188
+ return CommandResult.error_result(f"Error listing services: {e}")
189
+
190
+ def _setup_oauth(self, args) -> CommandResult:
191
+ """Set up OAuth for a service."""
192
+ service_name = args.service_name
193
+
194
+ # Get service info from registry to get provider and scopes
195
+ try:
196
+ from claude_mpm.services.mcp_service_registry import MCPServiceRegistry
197
+
198
+ service = MCPServiceRegistry.get(service_name)
199
+ if not service:
200
+ return CommandResult.error_result(f"Service '{service_name}' not found")
201
+
202
+ provider_name = service.oauth_provider
203
+ if not provider_name:
204
+ return CommandResult.error_result(
205
+ f"Service '{service_name}' does not use OAuth"
206
+ )
207
+
208
+ scopes = service.oauth_scopes or None
209
+ except ImportError:
210
+ return CommandResult.error_result("MCP Service Registry not available")
211
+
212
+ # Priority: 1) .env files, 2) environment variables, 3) interactive prompt
213
+ client_id, client_secret = _load_oauth_credentials_from_env_files()
214
+
215
+ # Fall back to environment variables if not found in .env files
216
+ if not client_id:
217
+ client_id = os.environ.get("GOOGLE_OAUTH_CLIENT_ID")
218
+ if not client_secret:
219
+ client_secret = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET")
220
+
221
+ # Set credentials in environment so OAuth provider can access them
222
+ if client_id and client_secret:
223
+ os.environ["GOOGLE_OAUTH_CLIENT_ID"] = client_id
224
+ os.environ["GOOGLE_OAUTH_CLIENT_SECRET"] = client_secret
225
+
226
+ # If credentials missing, prompt for them interactively
227
+ if not client_id or not client_secret:
228
+ console.print("\n[yellow]Google OAuth credentials not found.[/yellow]")
229
+ console.print("Checked: .env.local, .env, and environment variables.\n")
230
+ console.print(
231
+ "Get credentials from: https://console.cloud.google.com/apis/credentials\n"
232
+ )
233
+ console.print("[dim]Tip: Add to .env.local for automatic loading:[/dim]")
234
+ console.print('[dim] GOOGLE_OAUTH_CLIENT_ID="your-client-id"[/dim]')
235
+ console.print(
236
+ '[dim] GOOGLE_OAUTH_CLIENT_SECRET="your-client-secret"[/dim]\n' # pragma: allowlist secret
237
+ )
238
+
239
+ try:
240
+ from prompt_toolkit import prompt as pt_prompt
241
+
242
+ client_id = pt_prompt("Enter GOOGLE_OAUTH_CLIENT_ID: ")
243
+ if not client_id.strip():
244
+ return CommandResult.error_result("Client ID is required")
245
+
246
+ client_secret = pt_prompt(
247
+ "Enter GOOGLE_OAUTH_CLIENT_SECRET: ", is_password=True
248
+ )
249
+ if not client_secret.strip():
250
+ return CommandResult.error_result("Client Secret is required")
251
+
252
+ # Set in environment for this session
253
+ os.environ["GOOGLE_OAUTH_CLIENT_ID"] = client_id.strip()
254
+ os.environ["GOOGLE_OAUTH_CLIENT_SECRET"] = client_secret.strip()
255
+ console.print("\n[green]Credentials set for this session.[/green]")
256
+
257
+ except (EOFError, KeyboardInterrupt):
258
+ return CommandResult.error_result("Credential entry cancelled")
259
+ except ImportError:
260
+ return CommandResult.error_result(
261
+ "prompt_toolkit not available for interactive input. "
262
+ "Please set GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET environment variables."
263
+ )
264
+
265
+ # Run OAuth flow
266
+ try:
267
+ from claude_mpm.auth import OAuthManager
268
+ from claude_mpm.auth.callback_server import DEFAULT_PORT
269
+ from claude_mpm.auth.providers.google import OAuthError
270
+
271
+ manager = OAuthManager()
272
+
273
+ # Get the actual callback port from the server
274
+ callback_port = DEFAULT_PORT
275
+ no_browser = getattr(args, "no_browser", False)
276
+
277
+ console.print(f"\n[cyan]Setting up OAuth for '{service_name}'...[/cyan]")
278
+ console.print(
279
+ f"Callback server listening on http://localhost:{callback_port}/callback"
280
+ )
281
+
282
+ if not no_browser:
283
+ console.print("Opening browser for authentication...")
284
+ else:
285
+ console.print(
286
+ "[yellow]Browser auto-open disabled. Please open the URL manually.[/yellow]"
287
+ )
288
+
289
+ # Run async OAuth flow - authenticate returns OAuthToken directly
290
+ # and raises OAuthError on failure
291
+ token = asyncio.run(
292
+ manager.authenticate(
293
+ service_name=service_name,
294
+ provider_name=provider_name,
295
+ scopes=scopes,
296
+ open_browser=not no_browser,
297
+ )
298
+ )
299
+
300
+ # Success - token was returned
301
+ console.print(f"\n[green]OAuth setup complete for '{service_name}'[/green]")
302
+ if token.expires_at:
303
+ console.print(f" Token expires: {token.expires_at}")
304
+ return CommandResult.success_result(
305
+ f"OAuth setup complete for '{service_name}'"
306
+ )
307
+
308
+ except OAuthError as e:
309
+ return CommandResult.error_result(f"OAuth setup failed: {e}")
310
+ except ImportError as e:
311
+ return CommandResult.error_result(f"OAuth module not available: {e}")
312
+ except Exception as e:
313
+ return CommandResult.error_result(f"Error during OAuth setup: {e}")
314
+
315
+ def _show_status(self, args) -> CommandResult:
316
+ """Show OAuth token status for a service."""
317
+ service_name = args.service_name
318
+
319
+ try:
320
+ from claude_mpm.auth import OAuthManager
321
+ from claude_mpm.auth.models import TokenStatus
322
+
323
+ manager = OAuthManager()
324
+ # get_status is synchronous and returns (TokenStatus, StoredToken | None)
325
+ token_status, stored_token = manager.get_status(service_name)
326
+
327
+ if token_status == TokenStatus.MISSING or stored_token is None:
328
+ console.print(
329
+ f"[yellow]No OAuth tokens found for '{service_name}'[/yellow]"
330
+ )
331
+ return CommandResult.success_result(
332
+ f"No tokens found for '{service_name}'"
333
+ )
334
+
335
+ # Build status dict for display
336
+ is_valid = token_status == TokenStatus.VALID
337
+ status_data = {
338
+ "valid": is_valid,
339
+ "status": token_status.name,
340
+ "expires_at": stored_token.token.expires_at,
341
+ "scopes": stored_token.token.scopes,
342
+ }
343
+
344
+ # Check output format
345
+ output_format = getattr(args, "format", "table")
346
+
347
+ if output_format == "json":
348
+ console.print(json.dumps(status_data, indent=2, default=str))
349
+ return CommandResult.success_result(
350
+ "Status displayed", data=status_data
351
+ )
352
+
353
+ # Table format
354
+ self._print_token_status(service_name, status_data)
355
+ return CommandResult.success_result("Status displayed")
356
+
357
+ except ImportError:
358
+ return CommandResult.error_result("OAuth module not available")
359
+ except Exception as e:
360
+ return CommandResult.error_result(f"Error checking status: {e}")
361
+
362
+ def _print_token_status(self, name: str, status: dict[str, Any]) -> None:
363
+ """Print token status information."""
364
+ panel_content = []
365
+ panel_content.append(f"[bold]Service:[/bold] {name}")
366
+ panel_content.append("[bold]Stored:[/bold] Yes")
367
+
368
+ if status.get("valid"):
369
+ panel_content.append("[bold]Status:[/bold] [green]Valid[/green]")
370
+ else:
371
+ panel_content.append("[bold]Status:[/bold] [red]Invalid/Expired[/red]")
372
+
373
+ if status.get("expires_at"):
374
+ panel_content.append(f"[bold]Expires:[/bold] {status['expires_at']}")
375
+
376
+ if status.get("scopes"):
377
+ scopes = ", ".join(status["scopes"])
378
+ panel_content.append(f"[bold]Scopes:[/bold] {scopes}")
379
+
380
+ panel = Panel(
381
+ "\n".join(panel_content),
382
+ title="OAuth Token Status",
383
+ border_style="green" if status.get("valid") else "red",
384
+ )
385
+ console.print(panel)
386
+
387
+ def _revoke_tokens(self, args) -> CommandResult:
388
+ """Revoke OAuth tokens for a service."""
389
+ service_name = args.service_name
390
+
391
+ # Confirm unless -y flag
392
+ if not getattr(args, "yes", False):
393
+ console.print(
394
+ f"[yellow]This will revoke OAuth tokens for '{service_name}'.[/yellow]"
395
+ )
396
+ try:
397
+ from prompt_toolkit import prompt as pt_prompt
398
+
399
+ confirm = pt_prompt("Are you sure? (y/N): ")
400
+ if confirm.lower() not in ("y", "yes"):
401
+ return CommandResult.success_result("Revocation cancelled")
402
+ except (EOFError, KeyboardInterrupt):
403
+ return CommandResult.success_result("Revocation cancelled")
404
+ except ImportError:
405
+ # No prompt_toolkit, proceed without confirmation
406
+ pass
407
+
408
+ try:
409
+ from claude_mpm.auth import OAuthManager
410
+
411
+ manager = OAuthManager()
412
+
413
+ console.print(f"[cyan]Revoking OAuth tokens for '{service_name}'...[/cyan]")
414
+ # revoke() returns bool directly
415
+ revoked = asyncio.run(manager.revoke(service_name))
416
+
417
+ if revoked:
418
+ console.print(
419
+ f"[green]OAuth tokens revoked for '{service_name}'[/green]"
420
+ )
421
+ return CommandResult.success_result(
422
+ f"Tokens revoked for '{service_name}'"
423
+ )
424
+ return CommandResult.error_result(
425
+ f"Failed to revoke tokens for '{service_name}'"
426
+ )
427
+
428
+ except ImportError:
429
+ return CommandResult.error_result("OAuth module not available")
430
+ except Exception as e:
431
+ return CommandResult.error_result(f"Error revoking tokens: {e}")
432
+
433
+ def _refresh_tokens(self, args) -> CommandResult:
434
+ """Refresh OAuth tokens for a service."""
435
+ service_name = args.service_name
436
+
437
+ try:
438
+ from claude_mpm.auth import OAuthManager
439
+ from claude_mpm.auth.providers.google import OAuthError
440
+
441
+ manager = OAuthManager()
442
+
443
+ console.print(
444
+ f"[cyan]Refreshing OAuth tokens for '{service_name}'...[/cyan]"
445
+ )
446
+ # refresh_if_needed() returns Optional[OAuthToken]
447
+ token = asyncio.run(manager.refresh_if_needed(service_name))
448
+
449
+ if token is not None:
450
+ console.print(
451
+ f"[green]OAuth tokens refreshed for '{service_name}'[/green]"
452
+ )
453
+ if token.expires_at:
454
+ console.print(f" New expiry: {token.expires_at}")
455
+ return CommandResult.success_result(
456
+ f"Tokens refreshed for '{service_name}'"
457
+ )
458
+ return CommandResult.error_result(
459
+ f"Failed to refresh tokens for '{service_name}' - no token found or no refresh token available"
460
+ )
461
+
462
+ except OAuthError as e:
463
+ return CommandResult.error_result(f"Failed to refresh: {e}")
464
+ except ImportError:
465
+ return CommandResult.error_result("OAuth module not available")
466
+ except Exception as e:
467
+ return CommandResult.error_result(f"Error refreshing tokens: {e}")
468
+
469
+
470
+ def manage_oauth(args) -> int:
471
+ """Main entry point for OAuth management commands.
472
+
473
+ Args:
474
+ args: Parsed command line arguments
475
+
476
+ Returns:
477
+ Exit code (0 for success, non-zero for errors)
478
+ """
479
+ command = OAuthCommand()
480
+ result = command.execute(args)
481
+ return result.exit_code
@@ -159,6 +159,14 @@ def execute_command(command: str, args) -> int:
159
159
  result = summarize_command(args)
160
160
  return result if result is not None else 0
161
161
 
162
+ # Handle oauth command with lazy import
163
+ if command == "oauth":
164
+ # Lazy import to avoid loading unless needed
165
+ from .commands.oauth import manage_oauth
166
+
167
+ result = manage_oauth(args)
168
+ return result if result is not None else 0
169
+
162
170
  # Handle profile command with lazy import
163
171
  if command == "profile":
164
172
  # Lazy import to avoid loading unless needed
@@ -390,6 +398,7 @@ def execute_command(command: str, args) -> int:
390
398
  "agent-source",
391
399
  "hook-errors",
392
400
  "autotodos",
401
+ "oauth",
393
402
  ]
394
403
 
395
404
  suggestion = suggest_similar_commands(command, all_commands)
claude_mpm/cli/helpers.py CHANGED
@@ -30,7 +30,7 @@ def is_interactive_session() -> bool:
30
30
 
31
31
  def should_skip_config_check(command: str | None) -> bool:
32
32
  """Check if command should skip configuration check."""
33
- skip_commands = ["configure", "doctor", "info", "mcp", "config"]
33
+ skip_commands = ["configure", "doctor", "info", "mcp", "config", "oauth"]
34
34
  return command in skip_commands if command else False
35
35
 
36
36
 
@@ -307,6 +307,12 @@ def add_top_level_run_arguments(parser: argparse.ArgumentParser) -> None:
307
307
  action="store_true",
308
308
  help="Disable Claude in Chrome integration (passed to Claude Code)",
309
309
  )
310
+ run_group.add_argument(
311
+ "--mcp",
312
+ type=str,
313
+ metavar="SERVICES",
314
+ help="Comma-separated list of MCP services to enable for this session (e.g., --mcp kuzu-memory,mcp-ticketer)",
315
+ )
310
316
 
311
317
  # Dependency checking options (for backward compatibility at top level)
312
318
  dep_group_top = parser.add_argument_group(
@@ -509,6 +515,13 @@ def create_parser(
509
515
  except ImportError:
510
516
  pass
511
517
 
518
+ try:
519
+ from .oauth_parser import add_oauth_subparser
520
+
521
+ add_oauth_subparser(subparsers)
522
+ except ImportError:
523
+ pass
524
+
512
525
  # Add uninstall command parser
513
526
  try:
514
527
  from ..commands.uninstall import add_uninstall_parser
@@ -192,4 +192,83 @@ def add_mcp_subparser(subparsers) -> argparse.ArgumentParser:
192
192
  "--force", action="store_true", help="Force overwrite existing configuration"
193
193
  )
194
194
 
195
+ # =========================================================================
196
+ # Service Management Commands (enable/disable/list)
197
+ # =========================================================================
198
+
199
+ # Enable MCP service
200
+ enable_parser = mcp_subparsers.add_parser(
201
+ MCPCommands.ENABLE.value,
202
+ help="Enable an MCP service in configuration",
203
+ )
204
+ enable_parser.add_argument(
205
+ "service_name",
206
+ help="Name of the MCP service to enable (e.g., kuzu-memory, mcp-github)",
207
+ )
208
+ enable_parser.add_argument(
209
+ "--interactive",
210
+ "-i",
211
+ action="store_true",
212
+ help="Prompt for required credentials interactively",
213
+ )
214
+ enable_parser.add_argument(
215
+ "--env",
216
+ "-e",
217
+ action="append",
218
+ metavar="KEY=VALUE",
219
+ help="Set environment variable (can be used multiple times)",
220
+ )
221
+ enable_parser.add_argument(
222
+ "--global",
223
+ dest="use_global",
224
+ action="store_true",
225
+ help="Enable in global ~/.claude.json instead of project .mcp.json",
226
+ )
227
+
228
+ # Disable MCP service
229
+ disable_parser = mcp_subparsers.add_parser(
230
+ MCPCommands.DISABLE.value,
231
+ help="Disable an MCP service from configuration",
232
+ )
233
+ disable_parser.add_argument(
234
+ "service_name",
235
+ help="Name of the MCP service to disable",
236
+ )
237
+ disable_parser.add_argument(
238
+ "--global",
239
+ dest="use_global",
240
+ action="store_true",
241
+ help="Disable in global ~/.claude.json instead of project .mcp.json",
242
+ )
243
+
244
+ # List MCP services
245
+ list_parser = mcp_subparsers.add_parser(
246
+ MCPCommands.LIST.value,
247
+ help="List MCP services (available and enabled)",
248
+ )
249
+ list_parser.add_argument(
250
+ "--available",
251
+ "-a",
252
+ action="store_true",
253
+ help="Show all available services from registry",
254
+ )
255
+ list_parser.add_argument(
256
+ "--enabled",
257
+ "-e",
258
+ action="store_true",
259
+ help="Show only enabled services",
260
+ )
261
+ list_parser.add_argument(
262
+ "--global",
263
+ dest="use_global",
264
+ action="store_true",
265
+ help="Check global ~/.claude.json instead of project .mcp.json",
266
+ )
267
+ list_parser.add_argument(
268
+ "--verbose",
269
+ "-v",
270
+ action="store_true",
271
+ help="Show detailed service information",
272
+ )
273
+
195
274
  return mcp_parser