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,200 @@
|
|
|
1
|
+
"""Configuration manager implementation for MCP Proxy.
|
|
2
|
+
|
|
3
|
+
This module provides configuration loading from environment variables,
|
|
4
|
+
validation, and default value management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional, Dict, Any
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
|
|
11
|
+
from ..models import ProxyConfig
|
|
12
|
+
from ..exceptions import ConfigurationError, InvalidConfigError, MissingConfigError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigurationManagerImpl:
|
|
16
|
+
"""Implementation of configuration management.
|
|
17
|
+
|
|
18
|
+
Loads configuration from environment variables with fallback to defaults,
|
|
19
|
+
validates all parameters, and provides clear error messages for invalid
|
|
20
|
+
configurations.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Environment variable names
|
|
24
|
+
ENV_MCP_SERVER_URL = "MCP_SERVER_URL"
|
|
25
|
+
ENV_OAUTH_PROVIDER_URL = "OAUTH_PROVIDER_URL"
|
|
26
|
+
ENV_CLIENT_NAME = "MCP_CLIENT_NAME"
|
|
27
|
+
ENV_SCOPES = "MCP_SCOPES"
|
|
28
|
+
ENV_CONNECTION_TIMEOUT = "MCP_CONNECTION_TIMEOUT"
|
|
29
|
+
ENV_RETRY_ATTEMPTS = "MCP_RETRY_ATTEMPTS"
|
|
30
|
+
ENV_LOG_LEVEL = "MCP_LOG_LEVEL"
|
|
31
|
+
ENV_MAX_BACKOFF_SECONDS = "MCP_MAX_BACKOFF_SECONDS"
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
"""Initialize configuration manager."""
|
|
35
|
+
self._config: Optional[ProxyConfig] = None
|
|
36
|
+
|
|
37
|
+
async def load(self) -> ProxyConfig:
|
|
38
|
+
"""Load configuration from environment variables.
|
|
39
|
+
|
|
40
|
+
Loads configuration parameters from environment variables with fallback
|
|
41
|
+
to default values for optional parameters. Required parameters must be
|
|
42
|
+
provided via environment variables.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
ProxyConfig: Validated configuration object
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
MissingConfigError: If required configuration is missing
|
|
49
|
+
InvalidConfigError: If configuration validation fails
|
|
50
|
+
"""
|
|
51
|
+
# Load required parameters
|
|
52
|
+
mcp_server_url = os.getenv(self.ENV_MCP_SERVER_URL)
|
|
53
|
+
if not mcp_server_url:
|
|
54
|
+
raise MissingConfigError(
|
|
55
|
+
self.ENV_MCP_SERVER_URL,
|
|
56
|
+
details="MCP server URL must be provided via environment variable"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
oauth_provider_url = os.getenv(self.ENV_OAUTH_PROVIDER_URL)
|
|
60
|
+
if not oauth_provider_url:
|
|
61
|
+
raise MissingConfigError(
|
|
62
|
+
self.ENV_OAUTH_PROVIDER_URL,
|
|
63
|
+
details="OAuth provider URL must be provided via environment variable"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Load optional parameters with defaults
|
|
67
|
+
config_dict: Dict[str, Any] = {
|
|
68
|
+
"mcp_server_url": mcp_server_url,
|
|
69
|
+
"oauth_provider_url": oauth_provider_url,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Client name (optional)
|
|
73
|
+
client_name = os.getenv(self.ENV_CLIENT_NAME)
|
|
74
|
+
if client_name:
|
|
75
|
+
config_dict["client_name"] = client_name
|
|
76
|
+
|
|
77
|
+
# Scopes (optional, comma-separated)
|
|
78
|
+
scopes_str = os.getenv(self.ENV_SCOPES)
|
|
79
|
+
if scopes_str:
|
|
80
|
+
scopes = [s.strip() for s in scopes_str.split(",") if s.strip()]
|
|
81
|
+
if scopes:
|
|
82
|
+
config_dict["scopes"] = scopes
|
|
83
|
+
|
|
84
|
+
# Connection timeout (optional)
|
|
85
|
+
timeout_str = os.getenv(self.ENV_CONNECTION_TIMEOUT)
|
|
86
|
+
if timeout_str:
|
|
87
|
+
try:
|
|
88
|
+
config_dict["connection_timeout"] = int(timeout_str)
|
|
89
|
+
except ValueError:
|
|
90
|
+
raise InvalidConfigError(
|
|
91
|
+
f"Invalid connection timeout: {timeout_str}. Must be an integer.",
|
|
92
|
+
details={"parameter": self.ENV_CONNECTION_TIMEOUT, "value": timeout_str}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Retry attempts (optional)
|
|
96
|
+
retry_str = os.getenv(self.ENV_RETRY_ATTEMPTS)
|
|
97
|
+
if retry_str:
|
|
98
|
+
try:
|
|
99
|
+
config_dict["retry_attempts"] = int(retry_str)
|
|
100
|
+
except ValueError:
|
|
101
|
+
raise InvalidConfigError(
|
|
102
|
+
f"Invalid retry attempts: {retry_str}. Must be an integer.",
|
|
103
|
+
details={"parameter": self.ENV_RETRY_ATTEMPTS, "value": retry_str}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Log level (optional)
|
|
107
|
+
log_level = os.getenv(self.ENV_LOG_LEVEL)
|
|
108
|
+
if log_level:
|
|
109
|
+
config_dict["log_level"] = log_level
|
|
110
|
+
|
|
111
|
+
# Max backoff seconds (optional)
|
|
112
|
+
backoff_str = os.getenv(self.ENV_MAX_BACKOFF_SECONDS)
|
|
113
|
+
if backoff_str:
|
|
114
|
+
try:
|
|
115
|
+
config_dict["max_backoff_seconds"] = int(backoff_str)
|
|
116
|
+
except ValueError:
|
|
117
|
+
raise InvalidConfigError(
|
|
118
|
+
f"Invalid max backoff seconds: {backoff_str}. Must be an integer.",
|
|
119
|
+
details={"parameter": self.ENV_MAX_BACKOFF_SECONDS, "value": backoff_str}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Validate and create config
|
|
123
|
+
try:
|
|
124
|
+
self._config = ProxyConfig(**config_dict)
|
|
125
|
+
except ValidationError as e:
|
|
126
|
+
# Convert Pydantic validation errors to our custom exception
|
|
127
|
+
error_messages = []
|
|
128
|
+
for error in e.errors():
|
|
129
|
+
field = ".".join(str(loc) for loc in error["loc"])
|
|
130
|
+
msg = error["msg"]
|
|
131
|
+
error_messages.append(f"{field}: {msg}")
|
|
132
|
+
|
|
133
|
+
raise InvalidConfigError(
|
|
134
|
+
f"Configuration validation failed: {'; '.join(error_messages)}",
|
|
135
|
+
details={"validation_errors": e.errors()}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return self._config
|
|
139
|
+
|
|
140
|
+
def validate(self, config: ProxyConfig) -> bool:
|
|
141
|
+
"""Validate a configuration object.
|
|
142
|
+
|
|
143
|
+
Performs validation on a ProxyConfig object to ensure all parameters
|
|
144
|
+
are valid. This method is useful for validating configurations created
|
|
145
|
+
programmatically rather than loaded from environment variables.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
config: Configuration object to validate
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
bool: True if configuration is valid
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
InvalidConfigError: If configuration is invalid
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
# Pydantic models validate on construction, but we can also
|
|
158
|
+
# trigger validation explicitly
|
|
159
|
+
config.model_validate(config.model_dump())
|
|
160
|
+
return True
|
|
161
|
+
except ValidationError as e:
|
|
162
|
+
error_messages = []
|
|
163
|
+
for error in e.errors():
|
|
164
|
+
field = ".".join(str(loc) for loc in error["loc"])
|
|
165
|
+
msg = error["msg"]
|
|
166
|
+
error_messages.append(f"{field}: {msg}")
|
|
167
|
+
|
|
168
|
+
raise InvalidConfigError(
|
|
169
|
+
f"Configuration validation failed: {'; '.join(error_messages)}",
|
|
170
|
+
details={"validation_errors": e.errors()}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def get_defaults(self) -> ProxyConfig:
|
|
174
|
+
"""Get default configuration values.
|
|
175
|
+
|
|
176
|
+
Returns a ProxyConfig object with all default values. This is useful
|
|
177
|
+
for documentation and testing purposes. Note that required fields
|
|
178
|
+
(mcp_server_url and oauth_provider_url) are set to placeholder values.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
ProxyConfig: Configuration with default values
|
|
182
|
+
"""
|
|
183
|
+
return ProxyConfig(
|
|
184
|
+
mcp_server_url="https://example.com/mcp",
|
|
185
|
+
oauth_provider_url="https://oauth.example.com",
|
|
186
|
+
client_name="mcp-proxy-client",
|
|
187
|
+
scopes=["mcp:read", "mcp:write"],
|
|
188
|
+
connection_timeout=30,
|
|
189
|
+
retry_attempts=3,
|
|
190
|
+
log_level="info",
|
|
191
|
+
max_backoff_seconds=60,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def get_current_config(self) -> Optional[ProxyConfig]:
|
|
195
|
+
"""Get the currently loaded configuration.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Optional[ProxyConfig]: Current configuration or None if not loaded
|
|
199
|
+
"""
|
|
200
|
+
return self._config
|
mcp_proxy/exceptions.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Custom exceptions for the MCP Proxy.
|
|
2
|
+
|
|
3
|
+
This module defines the exception hierarchy for error handling
|
|
4
|
+
throughout the proxy system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class McpProxyError(Exception):
|
|
11
|
+
"""Base exception for all MCP Proxy errors."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str, details: Optional[Any] = None):
|
|
14
|
+
"""Initialize exception with message and optional details.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
message: Error message
|
|
18
|
+
details: Additional error details
|
|
19
|
+
"""
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.message = message
|
|
22
|
+
self.details = details
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# Protocol Translation Errors
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ProtocolError(McpProxyError):
|
|
31
|
+
"""Base exception for protocol translation errors."""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InvalidJsonRpcError(ProtocolError):
|
|
36
|
+
"""Raised when JSON-RPC message is invalid."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MessageCorrelationError(ProtocolError):
|
|
41
|
+
"""Raised when message correlation fails."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UnsupportedMethodError(ProtocolError):
|
|
46
|
+
"""Raised when an unsupported MCP method is called."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, method: str, details: Optional[Any] = None):
|
|
49
|
+
super().__init__(f"Unsupported method: {method}", details)
|
|
50
|
+
self.method = method
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ============================================================================
|
|
54
|
+
# Authentication Errors
|
|
55
|
+
# ============================================================================
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AuthenticationError(McpProxyError):
|
|
59
|
+
"""Base exception for authentication errors."""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DcrError(AuthenticationError):
|
|
64
|
+
"""Raised when OAuth Dynamic Client Registration fails."""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TokenError(AuthenticationError):
|
|
69
|
+
"""Raised when token operations fail."""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TokenExpiredError(TokenError):
|
|
74
|
+
"""Raised when access token has expired."""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TokenRefreshError(TokenError):
|
|
79
|
+
"""Raised when token refresh fails."""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class InvalidCredentialsError(AuthenticationError):
|
|
84
|
+
"""Raised when client credentials are invalid."""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ============================================================================
|
|
89
|
+
# Network and Connection Errors
|
|
90
|
+
# ============================================================================
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class NetworkError(McpProxyError):
|
|
94
|
+
"""Base exception for network-related errors."""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ConnectionError(NetworkError):
|
|
99
|
+
"""Raised when connection to backend server fails."""
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ConnectionTimeoutError(NetworkError):
|
|
104
|
+
"""Raised when connection times out."""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class StreamError(NetworkError):
|
|
109
|
+
"""Raised when SSE stream encounters an error."""
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class HttpError(NetworkError):
|
|
114
|
+
"""Raised when HTTP request fails."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, status_code: int, message: str, details: Optional[Any] = None):
|
|
117
|
+
super().__init__(f"HTTP {status_code}: {message}", details)
|
|
118
|
+
self.status_code = status_code
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ============================================================================
|
|
122
|
+
# Configuration Errors
|
|
123
|
+
# ============================================================================
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ConfigurationError(McpProxyError):
|
|
127
|
+
"""Base exception for configuration errors."""
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class InvalidConfigError(ConfigurationError):
|
|
132
|
+
"""Raised when configuration is invalid."""
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class MissingConfigError(ConfigurationError):
|
|
137
|
+
"""Raised when required configuration is missing."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, parameter: str, details: Optional[Any] = None):
|
|
140
|
+
super().__init__(f"Missing required configuration: {parameter}", details)
|
|
141
|
+
self.parameter = parameter
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ============================================================================
|
|
145
|
+
# Utility Functions
|
|
146
|
+
# ============================================================================
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def to_jsonrpc_error(error: Exception, default_code: int = -32603) -> dict:
|
|
150
|
+
"""Convert exception to JSON-RPC error format.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
error: Exception to convert
|
|
154
|
+
default_code: Default error code if not specified
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
JSON-RPC error dictionary
|
|
158
|
+
"""
|
|
159
|
+
# Map exception types to JSON-RPC error codes
|
|
160
|
+
error_code_map = {
|
|
161
|
+
InvalidJsonRpcError: -32700, # Parse error
|
|
162
|
+
UnsupportedMethodError: -32601, # Method not found
|
|
163
|
+
InvalidConfigError: -32602, # Invalid params
|
|
164
|
+
AuthenticationError: -32000, # Server error (custom)
|
|
165
|
+
NetworkError: -32001, # Server error (custom)
|
|
166
|
+
ConnectionError: -32002, # Server error (custom)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Determine error code
|
|
170
|
+
code = default_code
|
|
171
|
+
for exc_type, exc_code in error_code_map.items():
|
|
172
|
+
if isinstance(error, exc_type):
|
|
173
|
+
code = exc_code
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
# Build error object
|
|
177
|
+
error_obj = {
|
|
178
|
+
"code": code,
|
|
179
|
+
"message": str(error),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Add details if available
|
|
183
|
+
if isinstance(error, McpProxyError) and error.details:
|
|
184
|
+
error_obj["data"] = error.details
|
|
185
|
+
|
|
186
|
+
return error_obj
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""HTTP client module for MCP Proxy.
|
|
2
|
+
|
|
3
|
+
This module handles HTTP communication with backend MCP servers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .client import HttpClientImpl
|
|
7
|
+
from .authenticated_client import AuthenticatedHttpClient
|
|
8
|
+
|
|
9
|
+
__all__ = ["HttpClientImpl", "AuthenticatedHttpClient"]
|