claude-mpm 4.0.17__py3-none-any.whl → 4.0.19__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.
@@ -13,6 +13,7 @@ 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
19
  from typing import Any, Dict, List, Optional
@@ -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
 
@@ -356,19 +433,40 @@ 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
438
+ self._ticket_tool_initialized = False
439
+
440
+ async def _initialize_ticket_tool(self):
441
+ """
442
+ Initialize the unified ticket tool asynchronously.
443
+
444
+ This is called lazily when the tool is first needed,
445
+ ensuring an event loop is available.
446
+ """
447
+ if self._ticket_tool_initialized or not TICKET_TOOLS_AVAILABLE:
448
+ return
449
+
450
+ try:
451
+ self.logger.info("Initializing unified ticket tool...")
452
+ self.unified_ticket_tool = UnifiedTicketTool()
453
+ # If the tool has an async init method, call it
454
+ if hasattr(self.unified_ticket_tool, 'initialize'):
455
+ await self.unified_ticket_tool.initialize()
456
+ self._ticket_tool_initialized = True
457
+ self.logger.info("Unified ticket tool initialized successfully")
458
+ except Exception as e:
459
+ self.logger.warning(f"Failed to initialize unified ticket tool: {e}")
460
+ self.unified_ticket_tool = None
461
+ self._ticket_tool_initialized = True # Mark as attempted
368
462
 
369
463
  @self.server.list_tools()
370
464
  async def handle_list_tools() -> List[Tool]:
371
465
  """List available tools."""
466
+ # Initialize ticket tool lazily if needed
467
+ if not self._ticket_tool_initialized and TICKET_TOOLS_AVAILABLE:
468
+ await self._initialize_ticket_tool()
469
+
372
470
  tools = [
373
471
  Tool(
374
472
  name="echo",
@@ -594,28 +692,35 @@ class SimpleMCPServer:
594
692
 
595
693
  result = await self._summarize_content(content, style, max_length)
596
694
 
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
- )
695
+ elif name == "ticket":
696
+ # Initialize ticket tool lazily if needed
697
+ if not self._ticket_tool_initialized and TICKET_TOOLS_AVAILABLE:
698
+ await self._initialize_ticket_tool()
699
+
700
+ if self.unified_ticket_tool:
701
+ # Handle unified ticket tool invocations
702
+ from claude_mpm.services.mcp_gateway.core.interfaces import (
703
+ MCPToolInvocation,
704
+ )
602
705
 
603
- invocation = MCPToolInvocation(
604
- tool_name=name,
605
- parameters=arguments,
606
- request_id=f"req_{name}_{id(arguments)}",
607
- )
706
+ invocation = MCPToolInvocation(
707
+ tool_name=name,
708
+ parameters=arguments,
709
+ request_id=f"req_{name}_{id(arguments)}",
710
+ )
608
711
 
609
- tool_result = await self.unified_ticket_tool.invoke(invocation)
712
+ tool_result = await self.unified_ticket_tool.invoke(invocation)
610
713
 
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
- )
714
+ if tool_result.success:
715
+ result = (
716
+ tool_result.data
717
+ if isinstance(tool_result.data, str)
718
+ else str(tool_result.data)
719
+ )
720
+ else:
721
+ result = f"Error: {tool_result.error}"
617
722
  else:
618
- result = f"Error: {tool_result.error}"
723
+ result = "Ticket tool not available"
619
724
 
620
725
  else:
621
726
  result = f"Unknown tool: {name}"
@@ -628,12 +733,14 @@ class SimpleMCPServer:
628
733
  self.logger.error(error_msg)
629
734
  return [TextContent(type="text", text=error_msg)]
630
735
 
736
+
631
737
  async def run(self):
632
738
  """
633
- Run the MCP server using stdio communication.
739
+ Run the MCP server using stdio communication with backward compatibility.
634
740
 
635
741
  WHY: This is the main entry point that sets up stdio communication
636
- and runs the server until the connection is closed.
742
+ and runs the server until the connection is closed. The backward
743
+ compatibility patches are applied during server initialization.
637
744
  """
638
745
  try:
639
746
  self.logger.info(f"Starting {self.name} v{self.version}")
@@ -652,7 +759,7 @@ class SimpleMCPServer:
652
759
  ),
653
760
  )
654
761
 
655
- # Run the server
762
+ # Run the server (with patches already applied)
656
763
  await self.server.run(read_stream, write_stream, init_options)
657
764
 
658
765
  self.logger.info("Server shutting down normally")
@@ -674,7 +781,16 @@ async def main():
674
781
  level=logging.INFO,
675
782
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
676
783
  stream=sys.stderr,
784
+ force=True # Force reconfiguration even if already configured
677
785
  )
786
+
787
+ # Ensure all loggers output to stderr
788
+ for logger_name in logging.Logger.manager.loggerDict:
789
+ logger = logging.getLogger(logger_name)
790
+ for handler in logger.handlers[:]:
791
+ # Remove any handlers that might write to stdout
792
+ if hasattr(handler, 'stream') and handler.stream == sys.stdout:
793
+ logger.removeHandler(handler)
678
794
 
679
795
  # Create and run server
680
796
  server = SimpleMCPServer()