mcp-proxy-oauth-dcr 0.1.0__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.
- mcp_proxy/__init__.py +89 -0
- mcp_proxy/__main__.py +340 -0
- mcp_proxy/auth/__init__.py +8 -0
- mcp_proxy/auth/manager.py +908 -0
- mcp_proxy/config/__init__.py +8 -0
- mcp_proxy/config/manager.py +200 -0
- mcp_proxy/exceptions.py +186 -0
- mcp_proxy/http/__init__.py +9 -0
- mcp_proxy/http/authenticated_client.py +388 -0
- mcp_proxy/http/client.py +997 -0
- mcp_proxy/logging_config.py +71 -0
- mcp_proxy/models.py +259 -0
- mcp_proxy/protocols.py +122 -0
- mcp_proxy/proxy.py +586 -0
- mcp_proxy/stdio/__init__.py +31 -0
- mcp_proxy/stdio/interface.py +580 -0
- mcp_proxy/stdio/jsonrpc.py +371 -0
- mcp_proxy/translator/__init__.py +11 -0
- mcp_proxy/translator/translator.py +691 -0
- mcp_proxy_oauth_dcr-0.1.0.dist-info/METADATA +167 -0
- mcp_proxy_oauth_dcr-0.1.0.dist-info/RECORD +25 -0
- mcp_proxy_oauth_dcr-0.1.0.dist-info/WHEEL +5 -0
- mcp_proxy_oauth_dcr-0.1.0.dist-info/entry_points.txt +2 -0
- mcp_proxy_oauth_dcr-0.1.0.dist-info/licenses/LICENSE +21 -0
- mcp_proxy_oauth_dcr-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
"""Stdio interface for communication with Kiro.
|
|
2
|
+
|
|
3
|
+
This module implements the stdio MCP server interface that Kiro expects,
|
|
4
|
+
handling async stdin/stdout communication, subprocess lifecycle management,
|
|
5
|
+
message queuing, and signal handling.
|
|
6
|
+
|
|
7
|
+
Requirements satisfied:
|
|
8
|
+
- 1.1: Present valid stdio MCP server interface to Kiro
|
|
9
|
+
- 1.4: Maintain message ordering and correlation between stdio and HTTP protocols
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import signal
|
|
14
|
+
import sys
|
|
15
|
+
from typing import Optional, Callable, Awaitable, Union
|
|
16
|
+
from asyncio import Queue, StreamReader, StreamWriter
|
|
17
|
+
|
|
18
|
+
from ..models import JsonRpcMessage
|
|
19
|
+
from ..exceptions import ProtocolError, InvalidJsonRpcError
|
|
20
|
+
from .jsonrpc import JsonRpcParser, JsonRpcSerializer
|
|
21
|
+
from ..logging_config import get_logger
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
# Default queue size limit to prevent unbounded memory growth
|
|
26
|
+
DEFAULT_QUEUE_SIZE = 1000
|
|
27
|
+
|
|
28
|
+
# Timeout for graceful shutdown operations
|
|
29
|
+
SHUTDOWN_TIMEOUT = 5.0
|
|
30
|
+
|
|
31
|
+
# Timeout for draining pending messages on shutdown
|
|
32
|
+
DRAIN_TIMEOUT = 2.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StdioInterface:
|
|
36
|
+
"""Stdio interface for JSON-RPC communication with Kiro.
|
|
37
|
+
|
|
38
|
+
This class manages async stdin/stdout communication, providing a clean
|
|
39
|
+
interface for sending and receiving JSON-RPC messages. It handles:
|
|
40
|
+
- Async reading from stdin with proper buffering
|
|
41
|
+
- Async writing to stdout with message queuing
|
|
42
|
+
- Subprocess lifecycle and signal management
|
|
43
|
+
- Graceful shutdown and cleanup
|
|
44
|
+
|
|
45
|
+
The interface maintains message ordering by using FIFO queues for both
|
|
46
|
+
incoming and outgoing messages, ensuring that messages are processed
|
|
47
|
+
and sent in the order they were received/queued.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
is_running: Whether the interface is currently running
|
|
51
|
+
pending_incoming: Number of messages waiting to be processed
|
|
52
|
+
pending_outgoing: Number of messages waiting to be sent
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
message_handler: Optional[Callable[[JsonRpcMessage], Awaitable[Optional[JsonRpcMessage]]]] = None,
|
|
58
|
+
queue_size: int = DEFAULT_QUEUE_SIZE
|
|
59
|
+
):
|
|
60
|
+
"""Initialize the stdio interface.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
message_handler: Optional async callback for handling incoming messages.
|
|
64
|
+
Should accept a JsonRpcMessage and return a JsonRpcMessage response
|
|
65
|
+
or None for notifications.
|
|
66
|
+
queue_size: Maximum size for message queues (default: 1000)
|
|
67
|
+
"""
|
|
68
|
+
self._message_handler = message_handler
|
|
69
|
+
self._running = False
|
|
70
|
+
self._shutdown_event = asyncio.Event()
|
|
71
|
+
self._started_event = asyncio.Event()
|
|
72
|
+
|
|
73
|
+
# Message queues for async processing with size limits
|
|
74
|
+
self._incoming_queue: Queue[JsonRpcMessage] = Queue(maxsize=queue_size)
|
|
75
|
+
self._outgoing_queue: Queue[JsonRpcMessage] = Queue(maxsize=queue_size)
|
|
76
|
+
|
|
77
|
+
# Stream readers/writers
|
|
78
|
+
self._stdin_reader: Optional[StreamReader] = None
|
|
79
|
+
self._stdout_writer: Optional[StreamWriter] = None
|
|
80
|
+
|
|
81
|
+
# Background tasks
|
|
82
|
+
self._tasks: list[asyncio.Task] = []
|
|
83
|
+
|
|
84
|
+
# Parser and serializer
|
|
85
|
+
self._parser = JsonRpcParser()
|
|
86
|
+
self._serializer = JsonRpcSerializer()
|
|
87
|
+
|
|
88
|
+
# Track if we're in the middle of shutdown
|
|
89
|
+
self._shutting_down = False
|
|
90
|
+
|
|
91
|
+
logger.info("StdioInterface initialized", queue_size=queue_size)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def is_running(self) -> bool:
|
|
95
|
+
"""Check if the interface is currently running."""
|
|
96
|
+
return self._running
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def pending_incoming(self) -> int:
|
|
100
|
+
"""Get the number of messages waiting to be processed."""
|
|
101
|
+
return self._incoming_queue.qsize()
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def pending_outgoing(self) -> int:
|
|
105
|
+
"""Get the number of messages waiting to be sent."""
|
|
106
|
+
return self._outgoing_queue.qsize()
|
|
107
|
+
|
|
108
|
+
def set_message_handler(
|
|
109
|
+
self,
|
|
110
|
+
handler: Callable[[JsonRpcMessage], Awaitable[Optional[JsonRpcMessage]]]
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Set or update the message handler.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
handler: Async callback for handling incoming messages.
|
|
116
|
+
Should return a JsonRpcMessage for requests or None for notifications.
|
|
117
|
+
"""
|
|
118
|
+
self._message_handler = handler
|
|
119
|
+
logger.debug("Message handler updated")
|
|
120
|
+
|
|
121
|
+
async def start(self) -> None:
|
|
122
|
+
"""Start the stdio interface and begin processing messages.
|
|
123
|
+
|
|
124
|
+
This method:
|
|
125
|
+
1. Sets up stdin/stdout streams
|
|
126
|
+
2. Installs signal handlers for graceful shutdown
|
|
127
|
+
3. Starts background tasks for reading, writing, and processing
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
RuntimeError: If interface is already running
|
|
131
|
+
"""
|
|
132
|
+
if self._running:
|
|
133
|
+
raise RuntimeError("StdioInterface is already running")
|
|
134
|
+
|
|
135
|
+
logger.info("Starting StdioInterface")
|
|
136
|
+
self._running = True
|
|
137
|
+
self._shutting_down = False
|
|
138
|
+
self._shutdown_event.clear()
|
|
139
|
+
self._started_event.clear()
|
|
140
|
+
|
|
141
|
+
# Set up stdin/stdout streams
|
|
142
|
+
await self._setup_streams()
|
|
143
|
+
|
|
144
|
+
# Install signal handlers for graceful shutdown
|
|
145
|
+
self._install_signal_handlers()
|
|
146
|
+
|
|
147
|
+
# Start background tasks
|
|
148
|
+
self._tasks = [
|
|
149
|
+
asyncio.create_task(self._read_loop(), name="stdio-read"),
|
|
150
|
+
asyncio.create_task(self._write_loop(), name="stdio-write"),
|
|
151
|
+
asyncio.create_task(self._process_loop(), name="stdio-process"),
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
# Signal that we're fully started
|
|
155
|
+
self._started_event.set()
|
|
156
|
+
|
|
157
|
+
logger.info("StdioInterface started successfully")
|
|
158
|
+
|
|
159
|
+
async def wait_until_started(self, timeout: Optional[float] = None) -> bool:
|
|
160
|
+
"""Wait until the interface has fully started.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
timeout: Maximum time to wait in seconds (None for no timeout)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if started successfully, False if timeout occurred
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
if timeout is not None:
|
|
170
|
+
await asyncio.wait_for(self._started_event.wait(), timeout=timeout)
|
|
171
|
+
else:
|
|
172
|
+
await self._started_event.wait()
|
|
173
|
+
return True
|
|
174
|
+
except asyncio.TimeoutError:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
async def stop(self) -> None:
|
|
178
|
+
"""Stop the stdio interface and cleanup resources.
|
|
179
|
+
|
|
180
|
+
This method:
|
|
181
|
+
1. Signals shutdown to all background tasks
|
|
182
|
+
2. Drains pending outgoing messages
|
|
183
|
+
3. Waits for tasks to complete gracefully
|
|
184
|
+
4. Closes streams and cleans up resources
|
|
185
|
+
"""
|
|
186
|
+
if not self._running:
|
|
187
|
+
logger.warning("StdioInterface is not running")
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
if self._shutting_down:
|
|
191
|
+
logger.warning("StdioInterface is already shutting down")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
logger.info("Stopping StdioInterface")
|
|
195
|
+
self._shutting_down = True
|
|
196
|
+
self._running = False
|
|
197
|
+
self._shutdown_event.set()
|
|
198
|
+
|
|
199
|
+
# Drain pending outgoing messages before stopping
|
|
200
|
+
await self._drain_outgoing_queue()
|
|
201
|
+
|
|
202
|
+
# Cancel all background tasks
|
|
203
|
+
for task in self._tasks:
|
|
204
|
+
if not task.done():
|
|
205
|
+
task.cancel()
|
|
206
|
+
|
|
207
|
+
# Wait for tasks to complete (with timeout)
|
|
208
|
+
if self._tasks:
|
|
209
|
+
try:
|
|
210
|
+
await asyncio.wait_for(
|
|
211
|
+
asyncio.gather(*self._tasks, return_exceptions=True),
|
|
212
|
+
timeout=SHUTDOWN_TIMEOUT
|
|
213
|
+
)
|
|
214
|
+
except asyncio.TimeoutError:
|
|
215
|
+
logger.warning("Timeout waiting for tasks to complete")
|
|
216
|
+
|
|
217
|
+
# Close streams
|
|
218
|
+
await self._close_streams()
|
|
219
|
+
|
|
220
|
+
# Clear queues
|
|
221
|
+
self._clear_queues()
|
|
222
|
+
|
|
223
|
+
self._tasks.clear()
|
|
224
|
+
self._shutting_down = False
|
|
225
|
+
logger.info("StdioInterface stopped")
|
|
226
|
+
|
|
227
|
+
async def _drain_outgoing_queue(self) -> None:
|
|
228
|
+
"""Drain and send all pending outgoing messages before shutdown.
|
|
229
|
+
|
|
230
|
+
This ensures that any queued responses are sent before the interface
|
|
231
|
+
stops, preventing message loss.
|
|
232
|
+
"""
|
|
233
|
+
if not self._stdout_writer or self._outgoing_queue.empty():
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
logger.info("Draining outgoing message queue", pending=self._outgoing_queue.qsize())
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
async with asyncio.timeout(DRAIN_TIMEOUT):
|
|
240
|
+
while not self._outgoing_queue.empty():
|
|
241
|
+
try:
|
|
242
|
+
message = self._outgoing_queue.get_nowait()
|
|
243
|
+
line = self._serializer.serialize(message)
|
|
244
|
+
self._stdout_writer.write(line.encode('utf-8'))
|
|
245
|
+
await self._stdout_writer.drain()
|
|
246
|
+
logger.debug("Drained message", message_id=message.id)
|
|
247
|
+
except asyncio.QueueEmpty:
|
|
248
|
+
break
|
|
249
|
+
except asyncio.TimeoutError:
|
|
250
|
+
logger.warning("Timeout draining outgoing queue", remaining=self._outgoing_queue.qsize())
|
|
251
|
+
|
|
252
|
+
async def send_message(self, message: JsonRpcMessage) -> None:
|
|
253
|
+
"""Send a JSON-RPC message to stdout.
|
|
254
|
+
|
|
255
|
+
Messages are queued and sent asynchronously in FIFO order,
|
|
256
|
+
maintaining message ordering as required by the MCP protocol.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
message: JsonRpcMessage to send
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
RuntimeError: If interface is not running
|
|
263
|
+
asyncio.QueueFull: If the outgoing queue is full
|
|
264
|
+
"""
|
|
265
|
+
if not self._running and not self._shutting_down:
|
|
266
|
+
raise RuntimeError("StdioInterface is not running")
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
# Use put_nowait to avoid blocking if queue is full
|
|
270
|
+
self._outgoing_queue.put_nowait(message)
|
|
271
|
+
logger.debug("Queued outgoing message", message_id=message.id)
|
|
272
|
+
except asyncio.QueueFull:
|
|
273
|
+
logger.error("Outgoing queue is full, message dropped", message_id=message.id)
|
|
274
|
+
raise
|
|
275
|
+
|
|
276
|
+
async def receive_message(self, timeout: Optional[float] = None) -> Optional[JsonRpcMessage]:
|
|
277
|
+
"""Receive a JSON-RPC message from stdin.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
timeout: Maximum time to wait in seconds (None for no timeout)
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
JsonRpcMessage received from stdin, or None if timeout occurred
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
RuntimeError: If interface is not running
|
|
287
|
+
"""
|
|
288
|
+
if not self._running:
|
|
289
|
+
raise RuntimeError("StdioInterface is not running")
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
if timeout is not None:
|
|
293
|
+
message = await asyncio.wait_for(
|
|
294
|
+
self._incoming_queue.get(),
|
|
295
|
+
timeout=timeout
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
message = await self._incoming_queue.get()
|
|
299
|
+
logger.debug("Received message from queue", message_id=message.id)
|
|
300
|
+
return message
|
|
301
|
+
except asyncio.TimeoutError:
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
async def wait_for_shutdown(self) -> None:
|
|
305
|
+
"""Wait for shutdown signal.
|
|
306
|
+
|
|
307
|
+
This method blocks until the interface is signaled to shut down,
|
|
308
|
+
useful for keeping the main process alive.
|
|
309
|
+
"""
|
|
310
|
+
await self._shutdown_event.wait()
|
|
311
|
+
|
|
312
|
+
# ========================================================================
|
|
313
|
+
# Private Methods - Stream Setup
|
|
314
|
+
# ========================================================================
|
|
315
|
+
|
|
316
|
+
async def _setup_streams(self) -> None:
|
|
317
|
+
"""Set up async stdin/stdout streams."""
|
|
318
|
+
loop = asyncio.get_event_loop()
|
|
319
|
+
|
|
320
|
+
# Create stdin reader
|
|
321
|
+
self._stdin_reader = asyncio.StreamReader()
|
|
322
|
+
protocol = asyncio.StreamReaderProtocol(self._stdin_reader)
|
|
323
|
+
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
324
|
+
|
|
325
|
+
# Create stdout writer
|
|
326
|
+
transport, protocol = await loop.connect_write_pipe(
|
|
327
|
+
asyncio.streams.FlowControlMixin,
|
|
328
|
+
sys.stdout
|
|
329
|
+
)
|
|
330
|
+
self._stdout_writer = asyncio.StreamWriter(
|
|
331
|
+
transport, protocol, None, loop
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
logger.debug("Stdio streams set up successfully")
|
|
335
|
+
|
|
336
|
+
async def _close_streams(self) -> None:
|
|
337
|
+
"""Close stdin/stdout streams."""
|
|
338
|
+
if self._stdout_writer:
|
|
339
|
+
try:
|
|
340
|
+
self._stdout_writer.close()
|
|
341
|
+
await self._stdout_writer.wait_closed()
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.error(f"Error closing stdout writer: {e}")
|
|
344
|
+
|
|
345
|
+
self._stdin_reader = None
|
|
346
|
+
self._stdout_writer = None
|
|
347
|
+
logger.debug("Stdio streams closed")
|
|
348
|
+
|
|
349
|
+
# ========================================================================
|
|
350
|
+
# Private Methods - Signal Handling
|
|
351
|
+
# ========================================================================
|
|
352
|
+
|
|
353
|
+
def _install_signal_handlers(self) -> None:
|
|
354
|
+
"""Install signal handlers for graceful shutdown."""
|
|
355
|
+
loop = asyncio.get_event_loop()
|
|
356
|
+
|
|
357
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
358
|
+
try:
|
|
359
|
+
loop.add_signal_handler(
|
|
360
|
+
sig,
|
|
361
|
+
lambda s=sig: asyncio.create_task(self._handle_signal(s))
|
|
362
|
+
)
|
|
363
|
+
logger.debug(f"Installed signal handler for {sig.name}")
|
|
364
|
+
except NotImplementedError:
|
|
365
|
+
# Signal handlers not supported on this platform (e.g., Windows)
|
|
366
|
+
logger.warning(f"Signal handlers not supported for {sig.name}")
|
|
367
|
+
|
|
368
|
+
async def _handle_signal(self, sig: signal.Signals) -> None:
|
|
369
|
+
"""Handle shutdown signals.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
sig: Signal that was received
|
|
373
|
+
"""
|
|
374
|
+
logger.info(f"Received signal {sig.name}, initiating shutdown")
|
|
375
|
+
await self.stop()
|
|
376
|
+
|
|
377
|
+
# ========================================================================
|
|
378
|
+
# Private Methods - Background Tasks
|
|
379
|
+
# ========================================================================
|
|
380
|
+
|
|
381
|
+
async def _read_loop(self) -> None:
|
|
382
|
+
"""Background task for reading messages from stdin.
|
|
383
|
+
|
|
384
|
+
Continuously reads newline-delimited JSON-RPC messages from stdin
|
|
385
|
+
and queues them for processing. Maintains message ordering by
|
|
386
|
+
processing messages in the order they are received.
|
|
387
|
+
"""
|
|
388
|
+
logger.info("Read loop started")
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
while self._running and self._stdin_reader:
|
|
392
|
+
try:
|
|
393
|
+
# Read a line from stdin (newline-delimited)
|
|
394
|
+
line_bytes = await self._stdin_reader.readline()
|
|
395
|
+
|
|
396
|
+
if not line_bytes:
|
|
397
|
+
# EOF reached - initiate graceful shutdown
|
|
398
|
+
logger.info("EOF reached on stdin, initiating shutdown")
|
|
399
|
+
asyncio.create_task(self.stop())
|
|
400
|
+
break
|
|
401
|
+
|
|
402
|
+
# Decode and parse the message
|
|
403
|
+
line = line_bytes.decode('utf-8')
|
|
404
|
+
|
|
405
|
+
# Skip empty lines
|
|
406
|
+
if not line.strip():
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
message = self._parser.parse(line)
|
|
411
|
+
try:
|
|
412
|
+
self._incoming_queue.put_nowait(message)
|
|
413
|
+
logger.debug("Read message from stdin", message_id=message.id, method=message.method)
|
|
414
|
+
except asyncio.QueueFull:
|
|
415
|
+
logger.error("Incoming queue full, message dropped", message_id=message.id)
|
|
416
|
+
except InvalidJsonRpcError as e:
|
|
417
|
+
logger.error("Invalid JSON-RPC message", error=str(e))
|
|
418
|
+
# Continue reading despite parse errors
|
|
419
|
+
|
|
420
|
+
except asyncio.CancelledError:
|
|
421
|
+
logger.info("Read loop cancelled")
|
|
422
|
+
break
|
|
423
|
+
except Exception as e:
|
|
424
|
+
logger.error("Error in read loop", error=str(e), exc_info=True)
|
|
425
|
+
# Continue reading despite errors
|
|
426
|
+
await asyncio.sleep(0.1)
|
|
427
|
+
|
|
428
|
+
finally:
|
|
429
|
+
logger.info("Read loop stopped")
|
|
430
|
+
|
|
431
|
+
async def _write_loop(self) -> None:
|
|
432
|
+
"""Background task for writing messages to stdout.
|
|
433
|
+
|
|
434
|
+
Continuously processes the outgoing message queue and writes
|
|
435
|
+
messages to stdout in newline-delimited format. Messages are
|
|
436
|
+
sent in FIFO order to maintain message ordering.
|
|
437
|
+
"""
|
|
438
|
+
logger.info("Write loop started")
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
while self._running:
|
|
442
|
+
try:
|
|
443
|
+
# Wait for a message with timeout to allow checking shutdown
|
|
444
|
+
try:
|
|
445
|
+
message = await asyncio.wait_for(
|
|
446
|
+
self._outgoing_queue.get(),
|
|
447
|
+
timeout=1.0
|
|
448
|
+
)
|
|
449
|
+
except asyncio.TimeoutError:
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
if not self._stdout_writer:
|
|
453
|
+
logger.error("Stdout writer not available")
|
|
454
|
+
continue
|
|
455
|
+
|
|
456
|
+
# Serialize and write the message
|
|
457
|
+
line = self._serializer.serialize(message)
|
|
458
|
+
self._stdout_writer.write(line.encode('utf-8'))
|
|
459
|
+
await self._stdout_writer.drain()
|
|
460
|
+
|
|
461
|
+
logger.debug("Wrote message to stdout", message_id=message.id)
|
|
462
|
+
|
|
463
|
+
except asyncio.CancelledError:
|
|
464
|
+
logger.info("Write loop cancelled")
|
|
465
|
+
break
|
|
466
|
+
except Exception as e:
|
|
467
|
+
logger.error("Error in write loop", error=str(e), exc_info=True)
|
|
468
|
+
await asyncio.sleep(0.1)
|
|
469
|
+
|
|
470
|
+
finally:
|
|
471
|
+
logger.info("Write loop stopped")
|
|
472
|
+
|
|
473
|
+
async def _process_loop(self) -> None:
|
|
474
|
+
"""Background task for processing incoming messages.
|
|
475
|
+
|
|
476
|
+
Continuously processes incoming messages using the message handler
|
|
477
|
+
and queues responses for sending. Handles both requests (which expect
|
|
478
|
+
responses) and notifications (which don't).
|
|
479
|
+
|
|
480
|
+
Messages are processed in FIFO order to maintain message ordering
|
|
481
|
+
as required by the MCP protocol.
|
|
482
|
+
"""
|
|
483
|
+
logger.info("Process loop started")
|
|
484
|
+
|
|
485
|
+
if not self._message_handler:
|
|
486
|
+
logger.warning("No message handler set, messages will not be processed")
|
|
487
|
+
# Still need to consume messages to prevent queue from filling up
|
|
488
|
+
while self._running:
|
|
489
|
+
try:
|
|
490
|
+
try:
|
|
491
|
+
message = await asyncio.wait_for(
|
|
492
|
+
self._incoming_queue.get(),
|
|
493
|
+
timeout=1.0
|
|
494
|
+
)
|
|
495
|
+
logger.warning("Message received but no handler set", message_id=message.id)
|
|
496
|
+
except asyncio.TimeoutError:
|
|
497
|
+
continue
|
|
498
|
+
except asyncio.CancelledError:
|
|
499
|
+
break
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
while self._running:
|
|
504
|
+
try:
|
|
505
|
+
# Wait for a message with timeout to allow checking shutdown
|
|
506
|
+
try:
|
|
507
|
+
message = await asyncio.wait_for(
|
|
508
|
+
self._incoming_queue.get(),
|
|
509
|
+
timeout=1.0
|
|
510
|
+
)
|
|
511
|
+
except asyncio.TimeoutError:
|
|
512
|
+
continue
|
|
513
|
+
|
|
514
|
+
# Process the message using the handler
|
|
515
|
+
try:
|
|
516
|
+
response = await self._message_handler(message)
|
|
517
|
+
|
|
518
|
+
# Queue the response for sending (if any)
|
|
519
|
+
# Notifications (messages without ID) don't get responses
|
|
520
|
+
if response is not None:
|
|
521
|
+
try:
|
|
522
|
+
self._outgoing_queue.put_nowait(response)
|
|
523
|
+
logger.debug("Processed message, queued response",
|
|
524
|
+
request_id=message.id, response_id=response.id)
|
|
525
|
+
except asyncio.QueueFull:
|
|
526
|
+
logger.error("Outgoing queue full, response dropped",
|
|
527
|
+
message_id=message.id)
|
|
528
|
+
elif message.is_notification():
|
|
529
|
+
logger.debug("Processed notification", method=message.method)
|
|
530
|
+
else:
|
|
531
|
+
logger.debug("Handler returned no response", message_id=message.id)
|
|
532
|
+
|
|
533
|
+
except Exception as e:
|
|
534
|
+
logger.error("Error processing message",
|
|
535
|
+
message_id=message.id, error=str(e), exc_info=True)
|
|
536
|
+
# Create error response for requests (not notifications)
|
|
537
|
+
if message.id is not None:
|
|
538
|
+
from .jsonrpc import create_error_response
|
|
539
|
+
error_response = create_error_response(
|
|
540
|
+
request_id=message.id,
|
|
541
|
+
code=-32603,
|
|
542
|
+
message=f"Internal error: {str(e)}"
|
|
543
|
+
)
|
|
544
|
+
try:
|
|
545
|
+
self._outgoing_queue.put_nowait(error_response)
|
|
546
|
+
except asyncio.QueueFull:
|
|
547
|
+
logger.error("Outgoing queue full, error response dropped",
|
|
548
|
+
message_id=message.id)
|
|
549
|
+
|
|
550
|
+
except asyncio.CancelledError:
|
|
551
|
+
logger.info("Process loop cancelled")
|
|
552
|
+
break
|
|
553
|
+
except Exception as e:
|
|
554
|
+
logger.error("Error in process loop", error=str(e), exc_info=True)
|
|
555
|
+
await asyncio.sleep(0.1)
|
|
556
|
+
|
|
557
|
+
finally:
|
|
558
|
+
logger.info("Process loop stopped")
|
|
559
|
+
|
|
560
|
+
# ========================================================================
|
|
561
|
+
# Private Methods - Utilities
|
|
562
|
+
# ========================================================================
|
|
563
|
+
|
|
564
|
+
def _clear_queues(self) -> None:
|
|
565
|
+
"""Clear all message queues."""
|
|
566
|
+
# Clear incoming queue
|
|
567
|
+
while not self._incoming_queue.empty():
|
|
568
|
+
try:
|
|
569
|
+
self._incoming_queue.get_nowait()
|
|
570
|
+
except asyncio.QueueEmpty:
|
|
571
|
+
break
|
|
572
|
+
|
|
573
|
+
# Clear outgoing queue
|
|
574
|
+
while not self._outgoing_queue.empty():
|
|
575
|
+
try:
|
|
576
|
+
self._outgoing_queue.get_nowait()
|
|
577
|
+
except asyncio.QueueEmpty:
|
|
578
|
+
break
|
|
579
|
+
|
|
580
|
+
logger.debug("Message queues cleared")
|