claude-mpm 3.9.8__py3-none-any.whl → 3.9.9__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 (44) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/base_agent.json +1 -1
  3. claude_mpm/cli/__init__.py +3 -1
  4. claude_mpm/cli/commands/__init__.py +3 -1
  5. claude_mpm/cli/commands/cleanup.py +21 -1
  6. claude_mpm/cli/commands/mcp.py +821 -0
  7. claude_mpm/cli/parser.py +148 -1
  8. claude_mpm/config/memory_guardian_config.py +325 -0
  9. claude_mpm/constants.py +13 -0
  10. claude_mpm/hooks/claude_hooks/hook_handler.py +76 -19
  11. claude_mpm/models/state_models.py +433 -0
  12. claude_mpm/services/communication/__init__.py +2 -2
  13. claude_mpm/services/communication/socketio.py +18 -16
  14. claude_mpm/services/infrastructure/__init__.py +4 -1
  15. claude_mpm/services/infrastructure/logging.py +3 -3
  16. claude_mpm/services/infrastructure/memory_guardian.py +770 -0
  17. claude_mpm/services/mcp_gateway/__init__.py +28 -12
  18. claude_mpm/services/mcp_gateway/main.py +326 -0
  19. claude_mpm/services/mcp_gateway/registry/__init__.py +6 -3
  20. claude_mpm/services/mcp_gateway/registry/service_registry.py +397 -0
  21. claude_mpm/services/mcp_gateway/registry/tool_registry.py +477 -0
  22. claude_mpm/services/mcp_gateway/server/__init__.py +9 -3
  23. claude_mpm/services/mcp_gateway/server/mcp_server.py +430 -0
  24. claude_mpm/services/mcp_gateway/server/mcp_server_simple.py +444 -0
  25. claude_mpm/services/mcp_gateway/server/stdio_handler.py +373 -0
  26. claude_mpm/services/mcp_gateway/tools/__init__.py +16 -3
  27. claude_mpm/services/mcp_gateway/tools/base_adapter.py +497 -0
  28. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +729 -0
  29. claude_mpm/services/mcp_gateway/tools/hello_world.py +551 -0
  30. claude_mpm/utils/file_utils.py +293 -0
  31. claude_mpm/utils/platform_memory.py +524 -0
  32. claude_mpm/utils/subprocess_utils.py +305 -0
  33. {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/METADATA +3 -1
  34. {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/RECORD +39 -28
  35. claude_mpm/agents/templates/.claude-mpm/memories/README.md +0 -36
  36. claude_mpm/agents/templates/.claude-mpm/memories/engineer_agent.md +0 -39
  37. claude_mpm/agents/templates/.claude-mpm/memories/qa_agent.md +0 -38
  38. claude_mpm/agents/templates/.claude-mpm/memories/research_agent.md +0 -39
  39. claude_mpm/agents/templates/.claude-mpm/memories/version_control_agent.md +0 -38
  40. /claude_mpm/agents/templates/{research_memory_efficient.json → backup/research_memory_efficient.json} +0 -0
  41. {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/WHEEL +0 -0
  42. {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/entry_points.txt +0 -0
  43. {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/licenses/LICENSE +0 -0
  44. {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,373 @@
1
+ """
2
+ STDIO Communication Handler for MCP
3
+ ====================================
4
+
5
+ Handles stdio-based communication for the MCP server.
6
+ Manages JSON-RPC message exchange over stdin/stdout.
7
+
8
+ Part of ISS-0035: MCP Server Implementation - Core Server and Tool Registry
9
+ """
10
+
11
+ import asyncio
12
+ import json
13
+ import sys
14
+ from typing import Dict, Any, Optional, Callable
15
+ from asyncio import StreamReader, StreamWriter
16
+ import traceback
17
+
18
+ from claude_mpm.services.mcp_gateway.core.interfaces import IMCPCommunication
19
+ from claude_mpm.services.mcp_gateway.core.base import BaseMCPService
20
+
21
+
22
+ class StdioHandler(BaseMCPService, IMCPCommunication):
23
+ """
24
+ STDIO-based communication handler for MCP.
25
+
26
+ WHY: The MCP protocol uses stdio (stdin/stdout) for communication between
27
+ Claude Desktop and MCP servers. This handler manages the low-level
28
+ message exchange, ensuring proper JSON-RPC formatting and error handling.
29
+
30
+ DESIGN DECISIONS:
31
+ - Use asyncio streams for non-blocking I/O
32
+ - Implement message framing with Content-Length headers (LSP-style)
33
+ - Handle both notification and request/response patterns
34
+ - Provide robust error recovery and logging
35
+ """
36
+
37
+ def __init__(self):
38
+ """Initialize the STDIO handler."""
39
+ super().__init__("StdioHandler")
40
+
41
+ # Async streams
42
+ self._reader: Optional[StreamReader] = None
43
+ self._writer: Optional[StreamWriter] = None
44
+
45
+ # Connection state
46
+ self._connected = False
47
+
48
+ # Message buffer for partial reads
49
+ self._buffer = b""
50
+
51
+ # Metrics
52
+ self._metrics = {
53
+ "messages_sent": 0,
54
+ "messages_received": 0,
55
+ "errors": 0,
56
+ "bytes_sent": 0,
57
+ "bytes_received": 0
58
+ }
59
+
60
+ async def _do_initialize(self) -> bool:
61
+ """
62
+ Initialize the STDIO handler.
63
+
64
+ Returns:
65
+ True if initialization successful
66
+ """
67
+ try:
68
+ self.log_info("Initializing STDIO handler")
69
+
70
+ # Create async streams for stdin/stdout
71
+ loop = asyncio.get_event_loop()
72
+
73
+ # For stdin
74
+ self._reader = asyncio.StreamReader()
75
+ stdin_protocol = asyncio.StreamReaderProtocol(self._reader)
76
+ await loop.connect_read_pipe(lambda: stdin_protocol, sys.stdin)
77
+
78
+ # For stdout (we'll write directly to sys.stdout)
79
+ # Note: stdout doesn't need async handling for writes
80
+
81
+ self._connected = True
82
+ self.log_info("STDIO handler initialized")
83
+ return True
84
+
85
+ except Exception as e:
86
+ self.log_error(f"Failed to initialize STDIO handler: {e}")
87
+ return False
88
+
89
+ async def _do_shutdown(self) -> None:
90
+ """Shutdown the STDIO handler."""
91
+ self.log_info("Shutting down STDIO handler")
92
+
93
+ self._connected = False
94
+
95
+ # Close streams if needed
96
+ if self._reader:
97
+ self._reader = None
98
+
99
+ self.log_info("STDIO handler shutdown complete")
100
+
101
+ async def send_message(self, message: Dict[str, Any]) -> None:
102
+ """
103
+ Send a message to the MCP client via stdout.
104
+
105
+ Uses Content-Length header for message framing (LSP-style).
106
+
107
+ Args:
108
+ message: Message to send
109
+ """
110
+ try:
111
+ if not self._connected:
112
+ raise RuntimeError("STDIO handler not connected")
113
+
114
+ # Convert message to JSON
115
+ json_str = json.dumps(message, separators=(',', ':'))
116
+ json_bytes = json_str.encode('utf-8')
117
+
118
+ # Create Content-Length header
119
+ content_length = len(json_bytes)
120
+ header = f"Content-Length: {content_length}\r\n\r\n"
121
+ header_bytes = header.encode('ascii')
122
+
123
+ # Write header and content to stdout
124
+ sys.stdout.buffer.write(header_bytes)
125
+ sys.stdout.buffer.write(json_bytes)
126
+ sys.stdout.buffer.flush()
127
+
128
+ # Update metrics
129
+ self._metrics["messages_sent"] += 1
130
+ self._metrics["bytes_sent"] += len(header_bytes) + len(json_bytes)
131
+
132
+ self.log_debug(f"Sent message: {message.get('method', message.get('id', 'unknown'))}")
133
+
134
+ except Exception as e:
135
+ self.log_error(f"Error sending message: {e}")
136
+ self._metrics["errors"] += 1
137
+ raise
138
+
139
+ async def receive_message(self) -> Optional[Dict[str, Any]]:
140
+ """
141
+ Receive a message from the MCP client via stdin.
142
+
143
+ Handles Content-Length based message framing.
144
+
145
+ Returns:
146
+ Received message or None if no message available
147
+ """
148
+ try:
149
+ if not self._connected or not self._reader:
150
+ return None
151
+
152
+ # Read header to get content length
153
+ headers = {}
154
+ while True:
155
+ line_bytes = await self._reader.readline()
156
+ if not line_bytes:
157
+ # EOF reached
158
+ self._connected = False
159
+ return None
160
+
161
+ line = line_bytes.decode('utf-8').rstrip('\r\n')
162
+
163
+ if not line:
164
+ # Empty line indicates end of headers
165
+ break
166
+
167
+ # Parse header
168
+ if ':' in line:
169
+ key, value = line.split(':', 1)
170
+ headers[key.strip()] = value.strip()
171
+
172
+ # Get content length
173
+ content_length = headers.get('Content-Length')
174
+ if not content_length:
175
+ self.log_warning("No Content-Length header found")
176
+ return None
177
+
178
+ content_length = int(content_length)
179
+
180
+ # Read content
181
+ content_bytes = await self._reader.readexactly(content_length)
182
+
183
+ # Parse JSON
184
+ message = json.loads(content_bytes.decode('utf-8'))
185
+
186
+ # Update metrics
187
+ self._metrics["messages_received"] += 1
188
+ self._metrics["bytes_received"] += len(line_bytes) + content_length
189
+
190
+ self.log_debug(f"Received message: {message.get('method', message.get('id', 'unknown'))}")
191
+
192
+ return message
193
+
194
+ except asyncio.IncompleteReadError:
195
+ self.log_warning("Incomplete read - client may have disconnected")
196
+ self._connected = False
197
+ return None
198
+ except json.JSONDecodeError as e:
199
+ self.log_error(f"Invalid JSON received: {e}")
200
+ self._metrics["errors"] += 1
201
+ return None
202
+ except Exception as e:
203
+ self.log_error(f"Error receiving message: {e}")
204
+ self._metrics["errors"] += 1
205
+ return None
206
+
207
+ async def send_response(self, request_id: str, result: Any) -> None:
208
+ """
209
+ Send a response to a request.
210
+
211
+ Args:
212
+ request_id: ID of the request being responded to
213
+ result: Result data
214
+ """
215
+ response = {
216
+ "jsonrpc": "2.0",
217
+ "id": request_id,
218
+ "result": result
219
+ }
220
+ await self.send_message(response)
221
+
222
+ async def send_error(self, request_id: str, error: str, code: int = -1) -> None:
223
+ """
224
+ Send an error response.
225
+
226
+ Args:
227
+ request_id: ID of the request that caused the error
228
+ error: Error message
229
+ code: Error code (default -1 for generic error)
230
+ """
231
+ response = {
232
+ "jsonrpc": "2.0",
233
+ "id": request_id,
234
+ "error": {
235
+ "code": code,
236
+ "message": error
237
+ }
238
+ }
239
+ await self.send_message(response)
240
+
241
+ async def send_notification(self, method: str, params: Optional[Dict[str, Any]] = None) -> None:
242
+ """
243
+ Send a notification (no response expected).
244
+
245
+ Args:
246
+ method: Notification method
247
+ params: Optional parameters
248
+ """
249
+ notification = {
250
+ "jsonrpc": "2.0",
251
+ "method": method
252
+ }
253
+ if params:
254
+ notification["params"] = params
255
+
256
+ await self.send_message(notification)
257
+
258
+ def is_connected(self) -> bool:
259
+ """
260
+ Check if communication channel is connected.
261
+
262
+ Returns:
263
+ True if connected
264
+ """
265
+ return self._connected
266
+
267
+ def get_metrics(self) -> Dict[str, Any]:
268
+ """
269
+ Get communication metrics.
270
+
271
+ Returns:
272
+ Metrics dictionary
273
+ """
274
+ return self._metrics.copy()
275
+
276
+
277
+ class AlternativeStdioHandler(StdioHandler):
278
+ """
279
+ Alternative STDIO handler using direct sys.stdin/stdout.
280
+
281
+ This implementation doesn't use asyncio streams but instead
282
+ reads directly from sys.stdin in a blocking manner, which
283
+ can be simpler for some use cases.
284
+
285
+ WHY: Some MCP implementations may work better with simpler
286
+ blocking I/O, especially when running as a subprocess.
287
+ """
288
+
289
+ async def _do_initialize(self) -> bool:
290
+ """
291
+ Initialize the alternative STDIO handler.
292
+
293
+ Returns:
294
+ True if initialization successful
295
+ """
296
+ try:
297
+ self.log_info("Initializing alternative STDIO handler")
298
+ self._connected = True
299
+ self.log_info("Alternative STDIO handler initialized")
300
+ return True
301
+
302
+ except Exception as e:
303
+ self.log_error(f"Failed to initialize alternative STDIO handler: {e}")
304
+ return False
305
+
306
+ async def receive_message(self) -> Optional[Dict[str, Any]]:
307
+ """
308
+ Receive a message using blocking I/O with asyncio executor.
309
+
310
+ Returns:
311
+ Received message or None if no message available
312
+ """
313
+ try:
314
+ if not self._connected:
315
+ return None
316
+
317
+ # Run blocking I/O in executor
318
+ loop = asyncio.get_event_loop()
319
+ message = await loop.run_in_executor(None, self._blocking_receive)
320
+
321
+ if message:
322
+ self._metrics["messages_received"] += 1
323
+ self.log_debug(f"Received message: {message.get('method', message.get('id', 'unknown'))}")
324
+
325
+ return message
326
+
327
+ except Exception as e:
328
+ self.log_error(f"Error receiving message: {e}")
329
+ self._metrics["errors"] += 1
330
+ return None
331
+
332
+ def _blocking_receive(self) -> Optional[Dict[str, Any]]:
333
+ """
334
+ Blocking receive implementation.
335
+
336
+ Returns:
337
+ Received message or None
338
+ """
339
+ try:
340
+ # Read headers
341
+ headers = {}
342
+ while True:
343
+ line = sys.stdin.readline()
344
+ if not line:
345
+ # EOF
346
+ self._connected = False
347
+ return None
348
+
349
+ line = line.rstrip('\r\n')
350
+ if not line:
351
+ # End of headers
352
+ break
353
+
354
+ if ':' in line:
355
+ key, value = line.split(':', 1)
356
+ headers[key.strip()] = value.strip()
357
+
358
+ # Get content length
359
+ content_length = headers.get('Content-Length')
360
+ if not content_length:
361
+ return None
362
+
363
+ content_length = int(content_length)
364
+
365
+ # Read content
366
+ content = sys.stdin.read(content_length)
367
+
368
+ # Parse JSON
369
+ return json.loads(content)
370
+
371
+ except Exception as e:
372
+ self.log_error(f"Error in blocking receive: {e}")
373
+ return None
@@ -2,8 +2,21 @@
2
2
  MCP Gateway Tools Module
3
3
  ========================
4
4
 
5
- Tool registry and adapter implementations for the MCP Gateway.
5
+ Tool adapters and implementations for the MCP Gateway service.
6
6
  """
7
7
 
8
- # Placeholder for future implementation
9
- # Will be implemented in ISS-0036: Tool Registry & Discovery System
8
+ from .base_adapter import (
9
+ BaseToolAdapter,
10
+ EchoToolAdapter,
11
+ CalculatorToolAdapter,
12
+ SystemInfoToolAdapter,
13
+ )
14
+ from .document_summarizer import DocumentSummarizerTool
15
+
16
+ __all__ = [
17
+ "BaseToolAdapter",
18
+ "EchoToolAdapter",
19
+ "CalculatorToolAdapter",
20
+ "SystemInfoToolAdapter",
21
+ "DocumentSummarizerTool",
22
+ ]