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.
@@ -0,0 +1,371 @@
1
+ """JSON-RPC message parser and serializer for stdio communication.
2
+
3
+ This module provides functionality for parsing and serializing JSON-RPC 2.0 messages
4
+ with newline-delimited format for stdio communication, along with message correlation
5
+ and ID generation.
6
+ """
7
+
8
+ import json
9
+ import uuid
10
+ from typing import Optional, Union, Dict, Any
11
+ from datetime import datetime
12
+
13
+ from ..models import JsonRpcMessage, JsonRpcError, MessageCorrelation, MessageStatus
14
+ from ..exceptions import InvalidJsonRpcError, MessageCorrelationError
15
+
16
+
17
+ class JsonRpcParser:
18
+ """Parser for JSON-RPC 2.0 messages with newline-delimited format."""
19
+
20
+ @staticmethod
21
+ def parse(line: str) -> JsonRpcMessage:
22
+ """Parse a newline-delimited JSON-RPC message.
23
+
24
+ Args:
25
+ line: A single line containing a JSON-RPC message
26
+
27
+ Returns:
28
+ Parsed JsonRpcMessage object
29
+
30
+ Raises:
31
+ InvalidJsonRpcError: If the message is invalid or malformed
32
+ """
33
+ if not line or not line.strip():
34
+ raise InvalidJsonRpcError("Empty message line")
35
+
36
+ try:
37
+ # Remove any trailing newline characters
38
+ line = line.rstrip('\n\r')
39
+
40
+ # Parse JSON
41
+ data = json.loads(line)
42
+
43
+ # Validate and create JsonRpcMessage
44
+ return JsonRpcMessage.model_validate(data)
45
+
46
+ except json.JSONDecodeError as e:
47
+ raise InvalidJsonRpcError(
48
+ f"Invalid JSON in message: {e}",
49
+ details={"line": line, "error": str(e)}
50
+ )
51
+ except Exception as e:
52
+ raise InvalidJsonRpcError(
53
+ f"Failed to parse JSON-RPC message: {e}",
54
+ details={"line": line, "error": str(e)}
55
+ )
56
+
57
+ @staticmethod
58
+ def parse_multiple(lines: str) -> list[JsonRpcMessage]:
59
+ """Parse multiple newline-delimited JSON-RPC messages.
60
+
61
+ Args:
62
+ lines: Multiple lines containing JSON-RPC messages
63
+
64
+ Returns:
65
+ List of parsed JsonRpcMessage objects
66
+
67
+ Raises:
68
+ InvalidJsonRpcError: If any message is invalid
69
+ """
70
+ messages = []
71
+ for line in lines.split('\n'):
72
+ if line.strip(): # Skip empty lines
73
+ messages.append(JsonRpcParser.parse(line))
74
+ return messages
75
+
76
+
77
+ class JsonRpcSerializer:
78
+ """Serializer for JSON-RPC 2.0 messages with newline-delimited format."""
79
+
80
+ @staticmethod
81
+ def serialize(message: JsonRpcMessage) -> str:
82
+ """Serialize a JSON-RPC message to newline-delimited format.
83
+
84
+ Args:
85
+ message: JsonRpcMessage to serialize
86
+
87
+ Returns:
88
+ Newline-delimited JSON string
89
+ """
90
+ # Use Pydantic's JSON serialization with exclude_none
91
+ json_str = message.model_dump_json(exclude_none=True)
92
+
93
+ # Add newline delimiter
94
+ return json_str + '\n'
95
+
96
+ @staticmethod
97
+ def serialize_multiple(messages: list[JsonRpcMessage]) -> str:
98
+ """Serialize multiple JSON-RPC messages to newline-delimited format.
99
+
100
+ Args:
101
+ messages: List of JsonRpcMessage objects to serialize
102
+
103
+ Returns:
104
+ Newline-delimited JSON string with multiple messages
105
+ """
106
+ return ''.join(JsonRpcSerializer.serialize(msg) for msg in messages)
107
+
108
+
109
+ class MessageIdGenerator:
110
+ """Generator for unique message IDs."""
111
+
112
+ def __init__(self, prefix: str = "mcp"):
113
+ """Initialize the ID generator.
114
+
115
+ Args:
116
+ prefix: Prefix for generated IDs (default: "mcp")
117
+ """
118
+ self.prefix = prefix
119
+ self._counter = 0
120
+
121
+ def generate(self) -> str:
122
+ """Generate a unique message ID.
123
+
124
+ Returns:
125
+ Unique message ID string
126
+ """
127
+ self._counter += 1
128
+ # Use UUID for uniqueness combined with counter for ordering
129
+ return f"{self.prefix}-{self._counter}-{uuid.uuid4().hex[:8]}"
130
+
131
+ def reset(self) -> None:
132
+ """Reset the counter (useful for testing)."""
133
+ self._counter = 0
134
+
135
+
136
+ class MessageCorrelationTracker:
137
+ """Tracks correlation between stdio and HTTP messages."""
138
+
139
+ def __init__(self):
140
+ """Initialize the correlation tracker."""
141
+ self._correlations: Dict[Union[str, int], MessageCorrelation] = {}
142
+
143
+ def add_correlation(
144
+ self,
145
+ stdio_request_id: Union[str, int],
146
+ http_request_id: str,
147
+ method: str
148
+ ) -> MessageCorrelation:
149
+ """Add a new message correlation.
150
+
151
+ Args:
152
+ stdio_request_id: ID from the stdio JSON-RPC request
153
+ http_request_id: ID for the HTTP request
154
+ method: Method name being called
155
+
156
+ Returns:
157
+ Created MessageCorrelation object
158
+
159
+ Raises:
160
+ MessageCorrelationError: If correlation already exists
161
+ """
162
+ if stdio_request_id in self._correlations:
163
+ raise MessageCorrelationError(
164
+ f"Correlation already exists for stdio request ID: {stdio_request_id}",
165
+ details={"stdio_request_id": stdio_request_id}
166
+ )
167
+
168
+ correlation = MessageCorrelation(
169
+ stdio_request_id=stdio_request_id,
170
+ http_request_id=http_request_id,
171
+ timestamp=datetime.now(),
172
+ method=method,
173
+ status=MessageStatus.PENDING
174
+ )
175
+
176
+ self._correlations[stdio_request_id] = correlation
177
+ return correlation
178
+
179
+ def get_correlation(
180
+ self,
181
+ stdio_request_id: Union[str, int]
182
+ ) -> Optional[MessageCorrelation]:
183
+ """Get correlation by stdio request ID.
184
+
185
+ Args:
186
+ stdio_request_id: ID from the stdio JSON-RPC request
187
+
188
+ Returns:
189
+ MessageCorrelation object if found, None otherwise
190
+ """
191
+ return self._correlations.get(stdio_request_id)
192
+
193
+ def get_correlation_by_http_id(
194
+ self,
195
+ http_request_id: str
196
+ ) -> Optional[MessageCorrelation]:
197
+ """Get correlation by HTTP request ID.
198
+
199
+ Args:
200
+ http_request_id: ID from the HTTP request
201
+
202
+ Returns:
203
+ MessageCorrelation object if found, None otherwise
204
+ """
205
+ for correlation in self._correlations.values():
206
+ if correlation.http_request_id == http_request_id:
207
+ return correlation
208
+ return None
209
+
210
+ def mark_completed(self, stdio_request_id: Union[str, int]) -> None:
211
+ """Mark a correlation as completed.
212
+
213
+ Args:
214
+ stdio_request_id: ID from the stdio JSON-RPC request
215
+
216
+ Raises:
217
+ MessageCorrelationError: If correlation not found
218
+ """
219
+ correlation = self.get_correlation(stdio_request_id)
220
+ if not correlation:
221
+ raise MessageCorrelationError(
222
+ f"No correlation found for stdio request ID: {stdio_request_id}",
223
+ details={"stdio_request_id": stdio_request_id}
224
+ )
225
+ correlation.mark_completed()
226
+
227
+ def mark_failed(self, stdio_request_id: Union[str, int]) -> None:
228
+ """Mark a correlation as failed.
229
+
230
+ Args:
231
+ stdio_request_id: ID from the stdio JSON-RPC request
232
+
233
+ Raises:
234
+ MessageCorrelationError: If correlation not found
235
+ """
236
+ correlation = self.get_correlation(stdio_request_id)
237
+ if not correlation:
238
+ raise MessageCorrelationError(
239
+ f"No correlation found for stdio request ID: {stdio_request_id}",
240
+ details={"stdio_request_id": stdio_request_id}
241
+ )
242
+ correlation.mark_failed()
243
+
244
+ def remove_correlation(self, stdio_request_id: Union[str, int]) -> None:
245
+ """Remove a correlation from tracking.
246
+
247
+ Args:
248
+ stdio_request_id: ID from the stdio JSON-RPC request
249
+ """
250
+ self._correlations.pop(stdio_request_id, None)
251
+
252
+ def get_pending_correlations(self) -> list[MessageCorrelation]:
253
+ """Get all pending correlations.
254
+
255
+ Returns:
256
+ List of pending MessageCorrelation objects
257
+ """
258
+ return [
259
+ corr for corr in self._correlations.values()
260
+ if corr.is_pending()
261
+ ]
262
+
263
+ def clear(self) -> None:
264
+ """Clear all correlations (useful for testing and cleanup)."""
265
+ self._correlations.clear()
266
+
267
+
268
+ def create_request(
269
+ method: str,
270
+ params: Optional[Any] = None,
271
+ request_id: Optional[Union[str, int]] = None
272
+ ) -> JsonRpcMessage:
273
+ """Create a JSON-RPC request message.
274
+
275
+ Args:
276
+ method: Method name to call
277
+ params: Method parameters (optional)
278
+ request_id: Request ID (optional, will be generated if not provided)
279
+
280
+ Returns:
281
+ JsonRpcMessage request object
282
+ """
283
+ if request_id is None:
284
+ # Generate a unique ID
285
+ request_id = MessageIdGenerator().generate()
286
+
287
+ return JsonRpcMessage(
288
+ jsonrpc="2.0",
289
+ id=request_id,
290
+ method=method,
291
+ params=params
292
+ )
293
+
294
+
295
+ def create_response(
296
+ request_id: Union[str, int],
297
+ result: Optional[Any] = None,
298
+ error: Optional[JsonRpcError] = None
299
+ ) -> JsonRpcMessage:
300
+ """Create a JSON-RPC response message.
301
+
302
+ Args:
303
+ request_id: ID from the original request
304
+ result: Result data (for success response)
305
+ error: Error object (for error response)
306
+
307
+ Returns:
308
+ JsonRpcMessage response object
309
+
310
+ Raises:
311
+ ValueError: If both result and error are provided, or neither
312
+ """
313
+ if (result is None and error is None) or (result is not None and error is not None):
314
+ raise ValueError("Must provide either result or error, but not both")
315
+
316
+ return JsonRpcMessage(
317
+ jsonrpc="2.0",
318
+ id=request_id,
319
+ result=result,
320
+ error=error
321
+ )
322
+
323
+
324
+ def create_notification(
325
+ method: str,
326
+ params: Optional[Any] = None
327
+ ) -> JsonRpcMessage:
328
+ """Create a JSON-RPC notification message (request without ID).
329
+
330
+ Args:
331
+ method: Method name to call
332
+ params: Method parameters (optional)
333
+
334
+ Returns:
335
+ JsonRpcMessage notification object
336
+ """
337
+ return JsonRpcMessage(
338
+ jsonrpc="2.0",
339
+ method=method,
340
+ params=params
341
+ )
342
+
343
+
344
+ def create_error_response(
345
+ request_id: Union[str, int],
346
+ code: int,
347
+ message: str,
348
+ data: Optional[Any] = None
349
+ ) -> JsonRpcMessage:
350
+ """Create a JSON-RPC error response message.
351
+
352
+ Args:
353
+ request_id: ID from the original request
354
+ code: Error code
355
+ message: Error message
356
+ data: Additional error data (optional)
357
+
358
+ Returns:
359
+ JsonRpcMessage error response object
360
+ """
361
+ error = JsonRpcError(
362
+ code=code,
363
+ message=message,
364
+ data=data
365
+ )
366
+
367
+ return JsonRpcMessage(
368
+ jsonrpc="2.0",
369
+ id=request_id,
370
+ error=error
371
+ )
@@ -0,0 +1,11 @@
1
+ """Protocol translator module for MCP Proxy.
2
+
3
+ This module handles translation between stdio and HTTP MCP protocols.
4
+ """
5
+
6
+ try:
7
+ from .translator import ProtocolTranslatorImpl
8
+ __all__ = ["ProtocolTranslatorImpl"]
9
+ except ImportError:
10
+ # Module not yet implemented
11
+ __all__ = []