claude-mpm 4.0.17__py3-none-any.whl → 4.0.20__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 (46) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__main__.py +4 -0
  3. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +38 -2
  4. claude_mpm/agents/OUTPUT_STYLE.md +84 -0
  5. claude_mpm/agents/templates/qa.json +24 -12
  6. claude_mpm/cli/__init__.py +85 -1
  7. claude_mpm/cli/__main__.py +4 -0
  8. claude_mpm/cli/commands/mcp_install_commands.py +62 -5
  9. claude_mpm/cli/commands/mcp_server_commands.py +60 -79
  10. claude_mpm/cli/commands/memory.py +32 -5
  11. claude_mpm/cli/commands/run.py +33 -6
  12. claude_mpm/cli/parsers/base_parser.py +5 -0
  13. claude_mpm/cli/parsers/run_parser.py +5 -0
  14. claude_mpm/cli/utils.py +17 -4
  15. claude_mpm/core/base_service.py +1 -1
  16. claude_mpm/core/config.py +70 -5
  17. claude_mpm/core/framework_loader.py +342 -31
  18. claude_mpm/core/interactive_session.py +55 -1
  19. claude_mpm/core/oneshot_session.py +7 -1
  20. claude_mpm/core/output_style_manager.py +468 -0
  21. claude_mpm/core/unified_paths.py +190 -21
  22. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -16
  23. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +3 -0
  24. claude_mpm/init.py +1 -0
  25. claude_mpm/scripts/mcp_server.py +68 -0
  26. claude_mpm/scripts/mcp_wrapper.py +39 -0
  27. claude_mpm/services/agents/deployment/agent_deployment.py +151 -7
  28. claude_mpm/services/agents/deployment/agent_template_builder.py +37 -1
  29. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +441 -0
  30. claude_mpm/services/agents/memory/__init__.py +0 -2
  31. claude_mpm/services/agents/memory/agent_memory_manager.py +737 -43
  32. claude_mpm/services/agents/memory/content_manager.py +144 -14
  33. claude_mpm/services/agents/memory/template_generator.py +7 -354
  34. claude_mpm/services/mcp_gateway/core/singleton_manager.py +312 -0
  35. claude_mpm/services/mcp_gateway/core/startup_verification.py +315 -0
  36. claude_mpm/services/mcp_gateway/main.py +7 -0
  37. claude_mpm/services/mcp_gateway/server/stdio_server.py +184 -176
  38. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +453 -0
  39. claude_mpm/services/subprocess_launcher_service.py +5 -0
  40. {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/METADATA +1 -1
  41. {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/RECORD +45 -38
  42. {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/entry_points.txt +1 -0
  43. claude_mpm/services/agents/memory/analyzer.py +0 -430
  44. {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/WHEEL +0 -0
  45. {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/licenses/LICENSE +0 -0
  46. {claude_mpm-4.0.17.dist-info → claude_mpm-4.0.20.dist-info}/top_level.txt +0 -0
@@ -13,9 +13,10 @@ use JSON-RPC protocol, and exit cleanly when stdin closes.
13
13
  """
14
14
 
15
15
  import asyncio
16
+ import json
16
17
  import logging
17
18
  import sys
18
- from typing import Any, Dict, List, Optional
19
+ from typing import Any, Dict, Optional
19
20
 
20
21
  # Import MCP SDK components
21
22
  from mcp.server import NotificationOptions, Server
@@ -23,6 +24,9 @@ from mcp.server.models import InitializationOptions
23
24
  from mcp.server.stdio import stdio_server
24
25
  from mcp.types import TextContent, Tool
25
26
 
27
+ # Import pydantic for model patching
28
+ from pydantic import BaseModel
29
+
26
30
  from claude_mpm.core.logger import get_logger
27
31
 
28
32
  # Import unified ticket tool if available
@@ -36,6 +40,75 @@ except ImportError:
36
40
  TICKET_TOOLS_AVAILABLE = False
37
41
 
38
42
 
43
+ def apply_backward_compatibility_patches():
44
+ """
45
+ Apply backward compatibility patches for MCP protocol differences.
46
+
47
+ This function patches the MCP Server to handle missing clientInfo
48
+ in initialize requests from older Claude Desktop versions.
49
+ """
50
+ try:
51
+ from mcp.server import Server
52
+
53
+ logger = get_logger("MCPPatcher")
54
+ logger.info("Applying MCP Server message handling patch for backward compatibility")
55
+
56
+ # Store the original _handle_message method
57
+ original_handle_message = Server._handle_message
58
+
59
+ async def patched_handle_message(self, message, session, lifespan_context, raise_exceptions=False):
60
+ """Patched message handler that adds clientInfo if missing from initialize requests."""
61
+ try:
62
+ # Check if this is a request responder with initialize method
63
+ if hasattr(message, 'request') and hasattr(message.request, 'method'):
64
+ request = message.request
65
+ if (request.method == 'initialize' and
66
+ hasattr(request, 'params') and
67
+ request.params is not None):
68
+
69
+ # Convert params to dict to check for clientInfo
70
+ params_dict = request.params
71
+ if hasattr(params_dict, 'model_dump'):
72
+ params_dict = params_dict.model_dump()
73
+ elif hasattr(params_dict, 'dict'):
74
+ params_dict = params_dict.dict()
75
+
76
+ if isinstance(params_dict, dict) and 'clientInfo' not in params_dict:
77
+ logger.info("Adding default clientInfo for backward compatibility")
78
+
79
+ # Add default clientInfo
80
+ params_dict['clientInfo'] = {
81
+ 'name': 'claude-desktop',
82
+ 'version': 'unknown'
83
+ }
84
+
85
+ # Try to update the params object
86
+ if hasattr(request.params, '__dict__'):
87
+ request.params.clientInfo = params_dict['clientInfo']
88
+
89
+ # Call the original handler
90
+ return await original_handle_message(self, message, session, lifespan_context, raise_exceptions)
91
+
92
+ except Exception as e:
93
+ logger.warning(f"Error in patched message handler: {e}")
94
+ # Fall back to original handler
95
+ return await original_handle_message(self, message, session, lifespan_context, raise_exceptions)
96
+
97
+ # Apply the patch
98
+ Server._handle_message = patched_handle_message
99
+ logger.info("Applied MCP Server message handling patch")
100
+ return True
101
+
102
+ except ImportError as e:
103
+ get_logger("MCPPatcher").warning(f"Could not import MCP Server for patching: {e}")
104
+ return False
105
+ except Exception as e:
106
+ get_logger("MCPPatcher").error(f"Failed to apply backward compatibility patch: {e}")
107
+ return False
108
+
109
+
110
+
111
+
39
112
  class SimpleMCPServer:
40
113
  """
41
114
  A simple stdio-based MCP server implementation.
@@ -48,6 +121,7 @@ class SimpleMCPServer:
48
121
  - Spawned on-demand by Claude
49
122
  - Communicates via stdin/stdout
50
123
  - Exits when connection closes
124
+ - Includes backward compatibility for protocol differences
51
125
  """
52
126
 
53
127
  def __init__(self, name: str = "claude-mpm-gateway", version: str = "1.0.0"):
@@ -62,6 +136,9 @@ class SimpleMCPServer:
62
136
  self.version = version
63
137
  self.logger = get_logger("MCPStdioServer")
64
138
 
139
+ # Apply backward compatibility patches before creating server
140
+ apply_backward_compatibility_patches()
141
+
65
142
  # Create MCP server instance
66
143
  self.server = Server(name)
67
144
 
@@ -113,7 +190,7 @@ class SimpleMCPServer:
113
190
  # Default to brief
114
191
  return self._create_brief_summary(sentences, max_length)
115
192
 
116
- def _create_brief_summary(self, sentences: List[str], max_length: int) -> str:
193
+ def _create_brief_summary(self, sentences: list[str], max_length: int) -> str:
117
194
  """Create a brief summary by selecting most important sentences."""
118
195
  if not sentences:
119
196
  return ""
@@ -195,7 +272,7 @@ class SimpleMCPServer:
195
272
  return " ".join(s[1] for s in selected)
196
273
 
197
274
  def _create_detailed_summary(
198
- self, sentences: List[str], content: str, max_length: int
275
+ self, sentences: list[str], content: str, max_length: int
199
276
  ) -> str:
200
277
  """Create a detailed summary preserving document structure."""
201
278
  import re
@@ -226,7 +303,7 @@ class SimpleMCPServer:
226
303
  return " ".join(words) + ("..." if len(result.split()) > max_length else "")
227
304
 
228
305
  def _create_bullet_summary(
229
- self, sentences: List[str], content: str, max_length: int
306
+ self, sentences: list[str], content: str, max_length: int
230
307
  ) -> str:
231
308
  """Extract key points as a bullet list."""
232
309
  import re
@@ -270,7 +347,7 @@ class SimpleMCPServer:
270
347
  return "\n".join(result_lines)
271
348
 
272
349
  def _create_executive_summary(
273
- self, sentences: List[str], content: str, max_length: int
350
+ self, sentences: list[str], content: str, max_length: int
274
351
  ) -> str:
275
352
  """Create an executive summary with overview, findings, and recommendations."""
276
353
  # Allocate words across sections
@@ -356,84 +433,35 @@ class SimpleMCPServer:
356
433
  We register them using decorators on handler functions.
357
434
  """
358
435
  # Initialize unified ticket tool if available
436
+ # NOTE: Defer initialization to avoid event loop issues
359
437
  self.unified_ticket_tool = None
360
- if TICKET_TOOLS_AVAILABLE:
361
- try:
362
- self.unified_ticket_tool = UnifiedTicketTool()
363
- # Initialize the unified ticket tool
364
- asyncio.create_task(self.unified_ticket_tool.initialize())
365
- except Exception as e:
366
- self.logger.warning(f"Failed to initialize unified ticket tool: {e}")
367
- self.unified_ticket_tool = None
368
-
438
+ self._ticket_tool_initialized = False
439
+
369
440
  @self.server.list_tools()
370
- async def handle_list_tools() -> List[Tool]:
441
+ async def handle_list_tools() -> list[Tool]:
371
442
  """List available tools."""
443
+ # Initialize ticket tool lazily if needed
444
+ if not self._ticket_tool_initialized and TICKET_TOOLS_AVAILABLE:
445
+ await self._initialize_ticket_tool()
446
+
372
447
  tools = [
373
448
  Tool(
374
- name="echo",
375
- description="Echo back the provided message",
376
- inputSchema={
377
- "type": "object",
378
- "properties": {
379
- "message": {
380
- "type": "string",
381
- "description": "Message to echo",
382
- }
383
- },
384
- "required": ["message"],
385
- },
386
- ),
387
- Tool(
388
- name="calculator",
389
- description="Perform basic arithmetic calculations",
390
- inputSchema={
391
- "type": "object",
392
- "properties": {
393
- "expression": {
394
- "type": "string",
395
- "description": "Mathematical expression to evaluate",
396
- }
397
- },
398
- "required": ["expression"],
399
- },
400
- ),
401
- Tool(
402
- name="system_info",
403
- description="Get system information",
449
+ name="status",
450
+ description="Get system and service status information",
404
451
  inputSchema={
405
452
  "type": "object",
406
453
  "properties": {
407
454
  "info_type": {
408
455
  "type": "string",
409
- "enum": ["platform", "python_version", "cwd"],
410
- "description": "Type of system information to retrieve",
456
+ "enum": ["platform", "python_version", "cwd", "all"],
457
+ "description": "Type of status information to retrieve (default: all)",
458
+ "default": "all",
411
459
  }
412
460
  },
413
- "required": ["info_type"],
414
- },
415
- ),
416
- Tool(
417
- name="run_command",
418
- description="Execute a shell command",
419
- inputSchema={
420
- "type": "object",
421
- "properties": {
422
- "command": {
423
- "type": "string",
424
- "description": "Shell command to execute",
425
- },
426
- "timeout": {
427
- "type": "number",
428
- "description": "Command timeout in seconds",
429
- "default": 30,
430
- },
431
- },
432
- "required": ["command"],
433
461
  },
434
462
  ),
435
463
  Tool(
436
- name="summarize_document",
464
+ name="document_summarizer",
437
465
  description="Summarize documents or text content",
438
466
  inputSchema={
439
467
  "type": "object",
@@ -478,60 +506,17 @@ class SimpleMCPServer:
478
506
  self.logger.info(f"Listing {len(tools)} available tools")
479
507
  return tools
480
508
 
509
+
481
510
  @self.server.call_tool()
482
511
  async def handle_call_tool(
483
512
  name: str, arguments: Dict[str, Any]
484
- ) -> List[TextContent]:
513
+ ) -> list[TextContent]:
485
514
  """Handle tool invocation."""
486
515
  self.logger.info(f"Invoking tool: {name} with arguments: {arguments}")
487
516
 
488
517
  try:
489
- if name == "echo":
490
- message = arguments.get("message", "")
491
- result = f"Echo: {message}"
492
-
493
- elif name == "calculator":
494
- expression = arguments.get("expression", "")
495
- try:
496
- # Safe evaluation of mathematical expressions
497
- import ast
498
- import operator as op
499
-
500
- # Supported operators
501
- ops = {
502
- ast.Add: op.add,
503
- ast.Sub: op.sub,
504
- ast.Mult: op.mul,
505
- ast.Div: op.truediv,
506
- ast.Pow: op.pow,
507
- ast.Mod: op.mod,
508
- ast.USub: op.neg,
509
- }
510
-
511
- def eval_expr(expr):
512
- """Safely evaluate mathematical expression."""
513
-
514
- def _eval(node):
515
- if isinstance(node, ast.Constant):
516
- return node.value
517
- elif isinstance(node, ast.BinOp):
518
- return ops[type(node.op)](
519
- _eval(node.left), _eval(node.right)
520
- )
521
- elif isinstance(node, ast.UnaryOp):
522
- return ops[type(node.op)](_eval(node.operand))
523
- else:
524
- raise TypeError(f"Unsupported operation: {node}")
525
-
526
- return _eval(ast.parse(expr, mode="eval").body)
527
-
528
- result_value = eval_expr(expression)
529
- result = f"{expression} = {result_value}"
530
- except Exception as e:
531
- result = f"Error evaluating expression: {str(e)}"
532
-
533
- elif name == "system_info":
534
- info_type = arguments.get("info_type", "platform")
518
+ if name == "status":
519
+ info_type = arguments.get("info_type", "all")
535
520
 
536
521
  if info_type == "platform":
537
522
  import platform
@@ -545,77 +530,60 @@ class SimpleMCPServer:
545
530
  import os
546
531
 
547
532
  result = f"Working Directory: {os.getcwd()}"
533
+ elif info_type == "all":
534
+ import platform
535
+ import sys
536
+ import os
537
+ import datetime
538
+
539
+ result = (
540
+ f"=== System Status ===\n"
541
+ f"Platform: {platform.system()} {platform.release()}\n"
542
+ f"Python: {sys.version.split()[0]}\n"
543
+ f"Working Directory: {os.getcwd()}\n"
544
+ f"Server: {self.name} v{self.version}\n"
545
+ f"Timestamp: {datetime.datetime.now().isoformat()}\n"
546
+ f"Tools Available: status, document_summarizer{', ticket' if self.unified_ticket_tool else ''}"
547
+ )
548
548
  else:
549
549
  result = f"Unknown info type: {info_type}"
550
550
 
551
- elif name == "run_command":
552
- command = arguments.get("command", "")
553
- timeout = arguments.get("timeout", 30)
554
-
555
- import shlex
556
- import subprocess
557
-
558
- try:
559
- # Split command string into a list to avoid shell injection
560
- command_parts = shlex.split(command)
561
-
562
- # Use create_subprocess_exec instead of create_subprocess_shell
563
- # to prevent command injection vulnerabilities
564
- proc = await asyncio.create_subprocess_exec(
565
- *command_parts,
566
- stdout=subprocess.PIPE,
567
- stderr=subprocess.PIPE,
568
- )
569
-
570
- stdout, stderr = await asyncio.wait_for(
571
- proc.communicate(), timeout=timeout
572
- )
573
-
574
- if proc.returncode == 0:
575
- result = (
576
- stdout.decode()
577
- if stdout
578
- else "Command completed successfully"
579
- )
580
- else:
581
- result = f"Command failed with code {proc.returncode}: {stderr.decode()}"
582
- except asyncio.TimeoutError:
583
- result = f"Command timed out after {timeout} seconds"
584
- except ValueError as e:
585
- # Handle shlex parsing errors (e.g., unmatched quotes)
586
- result = f"Invalid command syntax: {str(e)}"
587
- except Exception as e:
588
- result = f"Error running command: {str(e)}"
589
-
590
- elif name == "summarize_document":
551
+ elif name == "document_summarizer":
591
552
  content = arguments.get("content", "")
592
553
  style = arguments.get("style", "brief")
593
554
  max_length = arguments.get("max_length", 150)
594
555
 
595
556
  result = await self._summarize_content(content, style, max_length)
596
557
 
597
- elif name == "ticket" and self.unified_ticket_tool:
598
- # Handle unified ticket tool invocations
599
- from claude_mpm.services.mcp_gateway.core.interfaces import (
600
- MCPToolInvocation,
601
- )
558
+ elif name == "ticket":
559
+ # Initialize ticket tool lazily if needed
560
+ if not self._ticket_tool_initialized and TICKET_TOOLS_AVAILABLE:
561
+ await self._initialize_ticket_tool()
562
+
563
+ if self.unified_ticket_tool:
564
+ # Handle unified ticket tool invocations
565
+ from claude_mpm.services.mcp_gateway.core.interfaces import (
566
+ MCPToolInvocation,
567
+ )
602
568
 
603
- invocation = MCPToolInvocation(
604
- tool_name=name,
605
- parameters=arguments,
606
- request_id=f"req_{name}_{id(arguments)}",
607
- )
569
+ invocation = MCPToolInvocation(
570
+ tool_name=name,
571
+ parameters=arguments,
572
+ request_id=f"req_{name}_{id(arguments)}",
573
+ )
608
574
 
609
- tool_result = await self.unified_ticket_tool.invoke(invocation)
575
+ tool_result = await self.unified_ticket_tool.invoke(invocation)
610
576
 
611
- if tool_result.success:
612
- result = (
613
- tool_result.data
614
- if isinstance(tool_result.data, str)
615
- else str(tool_result.data)
616
- )
577
+ if tool_result.success:
578
+ result = (
579
+ tool_result.data
580
+ if isinstance(tool_result.data, str)
581
+ else str(tool_result.data)
582
+ )
583
+ else:
584
+ result = f"Error: {tool_result.error}"
617
585
  else:
618
- result = f"Error: {tool_result.error}"
586
+ result = "Ticket tool not available"
619
587
 
620
588
  else:
621
589
  result = f"Unknown tool: {name}"
@@ -628,12 +596,37 @@ class SimpleMCPServer:
628
596
  self.logger.error(error_msg)
629
597
  return [TextContent(type="text", text=error_msg)]
630
598
 
599
+
600
+ async def _initialize_ticket_tool(self):
601
+ """
602
+ Initialize the unified ticket tool asynchronously.
603
+
604
+ This is called lazily when the tool is first needed,
605
+ ensuring an event loop is available.
606
+ """
607
+ if self._ticket_tool_initialized or not TICKET_TOOLS_AVAILABLE:
608
+ return
609
+
610
+ try:
611
+ self.logger.info("Initializing unified ticket tool...")
612
+ self.unified_ticket_tool = UnifiedTicketTool()
613
+ # If the tool has an async init method, call it
614
+ if hasattr(self.unified_ticket_tool, 'initialize'):
615
+ await self.unified_ticket_tool.initialize()
616
+ self._ticket_tool_initialized = True
617
+ self.logger.info("Unified ticket tool initialized successfully")
618
+ except Exception as e:
619
+ self.logger.warning(f"Failed to initialize unified ticket tool: {e}")
620
+ self.unified_ticket_tool = None
621
+ self._ticket_tool_initialized = True # Mark as attempted
622
+
631
623
  async def run(self):
632
624
  """
633
- Run the MCP server using stdio communication.
625
+ Run the MCP server using stdio communication with backward compatibility.
634
626
 
635
627
  WHY: This is the main entry point that sets up stdio communication
636
- and runs the server until the connection is closed.
628
+ and runs the server until the connection is closed. The backward
629
+ compatibility patches are applied during server initialization.
637
630
  """
638
631
  try:
639
632
  self.logger.info(f"Starting {self.name} v{self.version}")
@@ -652,7 +645,7 @@ class SimpleMCPServer:
652
645
  ),
653
646
  )
654
647
 
655
- # Run the server
648
+ # Run the server (with patches already applied)
656
649
  await self.server.run(read_stream, write_stream, init_options)
657
650
 
658
651
  self.logger.info("Server shutting down normally")
@@ -674,7 +667,16 @@ async def main():
674
667
  level=logging.INFO,
675
668
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
676
669
  stream=sys.stderr,
670
+ force=True # Force reconfiguration even if already configured
677
671
  )
672
+
673
+ # Ensure all loggers output to stderr
674
+ for logger_name in logging.Logger.manager.loggerDict:
675
+ logger = logging.getLogger(logger_name)
676
+ for handler in logger.handlers[:]:
677
+ # Remove any handlers that might write to stdout
678
+ if hasattr(handler, 'stream') and handler.stream == sys.stdout:
679
+ logger.removeHandler(handler)
678
680
 
679
681
  # Create and run server
680
682
  server = SimpleMCPServer()
@@ -683,9 +685,15 @@ async def main():
683
685
 
684
686
  def main_sync():
685
687
  """Synchronous entry point for use as a console script."""
688
+ import os
689
+ # Disable telemetry by default
690
+ os.environ.setdefault('DISABLE_TELEMETRY', '1')
686
691
  asyncio.run(main())
687
692
 
688
693
 
689
694
  if __name__ == "__main__":
695
+ import os
696
+ # Disable telemetry by default
697
+ os.environ.setdefault('DISABLE_TELEMETRY', '1')
690
698
  # Run the async main function
691
699
  main_sync()