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,8 @@
1
+ """Configuration module for MCP Proxy.
2
+
3
+ This module handles configuration loading and validation.
4
+ """
5
+
6
+ from .manager import ConfigurationManagerImpl
7
+
8
+ __all__ = ["ConfigurationManagerImpl"]
@@ -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
@@ -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"]