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,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__ = []
|