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
mcp_proxy/proxy.py
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
"""Main MCP Proxy orchestration.
|
|
2
|
+
|
|
3
|
+
This module provides the main McpProxy class that orchestrates all components:
|
|
4
|
+
- Stdio interface for communication with Kiro
|
|
5
|
+
- Protocol translator for stdio <-> HTTP conversion
|
|
6
|
+
- Authenticated HTTP client for backend communication
|
|
7
|
+
- Configuration management
|
|
8
|
+
- Graceful startup and shutdown
|
|
9
|
+
- Signal handling and cleanup
|
|
10
|
+
|
|
11
|
+
Requirements satisfied:
|
|
12
|
+
- 3.1: Perform OAuth DCR with OAuth provider on startup
|
|
13
|
+
- 2.1: Establish connections to HTTP MCP server
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
import signal
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from .auth.manager import AuthenticationManagerImpl
|
|
22
|
+
from .config.manager import ConfigurationManagerImpl
|
|
23
|
+
from .http.authenticated_client import AuthenticatedHttpClient
|
|
24
|
+
from .http.client import HttpClientImpl
|
|
25
|
+
from .models import JsonRpcMessage, ProxyConfig
|
|
26
|
+
from .translator.translator import ProtocolTranslatorImpl
|
|
27
|
+
from .stdio.interface import StdioInterface
|
|
28
|
+
from .exceptions import McpProxyError, ConfigurationError
|
|
29
|
+
from .logging_config import get_logger, configure_logging
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class McpProxy:
|
|
35
|
+
"""Main MCP Proxy orchestrator.
|
|
36
|
+
|
|
37
|
+
This class brings together all components to create a complete proxy that:
|
|
38
|
+
- Presents a stdio MCP interface to Kiro
|
|
39
|
+
- Translates between stdio and HTTP MCP protocols
|
|
40
|
+
- Manages OAuth DCR authentication
|
|
41
|
+
- Handles HTTP communication with backend MCP server
|
|
42
|
+
- Provides graceful startup and shutdown
|
|
43
|
+
|
|
44
|
+
The proxy operates as follows:
|
|
45
|
+
1. Load configuration from environment variables
|
|
46
|
+
2. Initialize authentication manager (performs DCR)
|
|
47
|
+
3. Create HTTP client with authentication
|
|
48
|
+
4. Set up protocol translator
|
|
49
|
+
5. Start stdio interface
|
|
50
|
+
6. Process messages bidirectionally
|
|
51
|
+
7. Handle shutdown signals gracefully
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
config: Proxy configuration
|
|
55
|
+
is_running: Whether the proxy is currently running
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, config: Optional[ProxyConfig] = None):
|
|
59
|
+
"""Initialize the MCP Proxy.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
config: Optional proxy configuration. If not provided,
|
|
63
|
+
configuration will be loaded from environment variables.
|
|
64
|
+
"""
|
|
65
|
+
self._config = config
|
|
66
|
+
self._running = False
|
|
67
|
+
self._shutdown_event = asyncio.Event()
|
|
68
|
+
|
|
69
|
+
# Components (initialized during startup)
|
|
70
|
+
self._config_manager: Optional[ConfigurationManagerImpl] = None
|
|
71
|
+
self._auth_manager: Optional[AuthenticationManagerImpl] = None
|
|
72
|
+
self._http_client: Optional[HttpClientImpl] = None
|
|
73
|
+
self._authenticated_client: Optional[AuthenticatedHttpClient] = None
|
|
74
|
+
self._translator: Optional[ProtocolTranslatorImpl] = None
|
|
75
|
+
self._stdio_interface: Optional[StdioInterface] = None
|
|
76
|
+
|
|
77
|
+
logger.info("McpProxy instance created")
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def config(self) -> Optional[ProxyConfig]:
|
|
81
|
+
"""Get the current configuration."""
|
|
82
|
+
return self._config
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def is_running(self) -> bool:
|
|
86
|
+
"""Check if the proxy is currently running."""
|
|
87
|
+
return self._running
|
|
88
|
+
|
|
89
|
+
async def start(self) -> None:
|
|
90
|
+
"""Start the MCP Proxy.
|
|
91
|
+
|
|
92
|
+
This method performs the complete startup sequence:
|
|
93
|
+
1. Load configuration (if not provided)
|
|
94
|
+
2. Set up logging
|
|
95
|
+
3. Initialize authentication manager (performs DCR)
|
|
96
|
+
4. Create and initialize HTTP client
|
|
97
|
+
5. Set up protocol translator
|
|
98
|
+
6. Start stdio interface
|
|
99
|
+
7. Install signal handlers
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ConfigurationError: If configuration is invalid
|
|
103
|
+
AuthenticationError: If OAuth DCR fails
|
|
104
|
+
RuntimeError: If proxy is already running
|
|
105
|
+
"""
|
|
106
|
+
if self._running:
|
|
107
|
+
raise RuntimeError("McpProxy is already running")
|
|
108
|
+
|
|
109
|
+
logger.info("Starting MCP Proxy")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
# Step 1: Load configuration
|
|
113
|
+
await self._load_configuration()
|
|
114
|
+
|
|
115
|
+
# Step 2: Set up logging
|
|
116
|
+
self._setup_logging()
|
|
117
|
+
|
|
118
|
+
# Step 3: Initialize authentication manager
|
|
119
|
+
await self._initialize_authentication()
|
|
120
|
+
|
|
121
|
+
# Step 4: Create and initialize HTTP client
|
|
122
|
+
await self._initialize_http_client()
|
|
123
|
+
|
|
124
|
+
# Step 5: Set up protocol translator
|
|
125
|
+
self._initialize_translator()
|
|
126
|
+
|
|
127
|
+
# Step 6: Start stdio interface
|
|
128
|
+
await self._start_stdio_interface()
|
|
129
|
+
|
|
130
|
+
# Step 7: Install signal handlers
|
|
131
|
+
self._install_signal_handlers()
|
|
132
|
+
|
|
133
|
+
self._running = True
|
|
134
|
+
self._shutdown_event.clear()
|
|
135
|
+
|
|
136
|
+
logger.info("MCP Proxy started successfully")
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.error(f"Failed to start MCP Proxy: {e}", exc_info=True)
|
|
140
|
+
# Clean up any partially initialized components
|
|
141
|
+
await self._cleanup()
|
|
142
|
+
raise
|
|
143
|
+
|
|
144
|
+
async def stop(self) -> None:
|
|
145
|
+
"""Stop the MCP Proxy gracefully.
|
|
146
|
+
|
|
147
|
+
This method performs graceful shutdown:
|
|
148
|
+
1. Signal shutdown to all components
|
|
149
|
+
2. Stop stdio interface (drains pending messages)
|
|
150
|
+
3. Close authenticated HTTP client
|
|
151
|
+
4. Close authentication manager
|
|
152
|
+
5. Clean up resources
|
|
153
|
+
"""
|
|
154
|
+
if not self._running:
|
|
155
|
+
logger.warning("McpProxy is not running")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
logger.info("Stopping MCP Proxy")
|
|
159
|
+
self._running = False
|
|
160
|
+
self._shutdown_event.set()
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
# Stop stdio interface first (drains pending messages)
|
|
164
|
+
if self._stdio_interface:
|
|
165
|
+
await self._stdio_interface.stop()
|
|
166
|
+
|
|
167
|
+
# Close authenticated HTTP client
|
|
168
|
+
if self._authenticated_client:
|
|
169
|
+
await self._authenticated_client.close()
|
|
170
|
+
|
|
171
|
+
# Close authentication manager
|
|
172
|
+
if self._auth_manager:
|
|
173
|
+
await self._auth_manager.close()
|
|
174
|
+
|
|
175
|
+
# Clean up remaining resources
|
|
176
|
+
await self._cleanup()
|
|
177
|
+
|
|
178
|
+
logger.info("MCP Proxy stopped successfully")
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Error during shutdown: {e}", exc_info=True)
|
|
182
|
+
raise
|
|
183
|
+
|
|
184
|
+
async def run(self) -> None:
|
|
185
|
+
"""Run the proxy until shutdown signal is received.
|
|
186
|
+
|
|
187
|
+
This method starts the proxy and waits for a shutdown signal.
|
|
188
|
+
It's the main entry point for running the proxy as a standalone process.
|
|
189
|
+
|
|
190
|
+
Usage:
|
|
191
|
+
proxy = McpProxy()
|
|
192
|
+
await proxy.run()
|
|
193
|
+
"""
|
|
194
|
+
await self.start()
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
# Wait for shutdown signal
|
|
198
|
+
logger.info("MCP Proxy running, waiting for shutdown signal")
|
|
199
|
+
await self._shutdown_event.wait()
|
|
200
|
+
except KeyboardInterrupt:
|
|
201
|
+
logger.info("Received keyboard interrupt")
|
|
202
|
+
finally:
|
|
203
|
+
await self.stop()
|
|
204
|
+
|
|
205
|
+
async def wait_for_shutdown(self) -> None:
|
|
206
|
+
"""Wait for shutdown signal.
|
|
207
|
+
|
|
208
|
+
This method blocks until the proxy is signaled to shut down.
|
|
209
|
+
Useful for keeping the main process alive while the proxy runs.
|
|
210
|
+
"""
|
|
211
|
+
await self._shutdown_event.wait()
|
|
212
|
+
|
|
213
|
+
# ========================================================================
|
|
214
|
+
# Private Methods - Initialization
|
|
215
|
+
# ========================================================================
|
|
216
|
+
|
|
217
|
+
async def _load_configuration(self) -> None:
|
|
218
|
+
"""Load configuration from environment variables if not provided."""
|
|
219
|
+
if self._config is None:
|
|
220
|
+
logger.info("Loading configuration from environment variables")
|
|
221
|
+
self._config_manager = ConfigurationManagerImpl()
|
|
222
|
+
self._config = await self._config_manager.load()
|
|
223
|
+
logger.info("Configuration loaded successfully")
|
|
224
|
+
else:
|
|
225
|
+
logger.info("Using provided configuration")
|
|
226
|
+
|
|
227
|
+
# Log configuration (excluding sensitive data)
|
|
228
|
+
logger.info(
|
|
229
|
+
"Configuration: mcp_server_url=%s, oauth_provider_url=%s, "
|
|
230
|
+
"client_name=%s, scopes=%s, connection_timeout=%s, "
|
|
231
|
+
"retry_attempts=%s, log_level=%s",
|
|
232
|
+
self._config.mcp_server_url,
|
|
233
|
+
self._config.oauth_provider_url,
|
|
234
|
+
self._config.client_name,
|
|
235
|
+
self._config.scopes,
|
|
236
|
+
self._config.connection_timeout,
|
|
237
|
+
self._config.retry_attempts,
|
|
238
|
+
self._config.log_level,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _setup_logging(self) -> None:
|
|
242
|
+
"""Set up logging based on configuration."""
|
|
243
|
+
if self._config:
|
|
244
|
+
configure_logging(self._config.log_level)
|
|
245
|
+
logger.info(f"Logging configured with level: {self._config.log_level}")
|
|
246
|
+
|
|
247
|
+
async def _initialize_authentication(self) -> None:
|
|
248
|
+
"""Initialize authentication manager and perform DCR.
|
|
249
|
+
|
|
250
|
+
This satisfies Requirement 3.1: Perform OAuth DCR on startup.
|
|
251
|
+
"""
|
|
252
|
+
if not self._config:
|
|
253
|
+
raise ConfigurationError("Configuration not loaded")
|
|
254
|
+
|
|
255
|
+
logger.info("Initializing authentication manager")
|
|
256
|
+
self._auth_manager = AuthenticationManagerImpl(self._config)
|
|
257
|
+
|
|
258
|
+
# Initialize performs DCR and obtains initial token
|
|
259
|
+
await self._auth_manager.initialize()
|
|
260
|
+
|
|
261
|
+
logger.info("Authentication manager initialized successfully")
|
|
262
|
+
|
|
263
|
+
async def _initialize_http_client(self) -> None:
|
|
264
|
+
"""Initialize HTTP client with authentication.
|
|
265
|
+
|
|
266
|
+
This satisfies Requirement 2.1: Establish connections to HTTP MCP server.
|
|
267
|
+
"""
|
|
268
|
+
if not self._config or not self._auth_manager:
|
|
269
|
+
raise RuntimeError("Configuration or authentication not initialized")
|
|
270
|
+
|
|
271
|
+
logger.info("Initializing HTTP client")
|
|
272
|
+
|
|
273
|
+
# Create retry configuration
|
|
274
|
+
from .http.client import RetryConfig
|
|
275
|
+
retry_config = RetryConfig(
|
|
276
|
+
max_retries=self._config.retry_attempts,
|
|
277
|
+
max_delay=self._config.max_backoff_seconds,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Create base HTTP client
|
|
281
|
+
self._http_client = HttpClientImpl(
|
|
282
|
+
base_url=str(self._config.mcp_server_url),
|
|
283
|
+
timeout=self._config.connection_timeout,
|
|
284
|
+
retry_config=retry_config,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Wrap with authentication
|
|
288
|
+
self._authenticated_client = AuthenticatedHttpClient(
|
|
289
|
+
http_client=self._http_client,
|
|
290
|
+
auth_manager=self._auth_manager,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Initialize authenticated client (ensures token is ready)
|
|
294
|
+
await self._authenticated_client.initialize()
|
|
295
|
+
|
|
296
|
+
logger.info("HTTP client initialized successfully")
|
|
297
|
+
|
|
298
|
+
def _initialize_translator(self) -> None:
|
|
299
|
+
"""Initialize protocol translator."""
|
|
300
|
+
if not self._config:
|
|
301
|
+
raise RuntimeError("Configuration not initialized")
|
|
302
|
+
|
|
303
|
+
logger.info("Initializing protocol translator")
|
|
304
|
+
|
|
305
|
+
self._translator = ProtocolTranslatorImpl(
|
|
306
|
+
base_url=str(self._config.mcp_server_url)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
logger.info("Protocol translator initialized successfully")
|
|
310
|
+
|
|
311
|
+
async def _start_stdio_interface(self) -> None:
|
|
312
|
+
"""Start stdio interface with message handler."""
|
|
313
|
+
logger.info("Starting stdio interface")
|
|
314
|
+
|
|
315
|
+
# Create stdio interface with message handler
|
|
316
|
+
self._stdio_interface = StdioInterface(
|
|
317
|
+
message_handler=self._handle_stdio_message
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Start the interface
|
|
321
|
+
await self._stdio_interface.start()
|
|
322
|
+
|
|
323
|
+
# Wait for interface to be fully started
|
|
324
|
+
started = await self._stdio_interface.wait_until_started(timeout=5.0)
|
|
325
|
+
if not started:
|
|
326
|
+
raise RuntimeError("Stdio interface failed to start within timeout")
|
|
327
|
+
|
|
328
|
+
logger.info("Stdio interface started successfully")
|
|
329
|
+
|
|
330
|
+
# ========================================================================
|
|
331
|
+
# Private Methods - Message Handling
|
|
332
|
+
# ========================================================================
|
|
333
|
+
|
|
334
|
+
async def _handle_stdio_message(
|
|
335
|
+
self,
|
|
336
|
+
message: JsonRpcMessage
|
|
337
|
+
) -> Optional[JsonRpcMessage]:
|
|
338
|
+
"""Handle incoming stdio message from Kiro.
|
|
339
|
+
|
|
340
|
+
This is the main message processing pipeline:
|
|
341
|
+
1. Translate stdio message to HTTP request
|
|
342
|
+
2. Send HTTP request via authenticated client
|
|
343
|
+
3. Translate HTTP response back to stdio message
|
|
344
|
+
4. Return stdio response
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
message: JSON-RPC message from Kiro
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
JSON-RPC response message, or None for notifications
|
|
351
|
+
"""
|
|
352
|
+
if not self._translator or not self._authenticated_client:
|
|
353
|
+
logger.error(
|
|
354
|
+
"Components not initialized",
|
|
355
|
+
has_translator=self._translator is not None,
|
|
356
|
+
has_authenticated_client=self._authenticated_client is not None,
|
|
357
|
+
)
|
|
358
|
+
return self._create_error_response(
|
|
359
|
+
message.id,
|
|
360
|
+
-32603,
|
|
361
|
+
"Internal error: proxy not fully initialized"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
# Handle notifications (no response expected)
|
|
366
|
+
if message.is_notification():
|
|
367
|
+
logger.debug(
|
|
368
|
+
"Received notification",
|
|
369
|
+
method=message.method,
|
|
370
|
+
)
|
|
371
|
+
# For now, we don't forward notifications to backend
|
|
372
|
+
# This could be extended to support server-to-client notifications
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
# Translate stdio request to HTTP
|
|
376
|
+
logger.debug(
|
|
377
|
+
"Translating stdio request to HTTP",
|
|
378
|
+
method=message.method,
|
|
379
|
+
id=message.id,
|
|
380
|
+
has_params=message.params is not None,
|
|
381
|
+
)
|
|
382
|
+
http_request = await self._translator.translate_stdio_to_http(message)
|
|
383
|
+
|
|
384
|
+
# Send HTTP request via authenticated client
|
|
385
|
+
logger.debug(
|
|
386
|
+
"Sending HTTP request",
|
|
387
|
+
http_method=http_request.method,
|
|
388
|
+
url=http_request.url,
|
|
389
|
+
session_id=http_request.session_id,
|
|
390
|
+
)
|
|
391
|
+
http_response = await self._authenticated_client.send_request(http_request)
|
|
392
|
+
|
|
393
|
+
# Translate HTTP response back to stdio
|
|
394
|
+
logger.debug(
|
|
395
|
+
"Translating HTTP response to stdio",
|
|
396
|
+
status=http_response.status,
|
|
397
|
+
content_type=http_response.content_type,
|
|
398
|
+
body_length=len(http_response.body) if http_response.body else 0,
|
|
399
|
+
)
|
|
400
|
+
stdio_response = await self._translator.translate_http_to_stdio(
|
|
401
|
+
http_response,
|
|
402
|
+
stdio_request_id=message.id
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Mark correlation as completed
|
|
406
|
+
if message.id is not None:
|
|
407
|
+
try:
|
|
408
|
+
self._translator.mark_correlation_completed(message.id)
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.warning(
|
|
411
|
+
"Failed to mark correlation completed",
|
|
412
|
+
error=str(e),
|
|
413
|
+
message_id=message.id,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
logger.info(
|
|
417
|
+
"Message processed successfully",
|
|
418
|
+
method=message.method,
|
|
419
|
+
request_id=message.id,
|
|
420
|
+
response_has_result=stdio_response.result is not None,
|
|
421
|
+
response_has_error=stdio_response.error is not None,
|
|
422
|
+
)
|
|
423
|
+
return stdio_response
|
|
424
|
+
|
|
425
|
+
except McpProxyError as e:
|
|
426
|
+
logger.error(
|
|
427
|
+
"Proxy error handling message",
|
|
428
|
+
error=str(e),
|
|
429
|
+
error_type=type(e).__name__,
|
|
430
|
+
message_id=message.id,
|
|
431
|
+
method=message.method,
|
|
432
|
+
has_details=hasattr(e, 'details') and e.details is not None,
|
|
433
|
+
exc_info=True,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Mark correlation as failed
|
|
437
|
+
if message.id is not None:
|
|
438
|
+
try:
|
|
439
|
+
self._translator.mark_correlation_failed(message.id)
|
|
440
|
+
except Exception:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
# Return error response
|
|
444
|
+
from .exceptions import to_jsonrpc_error
|
|
445
|
+
error_obj = to_jsonrpc_error(e)
|
|
446
|
+
return self._create_error_response(
|
|
447
|
+
message.id,
|
|
448
|
+
error_obj["code"],
|
|
449
|
+
error_obj["message"],
|
|
450
|
+
error_obj.get("data")
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
except Exception as e:
|
|
454
|
+
logger.error(
|
|
455
|
+
"Unexpected error handling message",
|
|
456
|
+
error=str(e),
|
|
457
|
+
error_type=type(e).__name__,
|
|
458
|
+
message_id=message.id,
|
|
459
|
+
method=message.method,
|
|
460
|
+
exc_info=True,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Mark correlation as failed
|
|
464
|
+
if message.id is not None:
|
|
465
|
+
try:
|
|
466
|
+
self._translator.mark_correlation_failed(message.id)
|
|
467
|
+
except Exception:
|
|
468
|
+
pass
|
|
469
|
+
|
|
470
|
+
# Return generic error response
|
|
471
|
+
return self._create_error_response(
|
|
472
|
+
message.id,
|
|
473
|
+
-32603,
|
|
474
|
+
f"Internal error: {str(e)}"
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
def _create_error_response(
|
|
478
|
+
self,
|
|
479
|
+
request_id: Optional[int | str],
|
|
480
|
+
code: int,
|
|
481
|
+
message: str,
|
|
482
|
+
data: Optional[any] = None
|
|
483
|
+
) -> JsonRpcMessage:
|
|
484
|
+
"""Create a JSON-RPC error response.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
request_id: Request ID to respond to
|
|
488
|
+
code: JSON-RPC error code
|
|
489
|
+
message: Error message
|
|
490
|
+
data: Optional additional error data
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
JSON-RPC error response
|
|
494
|
+
"""
|
|
495
|
+
from .models import JsonRpcError
|
|
496
|
+
|
|
497
|
+
return JsonRpcMessage(
|
|
498
|
+
jsonrpc="2.0",
|
|
499
|
+
id=request_id,
|
|
500
|
+
error=JsonRpcError(
|
|
501
|
+
code=code,
|
|
502
|
+
message=message,
|
|
503
|
+
data=data
|
|
504
|
+
)
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# ========================================================================
|
|
508
|
+
# Private Methods - Signal Handling
|
|
509
|
+
# ========================================================================
|
|
510
|
+
|
|
511
|
+
def _install_signal_handlers(self) -> None:
|
|
512
|
+
"""Install signal handlers for graceful shutdown."""
|
|
513
|
+
loop = asyncio.get_event_loop()
|
|
514
|
+
|
|
515
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
516
|
+
try:
|
|
517
|
+
loop.add_signal_handler(
|
|
518
|
+
sig,
|
|
519
|
+
lambda s=sig: asyncio.create_task(self._handle_signal(s))
|
|
520
|
+
)
|
|
521
|
+
logger.debug(f"Installed signal handler for {sig.name}")
|
|
522
|
+
except NotImplementedError:
|
|
523
|
+
# Signal handlers not supported on this platform (e.g., Windows)
|
|
524
|
+
logger.warning(f"Signal handlers not supported for {sig.name}")
|
|
525
|
+
|
|
526
|
+
async def _handle_signal(self, sig: signal.Signals) -> None:
|
|
527
|
+
"""Handle shutdown signals.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
sig: Signal that was received
|
|
531
|
+
"""
|
|
532
|
+
logger.info(f"Received signal {sig.name}, initiating shutdown")
|
|
533
|
+
await self.stop()
|
|
534
|
+
|
|
535
|
+
# ========================================================================
|
|
536
|
+
# Private Methods - Cleanup
|
|
537
|
+
# ========================================================================
|
|
538
|
+
|
|
539
|
+
async def _cleanup(self) -> None:
|
|
540
|
+
"""Clean up resources during shutdown or after failed startup."""
|
|
541
|
+
logger.debug("Cleaning up resources")
|
|
542
|
+
|
|
543
|
+
# Close components in reverse order of initialization
|
|
544
|
+
if self._stdio_interface and self._stdio_interface.is_running:
|
|
545
|
+
try:
|
|
546
|
+
await self._stdio_interface.stop()
|
|
547
|
+
except Exception as e:
|
|
548
|
+
logger.error(f"Error stopping stdio interface: {e}")
|
|
549
|
+
|
|
550
|
+
if self._authenticated_client:
|
|
551
|
+
try:
|
|
552
|
+
await self._authenticated_client.close()
|
|
553
|
+
except Exception as e:
|
|
554
|
+
logger.error(f"Error closing authenticated client: {e}")
|
|
555
|
+
|
|
556
|
+
if self._auth_manager:
|
|
557
|
+
try:
|
|
558
|
+
await self._auth_manager.close()
|
|
559
|
+
except Exception as e:
|
|
560
|
+
logger.error(f"Error closing auth manager: {e}")
|
|
561
|
+
|
|
562
|
+
# Clear references
|
|
563
|
+
self._stdio_interface = None
|
|
564
|
+
self._authenticated_client = None
|
|
565
|
+
self._http_client = None
|
|
566
|
+
self._auth_manager = None
|
|
567
|
+
self._translator = None
|
|
568
|
+
|
|
569
|
+
logger.debug("Cleanup completed")
|
|
570
|
+
|
|
571
|
+
# ========================================================================
|
|
572
|
+
# Context Manager Support
|
|
573
|
+
# ========================================================================
|
|
574
|
+
|
|
575
|
+
async def __aenter__(self) -> "McpProxy":
|
|
576
|
+
"""Async context manager entry."""
|
|
577
|
+
await self.start()
|
|
578
|
+
return self
|
|
579
|
+
|
|
580
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
581
|
+
"""Async context manager exit."""
|
|
582
|
+
await self.stop()
|
|
583
|
+
|
|
584
|
+
def __repr__(self) -> str:
|
|
585
|
+
"""String representation of the proxy."""
|
|
586
|
+
return f"McpProxy(running={self._running}, config={self._config is not None})"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Stdio interface module for MCP Proxy.
|
|
2
|
+
|
|
3
|
+
This module handles stdio communication with Kiro using JSON-RPC protocol.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .interface import StdioInterface, DEFAULT_QUEUE_SIZE, SHUTDOWN_TIMEOUT, DRAIN_TIMEOUT
|
|
7
|
+
from .jsonrpc import (
|
|
8
|
+
JsonRpcParser,
|
|
9
|
+
JsonRpcSerializer,
|
|
10
|
+
MessageIdGenerator,
|
|
11
|
+
MessageCorrelationTracker,
|
|
12
|
+
create_request,
|
|
13
|
+
create_response,
|
|
14
|
+
create_notification,
|
|
15
|
+
create_error_response,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"StdioInterface",
|
|
20
|
+
"DEFAULT_QUEUE_SIZE",
|
|
21
|
+
"SHUTDOWN_TIMEOUT",
|
|
22
|
+
"DRAIN_TIMEOUT",
|
|
23
|
+
"JsonRpcParser",
|
|
24
|
+
"JsonRpcSerializer",
|
|
25
|
+
"MessageIdGenerator",
|
|
26
|
+
"MessageCorrelationTracker",
|
|
27
|
+
"create_request",
|
|
28
|
+
"create_response",
|
|
29
|
+
"create_notification",
|
|
30
|
+
"create_error_response",
|
|
31
|
+
]
|