claude-mpm 3.9.9__py3-none-any.whl → 3.9.11__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 (38) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/memory_manager.json +155 -0
  3. claude_mpm/cli/__init__.py +15 -2
  4. claude_mpm/cli/commands/__init__.py +3 -0
  5. claude_mpm/cli/commands/mcp.py +280 -134
  6. claude_mpm/cli/commands/run_guarded.py +511 -0
  7. claude_mpm/cli/parser.py +8 -2
  8. claude_mpm/config/experimental_features.py +219 -0
  9. claude_mpm/config/memory_guardian_yaml.py +335 -0
  10. claude_mpm/constants.py +1 -0
  11. claude_mpm/core/memory_aware_runner.py +353 -0
  12. claude_mpm/services/infrastructure/context_preservation.py +537 -0
  13. claude_mpm/services/infrastructure/graceful_degradation.py +616 -0
  14. claude_mpm/services/infrastructure/health_monitor.py +775 -0
  15. claude_mpm/services/infrastructure/memory_dashboard.py +479 -0
  16. claude_mpm/services/infrastructure/memory_guardian.py +189 -15
  17. claude_mpm/services/infrastructure/restart_protection.py +642 -0
  18. claude_mpm/services/infrastructure/state_manager.py +774 -0
  19. claude_mpm/services/mcp_gateway/__init__.py +11 -11
  20. claude_mpm/services/mcp_gateway/core/__init__.py +2 -2
  21. claude_mpm/services/mcp_gateway/core/interfaces.py +10 -9
  22. claude_mpm/services/mcp_gateway/main.py +35 -5
  23. claude_mpm/services/mcp_gateway/manager.py +334 -0
  24. claude_mpm/services/mcp_gateway/registry/service_registry.py +4 -8
  25. claude_mpm/services/mcp_gateway/server/__init__.py +2 -2
  26. claude_mpm/services/mcp_gateway/server/{mcp_server.py → mcp_gateway.py} +60 -59
  27. claude_mpm/services/mcp_gateway/tools/base_adapter.py +1 -2
  28. claude_mpm/services/ticket_manager.py +8 -8
  29. claude_mpm/services/ticket_manager_di.py +5 -5
  30. claude_mpm/storage/__init__.py +9 -0
  31. claude_mpm/storage/state_storage.py +556 -0
  32. {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/METADATA +25 -2
  33. {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/RECORD +37 -24
  34. claude_mpm/services/mcp_gateway/server/mcp_server_simple.py +0 -444
  35. {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/WHEEL +0 -0
  36. {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/entry_points.txt +0 -0
  37. {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/licenses/LICENSE +0 -0
  38. {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/top_level.txt +0 -0
@@ -38,7 +38,7 @@ def manage_mcp(args):
38
38
  try:
39
39
  # Import MCP Gateway services with lazy loading
40
40
  from ...services.mcp_gateway import (
41
- MCPServer,
41
+ MCPGateway,
42
42
  ToolRegistry,
43
43
  MCPConfiguration,
44
44
  MCPServiceRegistry
@@ -107,94 +107,55 @@ def _show_help():
107
107
  print("Use 'claude-mpm mcp <command> --help' for more information")
108
108
 
109
109
 
110
+ # Daemon mode removed - MCP servers should be simple stdio responders
111
+
112
+
110
113
  async def _start_server(args, logger):
111
114
  """
112
- Start the MCP Gateway server.
113
-
114
- WHY: Users need to start the MCP server to enable tool invocation
115
- and external service integration in Claude sessions.
116
-
115
+ Start the MCP Gateway using the global manager.
116
+
117
+ WHY: Users need to start the MCP gateway to enable tool invocation
118
+ and external service integration in Claude sessions. We use the global
119
+ manager to ensure only one instance runs per installation.
120
+
117
121
  Args:
118
122
  args: Command arguments with optional port and configuration
119
123
  logger: Logger instance
120
-
124
+
121
125
  Returns:
122
126
  int: Exit code (0 for success, non-zero for failure)
123
127
  """
124
- from ...services.mcp_gateway import MCPServer, ToolRegistry, MCPConfiguration
125
- from ...services.mcp_gateway.server.stdio_handler import StdioHandler
128
+ from ...services.mcp_gateway.manager import (
129
+ start_global_gateway,
130
+ run_global_gateway,
131
+ is_gateway_running,
132
+ get_gateway_status
133
+ )
126
134
 
127
135
  try:
128
- print("Starting MCP Gateway server...")
129
-
130
- # Load configuration
131
- config_path = getattr(args, 'config_file', None)
132
- if config_path and Path(config_path).exists():
133
- logger.info(f"Loading configuration from: {config_path}")
134
- config = MCPConfiguration(config_path=Path(config_path))
135
- await config.initialize()
136
- else:
137
- logger.info("Using default MCP configuration")
138
- config = MCPConfiguration()
139
- await config.initialize()
140
-
141
- # Initialize server components
142
- server = MCPServer(
143
- server_name=config.get("mcp.server.name", "claude-mpm-gateway"),
144
- version=config.get("mcp.server.version", "1.0.0")
145
- )
146
-
147
- # Initialize tool registry
148
- tool_registry = ToolRegistry()
149
-
150
- # Load default tools if enabled
151
- if config.get("mcp.tools.enabled", True):
152
- logger.info("Loading default tools")
153
- _load_default_tools(tool_registry, logger)
154
-
155
- # Set dependencies
156
- server.set_tool_registry(tool_registry)
157
-
158
- # Start server based on mode
159
- mode = getattr(args, 'mode', 'stdio')
160
-
161
- if mode == 'stdio':
162
- # Standard I/O mode for Claude integration
163
- logger.info("Starting MCP server in stdio mode")
164
- stdio_handler = StdioHandler(server)
165
-
166
- # Run the server
167
- asyncio.run(stdio_handler.run())
168
-
169
- elif mode == 'standalone':
170
- # Standalone mode for testing
171
- port = getattr(args, 'port', 8766)
172
- logger.info(f"Starting MCP server in standalone mode on port {port}")
173
-
174
- # Create async event loop and run server
175
- loop = asyncio.new_event_loop()
176
- asyncio.set_event_loop(loop)
177
-
178
- async def run_standalone():
179
- await server.start()
180
- print(f"MCP Gateway server started on port {port}")
181
- print("Press Ctrl+C to stop")
182
-
183
- # Keep server running
184
- try:
185
- await asyncio.Event().wait()
186
- except KeyboardInterrupt:
187
- print("\nStopping server...")
188
- await server.stop()
189
-
190
- loop.run_until_complete(run_standalone())
191
-
192
- else:
193
- logger.error(f"Unknown server mode: {mode}")
194
- print(f"Error: Unknown server mode: {mode}")
136
+ logger.info("Starting MCP Gateway")
137
+
138
+ # Note: MCP gateways don't "run" as background services
139
+ # They are stdio-based protocol handlers activated by MCP clients
140
+
141
+ # Get configuration values
142
+ gateway_name = getattr(args, 'name', 'claude-mpm-gateway')
143
+ version = getattr(args, 'version', '1.0.0')
144
+
145
+ # Initialize the global gateway components
146
+ logger.info(f"Initializing MCP gateway: {gateway_name}")
147
+ if not await start_global_gateway(gateway_name, version):
148
+ logger.error("Failed to initialize MCP gateway")
149
+ print("Error: Failed to initialize MCP gateway")
195
150
  return 1
196
-
197
- print("MCP Gateway server stopped")
151
+
152
+ # Run the gateway stdio handler
153
+ logger.info("Starting MCP gateway stdio handler")
154
+ print("MCP Gateway ready for Claude Desktop", file=sys.stderr)
155
+ print("Listening for MCP protocol messages on stdin/stdout...", file=sys.stderr)
156
+
157
+ await run_global_gateway()
158
+
198
159
  return 0
199
160
 
200
161
  except KeyboardInterrupt:
@@ -290,23 +251,15 @@ def _show_status(args, logger):
290
251
  print("MCP Gateway Status")
291
252
  print("=" * 50)
292
253
 
293
- # Check server status
294
- pid_file = Path.home() / ".claude-mpm" / "mcp_server.pid"
295
-
296
- if pid_file.exists():
297
- with open(pid_file, 'r') as f:
298
- pid = int(f.read().strip())
299
-
300
- # Check if process is running
301
- import os
302
- try:
303
- os.kill(pid, 0)
304
- print(f"Server Status: ✅ Running (PID: {pid})")
305
- except ProcessLookupError:
306
- print("Server Status: ❌ Not running (stale PID file)")
307
- pid_file.unlink()
308
- else:
309
- print("Server Status: ❌ Not running")
254
+ # Check gateway manager status
255
+ from ...services.mcp_gateway.manager import get_gateway_status, is_gateway_running
256
+
257
+ # MCP gateways are stdio-based protocol handlers, not background services
258
+ print("Gateway Status: ℹ️ MCP protocol handler (stdio-based)")
259
+ print(" • Activated on-demand by MCP clients (Claude Desktop)")
260
+ print(" • No background processes - communicates via stdin/stdout")
261
+ print(" • Ready for Claude Desktop integration")
262
+ print(" • Test with: claude-mpm mcp test <tool_name>")
310
263
 
311
264
  print()
312
265
 
@@ -314,21 +267,23 @@ def _show_status(args, logger):
314
267
  print("Registered Tools:")
315
268
  print("-" * 30)
316
269
 
317
- # Initialize tool registry to check tools
318
- tool_registry = ToolRegistry()
319
-
320
- # Load configuration to check enabled tools
321
- config_file = Path.home() / ".claude-mpm" / "mcp_config.yaml"
322
- if config_file.exists():
323
- # For now, just load default tools if config file exists
324
- # TODO: Implement proper async config loading for CLI commands
325
- _load_default_tools(tool_registry, logger)
270
+ # Get tool registry with fallback to running server
271
+ try:
272
+ tool_registry = _create_tool_registry_with_fallback(logger)
273
+ config_file = Path.home() / ".claude-mcp" / "mcp_config.yaml"
274
+ config = _load_config_sync(config_file if config_file.exists() else None, logger)
275
+ except ValueError as e:
276
+ print(f"Configuration Error: {e}")
277
+ return 1
278
+ except Exception as e:
279
+ logger.error(f"Failed to initialize: {e}")
280
+ print(f"Error: Failed to initialize MCP components: {e}")
281
+ return 1
326
282
 
327
283
  tools = tool_registry.list_tools()
328
284
  if tools:
329
285
  for tool in tools:
330
- status = "✅" if tool.enabled else "❌"
331
- print(f" {status} {tool.name}: {tool.description}")
286
+ print(f" {tool.name}: {tool.description}")
332
287
  else:
333
288
  print(" No tools registered")
334
289
 
@@ -337,14 +292,17 @@ def _show_status(args, logger):
337
292
  # Show configuration
338
293
  print("Configuration:")
339
294
  print("-" * 30)
340
-
295
+
341
296
  if config_file.exists():
342
297
  print(f" Config file: {config_file}")
343
- print(f" Server name: {config.get('mcp.server.name', 'claude-mpm-gateway')}")
344
- print(f" Version: {config.get('mcp.server.version', '1.0.0')}")
345
- print(f" Tools enabled: {'Yes' if config.get('mcp.tools.enabled', True) else 'No'}")
346
298
  else:
347
299
  print(" Using default configuration")
300
+
301
+ server_config = config.get("mcp", {}).get("server", {})
302
+ tools_config = config.get("mcp", {}).get("tools", {})
303
+ print(f" Server name: {server_config.get('name', 'claude-mpm-gateway')}")
304
+ print(f" Version: {server_config.get('version', '1.0.0')}")
305
+ print(f" Tools enabled: {'Yes' if tools_config.get('enabled', True) else 'No'}")
348
306
 
349
307
  # Show verbose details if requested
350
308
  if getattr(args, 'verbose', False):
@@ -388,15 +346,8 @@ def _manage_tools(args, logger):
388
346
  # Check for subcommand
389
347
  action = getattr(args, 'tool_action', 'list')
390
348
 
391
- # Initialize tool registry
392
- tool_registry = ToolRegistry()
393
-
394
- # Load tools from configuration
395
- config_file = Path.home() / ".claude-mcp" / "mcp_config.yaml"
396
- if config_file.exists():
397
- # For now, just load default tools if config file exists
398
- # TODO: Implement proper async config loading for CLI commands
399
- _load_default_tools(tool_registry, logger)
349
+ # Get tool registry with fallback to running server
350
+ tool_registry = _create_tool_registry_with_fallback(logger)
400
351
 
401
352
  if action == 'list':
402
353
  # List all tools
@@ -422,8 +373,7 @@ def _manage_tools(args, logger):
422
373
  print("-" * 30)
423
374
 
424
375
  for tool in category_tools:
425
- status = "✅" if tool.enabled else "❌"
426
- print(f" {status} {tool.name}")
376
+ print(f" {tool.name}")
427
377
  print(f" {tool.description}")
428
378
 
429
379
  if getattr(args, 'verbose', False):
@@ -572,21 +522,14 @@ def _test_tool(args, logger):
572
522
  print(f"Arguments: {json.dumps(tool_args, indent=2)}")
573
523
  print("-" * 50)
574
524
 
575
- # Initialize tool registry
576
- tool_registry = ToolRegistry()
577
-
578
- # Load tools
579
- config_file = Path.home() / ".claude-mcp" / "mcp_config.yaml"
580
- if config_file.exists():
581
- # For now, just load default tools if config file exists
582
- # TODO: Implement proper async config loading for CLI commands
583
- _load_default_tools(tool_registry, logger)
525
+ # Get tool registry with fallback to running server
526
+ tool_registry = _create_tool_registry_with_fallback(logger)
584
527
 
585
528
  # Create invocation request
586
529
  invocation = MCPToolInvocation(
587
530
  tool_name=tool_name,
588
- arguments=tool_args,
589
- request_id=f"test-{tool_name}"
531
+ parameters=tool_args,
532
+ context={"source": "cli_test"}
590
533
  )
591
534
 
592
535
  # Invoke tool
@@ -594,7 +537,8 @@ def _test_tool(args, logger):
594
537
 
595
538
  if result.success:
596
539
  print("✅ Tool invocation successful!")
597
- print(f"Result: {json.dumps(result.result, indent=2)}")
540
+ print(f"Result: {json.dumps(result.data, indent=2)}")
541
+ print(f"Execution time: {result.execution_time:.3f}s")
598
542
  else:
599
543
  print("❌ Tool invocation failed!")
600
544
  print(f"Error: {result.error}")
@@ -818,4 +762,206 @@ async def _load_default_tools(tool_registry, logger):
818
762
  logger.debug(f"Loaded default tool: {tool_def.name}")
819
763
 
820
764
  except Exception as e:
821
- logger.error(f"Failed to load default tools: {e}", exc_info=True)
765
+ logger.error(f"Failed to load default tools: {e}", exc_info=True)
766
+ raise
767
+
768
+
769
+ def _load_config_sync(config_path=None, logger=None):
770
+ """
771
+ Load MCP configuration synchronously with validation.
772
+
773
+ Args:
774
+ config_path: Optional path to configuration file
775
+ logger: Optional logger for error reporting
776
+
777
+ Returns:
778
+ Dictionary with configuration data
779
+
780
+ Raises:
781
+ ValueError: If configuration is invalid
782
+ """
783
+ # Use default configuration structure
784
+ default_config = {
785
+ "mcp": {
786
+ "server": {
787
+ "name": "claude-mpm-gateway",
788
+ "version": "1.0.0",
789
+ "description": "Claude MPM MCP Gateway Server",
790
+ },
791
+ "tools": {
792
+ "enabled": True,
793
+ "auto_discover": True,
794
+ },
795
+ "logging": {
796
+ "level": "INFO",
797
+ }
798
+ }
799
+ }
800
+
801
+ if config_path and Path(config_path).exists():
802
+ try:
803
+ import yaml
804
+ with open(config_path, 'r') as f:
805
+ loaded_config = yaml.safe_load(f) or {}
806
+
807
+ # Validate configuration structure
808
+ if not isinstance(loaded_config, dict):
809
+ raise ValueError("Configuration must be a dictionary")
810
+
811
+ # Validate MCP section if present
812
+ if "mcp" in loaded_config:
813
+ mcp_config = loaded_config["mcp"]
814
+ if not isinstance(mcp_config, dict):
815
+ raise ValueError("mcp section must be a dictionary")
816
+
817
+ # Validate server section
818
+ if "server" in mcp_config:
819
+ server_config = mcp_config["server"]
820
+ if not isinstance(server_config, dict):
821
+ raise ValueError("mcp.server section must be a dictionary")
822
+
823
+ # Validate server name
824
+ if "name" in server_config:
825
+ if not isinstance(server_config["name"], str) or not server_config["name"].strip():
826
+ raise ValueError("mcp.server.name must be a non-empty string")
827
+
828
+ # Validate tools section
829
+ if "tools" in mcp_config:
830
+ tools_config = mcp_config["tools"]
831
+ if not isinstance(tools_config, dict):
832
+ raise ValueError("mcp.tools section must be a dictionary")
833
+
834
+ if "enabled" in tools_config and not isinstance(tools_config["enabled"], bool):
835
+ raise ValueError("mcp.tools.enabled must be a boolean")
836
+
837
+ # Merge with defaults
838
+ def merge_dict(base, overlay):
839
+ for key, value in overlay.items():
840
+ if key in base and isinstance(base[key], dict) and isinstance(value, dict):
841
+ merge_dict(base[key], value)
842
+ else:
843
+ base[key] = value
844
+
845
+ merge_dict(default_config, loaded_config)
846
+
847
+ if logger:
848
+ logger.info(f"Successfully loaded configuration from {config_path}")
849
+
850
+ except yaml.YAMLError as e:
851
+ error_msg = f"Invalid YAML in configuration file {config_path}: {e}"
852
+ if logger:
853
+ logger.error(error_msg)
854
+ raise ValueError(error_msg)
855
+ except ValueError as e:
856
+ error_msg = f"Configuration validation error in {config_path}: {e}"
857
+ if logger:
858
+ logger.error(error_msg)
859
+ raise
860
+ except Exception as e:
861
+ error_msg = f"Failed to load configuration from {config_path}: {e}"
862
+ if logger:
863
+ logger.warning(error_msg)
864
+ # Fall back to defaults for other errors
865
+
866
+ return default_config
867
+
868
+
869
+ def _load_default_tools_sync(tool_registry, logger):
870
+ """
871
+ Load default MCP tools into the registry synchronously.
872
+
873
+ Args:
874
+ tool_registry: ToolRegistry instance
875
+ logger: Logger instance
876
+ """
877
+ try:
878
+ # Import default tool adapters
879
+ from ...services.mcp_gateway.tools.base_adapter import (
880
+ EchoToolAdapter,
881
+ CalculatorToolAdapter,
882
+ SystemInfoToolAdapter
883
+ )
884
+
885
+ # Register default tools
886
+ default_tools = [
887
+ EchoToolAdapter(),
888
+ CalculatorToolAdapter(),
889
+ SystemInfoToolAdapter()
890
+ ]
891
+
892
+ for adapter in default_tools:
893
+ # Initialize synchronously (skip async parts for CLI)
894
+ adapter._initialized = True
895
+ tool_registry.register_tool(adapter)
896
+ tool_def = adapter.get_definition()
897
+ logger.debug(f"Loaded default tool: {tool_def.name}")
898
+
899
+ except Exception as e:
900
+ logger.error(f"Failed to load default tools: {e}", exc_info=True)
901
+
902
+
903
+ def _get_server_tools_via_mcp(logger):
904
+ """
905
+ Get tools from running MCP server via MCP protocol.
906
+
907
+ Returns:
908
+ List of tool definitions or None if server not accessible
909
+ """
910
+ try:
911
+ import json
912
+ import subprocess
913
+ import tempfile
914
+
915
+ # Create a simple MCP client request
916
+ request = {
917
+ "jsonrpc": "2.0",
918
+ "id": 1,
919
+ "method": "tools/list",
920
+ "params": {}
921
+ }
922
+
923
+ # Try to communicate with running server
924
+ pid_file = Path.home() / ".claude-mcp" / "mcp_server.pid"
925
+ if not pid_file.exists():
926
+ return None
927
+
928
+ # For now, return None since we need a proper MCP client implementation
929
+ # This is a placeholder for future implementation
930
+ return None
931
+
932
+ except Exception as e:
933
+ logger.debug(f"Could not connect to running server: {e}")
934
+ return None
935
+
936
+
937
+ def _create_tool_registry_with_fallback(logger):
938
+ """
939
+ Create tool registry, trying to connect to running server first.
940
+
941
+ Args:
942
+ logger: Logger instance
943
+
944
+ Returns:
945
+ ToolRegistry instance with tools loaded
946
+ """
947
+ from ...services.mcp_gateway import ToolRegistry
948
+
949
+ # Create registry
950
+ tool_registry = ToolRegistry()
951
+
952
+ # Try to get tools from running server
953
+ server_tools = _get_server_tools_via_mcp(logger)
954
+
955
+ if server_tools:
956
+ logger.debug("Connected to running MCP server")
957
+ # TODO: Populate registry with server tools
958
+ # For now, fall back to loading default tools
959
+
960
+ # Fallback: Load default tools locally
961
+ config_file = Path.home() / ".claude-mcp" / "mcp_config.yaml"
962
+ config = _load_config_sync(config_file if config_file.exists() else None, logger)
963
+
964
+ if config.get("mcp", {}).get("tools", {}).get("enabled", True):
965
+ _load_default_tools_sync(tool_registry, logger)
966
+
967
+ return tool_registry