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 ADDED
@@ -0,0 +1,89 @@
1
+ """MCP Proxy with OAuth DCR Support.
2
+
3
+ A protocol translation service that enables Kiro to connect to HTTP streamable MCP servers
4
+ through a stdio interface while providing OAuth Dynamic Client Registration authentication.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+ __author__ = "MCP Proxy Team"
9
+ __email__ = "team@mcpproxy.dev"
10
+
11
+ from .models import (
12
+ AuthenticationState,
13
+ ClientCredentials,
14
+ HttpMcpRequest,
15
+ HttpMcpResponse,
16
+ JsonRpcError,
17
+ JsonRpcMessage,
18
+ MessageCorrelation,
19
+ MessageStatus,
20
+ OAuthTokenResponse,
21
+ ProxyConfig,
22
+ SessionState,
23
+ )
24
+ from .exceptions import (
25
+ McpProxyError,
26
+ ProtocolError,
27
+ InvalidJsonRpcError,
28
+ MessageCorrelationError,
29
+ UnsupportedMethodError,
30
+ AuthenticationError,
31
+ DcrError,
32
+ TokenError,
33
+ TokenExpiredError,
34
+ TokenRefreshError,
35
+ InvalidCredentialsError,
36
+ NetworkError,
37
+ ConnectionError,
38
+ ConnectionTimeoutError,
39
+ StreamError,
40
+ HttpError,
41
+ ConfigurationError,
42
+ InvalidConfigError,
43
+ MissingConfigError,
44
+ )
45
+ from .logging_config import configure_logging, get_logger
46
+ from .proxy import McpProxy
47
+
48
+ __all__ = [
49
+ "__version__",
50
+ "__author__",
51
+ "__email__",
52
+ # Main proxy class
53
+ "McpProxy",
54
+ # Models
55
+ "AuthenticationState",
56
+ "ClientCredentials",
57
+ "HttpMcpRequest",
58
+ "HttpMcpResponse",
59
+ "JsonRpcError",
60
+ "JsonRpcMessage",
61
+ "MessageCorrelation",
62
+ "MessageStatus",
63
+ "OAuthTokenResponse",
64
+ "ProxyConfig",
65
+ "SessionState",
66
+ # Exceptions
67
+ "McpProxyError",
68
+ "ProtocolError",
69
+ "InvalidJsonRpcError",
70
+ "MessageCorrelationError",
71
+ "UnsupportedMethodError",
72
+ "AuthenticationError",
73
+ "DcrError",
74
+ "TokenError",
75
+ "TokenExpiredError",
76
+ "TokenRefreshError",
77
+ "InvalidCredentialsError",
78
+ "NetworkError",
79
+ "ConnectionError",
80
+ "ConnectionTimeoutError",
81
+ "StreamError",
82
+ "HttpError",
83
+ "ConfigurationError",
84
+ "InvalidConfigError",
85
+ "MissingConfigError",
86
+ # Logging
87
+ "configure_logging",
88
+ "get_logger",
89
+ ]
mcp_proxy/__main__.py ADDED
@@ -0,0 +1,340 @@
1
+ """CLI entry point for MCP Proxy.
2
+
3
+ This module provides the command-line interface for running the MCP Proxy
4
+ as a standalone process with argparse-based configuration options.
5
+
6
+ Requirements satisfied:
7
+ - 7.4: Support environment variable configuration
8
+ - 7.5: Provide default values for optional configuration parameters
9
+ """
10
+
11
+ import argparse
12
+ import asyncio
13
+ import os
14
+ import sys
15
+ from typing import Optional
16
+
17
+ from .proxy import McpProxy
18
+ from .models import ProxyConfig
19
+ from .logging_config import get_logger, configure_logging
20
+ from .exceptions import ConfigurationError
21
+
22
+ # Version information
23
+ __version__ = "0.1.0"
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ def create_parser() -> argparse.ArgumentParser:
29
+ """Create and configure the argument parser.
30
+
31
+ Returns:
32
+ Configured ArgumentParser instance
33
+ """
34
+ parser = argparse.ArgumentParser(
35
+ prog="mcp-proxy",
36
+ description=(
37
+ "MCP Proxy with OAuth Dynamic Client Registration support. "
38
+ "Translates between stdio MCP (for Kiro) and HTTP streamable MCP "
39
+ "(for backend servers) while managing OAuth DCR authentication."
40
+ ),
41
+ epilog=(
42
+ "Configuration can be provided via command-line arguments or "
43
+ "environment variables. Command-line arguments take precedence. "
44
+ "Environment variables: MCP_SERVER_URL, OAUTH_PROVIDER_URL, "
45
+ "MCP_CLIENT_NAME, MCP_SCOPES, MCP_CONNECTION_TIMEOUT, "
46
+ "MCP_RETRY_ATTEMPTS, MCP_LOG_LEVEL, MCP_MAX_BACKOFF_SECONDS"
47
+ ),
48
+ formatter_class=argparse.RawDescriptionHelpFormatter,
49
+ )
50
+
51
+ # Version information
52
+ parser.add_argument(
53
+ "--version",
54
+ action="version",
55
+ version=f"%(prog)s {__version__}",
56
+ help="Show version information and exit",
57
+ )
58
+
59
+ # Required configuration (can also come from environment variables)
60
+ parser.add_argument(
61
+ "--mcp-server-url",
62
+ type=str,
63
+ default=None,
64
+ metavar="URL",
65
+ help=(
66
+ "URL of the HTTP MCP server to connect to "
67
+ "(env: MCP_SERVER_URL, required if not set via environment)"
68
+ ),
69
+ )
70
+
71
+ parser.add_argument(
72
+ "--oauth-provider-url",
73
+ type=str,
74
+ default=None,
75
+ metavar="URL",
76
+ help=(
77
+ "URL of the OAuth provider for DCR "
78
+ "(env: OAUTH_PROVIDER_URL, required if not set via environment)"
79
+ ),
80
+ )
81
+
82
+ # Optional configuration
83
+ parser.add_argument(
84
+ "--client-name",
85
+ type=str,
86
+ default=None,
87
+ metavar="NAME",
88
+ help=(
89
+ "OAuth client name for DCR "
90
+ "(env: MCP_CLIENT_NAME, default: mcp-proxy-client)"
91
+ ),
92
+ )
93
+
94
+ parser.add_argument(
95
+ "--scopes",
96
+ type=str,
97
+ default=None,
98
+ metavar="SCOPE1,SCOPE2",
99
+ help=(
100
+ "Comma-separated list of OAuth scopes "
101
+ "(env: MCP_SCOPES, default: mcp:read,mcp:write)"
102
+ ),
103
+ )
104
+
105
+ parser.add_argument(
106
+ "--connection-timeout",
107
+ type=int,
108
+ default=None,
109
+ metavar="SECONDS",
110
+ help=(
111
+ "HTTP connection timeout in seconds "
112
+ "(env: MCP_CONNECTION_TIMEOUT, default: 30)"
113
+ ),
114
+ )
115
+
116
+ parser.add_argument(
117
+ "--retry-attempts",
118
+ type=int,
119
+ default=None,
120
+ metavar="COUNT",
121
+ help=(
122
+ "Number of retry attempts for failed requests "
123
+ "(env: MCP_RETRY_ATTEMPTS, default: 3)"
124
+ ),
125
+ )
126
+
127
+ parser.add_argument(
128
+ "--max-backoff",
129
+ type=int,
130
+ default=None,
131
+ metavar="SECONDS",
132
+ help=(
133
+ "Maximum backoff time in seconds for retries "
134
+ "(env: MCP_MAX_BACKOFF_SECONDS, default: 60)"
135
+ ),
136
+ )
137
+
138
+ parser.add_argument(
139
+ "--log-level",
140
+ type=str,
141
+ choices=["debug", "info", "warn", "error"],
142
+ default=None,
143
+ metavar="LEVEL",
144
+ help=(
145
+ "Logging level (debug, info, warn, error) "
146
+ "(env: MCP_LOG_LEVEL, default: info)"
147
+ ),
148
+ )
149
+
150
+ # Operational flags
151
+ parser.add_argument(
152
+ "--validate-config",
153
+ action="store_true",
154
+ help="Validate configuration and exit without starting the proxy",
155
+ )
156
+
157
+ parser.add_argument(
158
+ "--show-config",
159
+ action="store_true",
160
+ help="Show the effective configuration and exit",
161
+ )
162
+
163
+ return parser
164
+
165
+
166
+ def load_config_from_args(args: argparse.Namespace) -> Optional[ProxyConfig]:
167
+ """Load configuration from command-line arguments and environment variables.
168
+
169
+ Command-line arguments take precedence over environment variables.
170
+ If neither is provided for required parameters, returns None to trigger
171
+ the default environment-based loading in ConfigurationManager.
172
+
173
+ Args:
174
+ args: Parsed command-line arguments
175
+
176
+ Returns:
177
+ ProxyConfig if sufficient configuration is provided, None otherwise
178
+
179
+ Raises:
180
+ ConfigurationError: If configuration is invalid
181
+ """
182
+ # Get MCP server URL (CLI arg or env var)
183
+ mcp_server_url = args.mcp_server_url or os.getenv("MCP_SERVER_URL")
184
+
185
+ # Get OAuth provider URL (CLI arg or env var)
186
+ oauth_provider_url = args.oauth_provider_url or os.getenv("OAUTH_PROVIDER_URL")
187
+
188
+ # If required parameters are missing, return None to use default loading
189
+ if not mcp_server_url or not oauth_provider_url:
190
+ return None
191
+
192
+ # Build configuration dictionary
193
+ config_dict = {
194
+ "mcp_server_url": mcp_server_url,
195
+ "oauth_provider_url": oauth_provider_url,
196
+ }
197
+
198
+ # Optional parameters (CLI args take precedence over env vars)
199
+ if args.client_name:
200
+ config_dict["client_name"] = args.client_name
201
+ elif os.getenv("MCP_CLIENT_NAME"):
202
+ config_dict["client_name"] = os.getenv("MCP_CLIENT_NAME")
203
+
204
+ if args.scopes:
205
+ config_dict["scopes"] = [s.strip() for s in args.scopes.split(",") if s.strip()]
206
+ elif os.getenv("MCP_SCOPES"):
207
+ scopes_str = os.getenv("MCP_SCOPES")
208
+ config_dict["scopes"] = [s.strip() for s in scopes_str.split(",") if s.strip()]
209
+
210
+ if args.connection_timeout is not None:
211
+ config_dict["connection_timeout"] = args.connection_timeout
212
+ elif os.getenv("MCP_CONNECTION_TIMEOUT"):
213
+ try:
214
+ config_dict["connection_timeout"] = int(os.getenv("MCP_CONNECTION_TIMEOUT"))
215
+ except ValueError:
216
+ raise ConfigurationError(
217
+ f"Invalid MCP_CONNECTION_TIMEOUT: {os.getenv('MCP_CONNECTION_TIMEOUT')}"
218
+ )
219
+
220
+ if args.retry_attempts is not None:
221
+ config_dict["retry_attempts"] = args.retry_attempts
222
+ elif os.getenv("MCP_RETRY_ATTEMPTS"):
223
+ try:
224
+ config_dict["retry_attempts"] = int(os.getenv("MCP_RETRY_ATTEMPTS"))
225
+ except ValueError:
226
+ raise ConfigurationError(
227
+ f"Invalid MCP_RETRY_ATTEMPTS: {os.getenv('MCP_RETRY_ATTEMPTS')}"
228
+ )
229
+
230
+ if args.max_backoff is not None:
231
+ config_dict["max_backoff_seconds"] = args.max_backoff
232
+ elif os.getenv("MCP_MAX_BACKOFF_SECONDS"):
233
+ try:
234
+ config_dict["max_backoff_seconds"] = int(os.getenv("MCP_MAX_BACKOFF_SECONDS"))
235
+ except ValueError:
236
+ raise ConfigurationError(
237
+ f"Invalid MCP_MAX_BACKOFF_SECONDS: {os.getenv('MCP_MAX_BACKOFF_SECONDS')}"
238
+ )
239
+
240
+ if args.log_level:
241
+ config_dict["log_level"] = args.log_level
242
+ elif os.getenv("MCP_LOG_LEVEL"):
243
+ config_dict["log_level"] = os.getenv("MCP_LOG_LEVEL")
244
+
245
+ try:
246
+ return ProxyConfig(**config_dict)
247
+ except Exception as e:
248
+ raise ConfigurationError(f"Invalid configuration: {e}")
249
+
250
+
251
+ def show_config(config: ProxyConfig) -> None:
252
+ """Display the effective configuration.
253
+
254
+ Args:
255
+ config: Configuration to display
256
+ """
257
+ print("MCP Proxy Configuration")
258
+ print("=" * 60)
259
+ print(f"MCP Server URL: {config.mcp_server_url}")
260
+ print(f"OAuth Provider URL: {config.oauth_provider_url}")
261
+ print(f"Client Name: {config.client_name}")
262
+ print(f"Scopes: {', '.join(config.scopes)}")
263
+ print(f"Connection Timeout: {config.connection_timeout}s")
264
+ print(f"Retry Attempts: {config.retry_attempts}")
265
+ print(f"Max Backoff: {config.max_backoff_seconds}s")
266
+ print(f"Log Level: {config.log_level}")
267
+ print("=" * 60)
268
+
269
+
270
+ async def main() -> int:
271
+ """Main entry point for the MCP Proxy CLI.
272
+
273
+ Returns:
274
+ Exit code (0 for success, non-zero for error)
275
+ """
276
+ # Parse command-line arguments
277
+ parser = create_parser()
278
+ args = parser.parse_args()
279
+
280
+ # Set up logging early if specified
281
+ if args.log_level:
282
+ configure_logging(args.log_level)
283
+
284
+ try:
285
+ # Load configuration from CLI args and environment
286
+ config = load_config_from_args(args)
287
+
288
+ # Handle --validate-config flag
289
+ if args.validate_config:
290
+ if config is None:
291
+ # Try to load from environment using ConfigurationManager
292
+ from .config.manager import ConfigurationManagerImpl
293
+ config_manager = ConfigurationManagerImpl()
294
+ config = await config_manager.load()
295
+
296
+ print("Configuration is valid ✓")
297
+ show_config(config)
298
+ return 0
299
+
300
+ # Handle --show-config flag
301
+ if args.show_config:
302
+ if config is None:
303
+ # Try to load from environment using ConfigurationManager
304
+ from .config.manager import ConfigurationManagerImpl
305
+ config_manager = ConfigurationManagerImpl()
306
+ config = await config_manager.load()
307
+
308
+ show_config(config)
309
+ return 0
310
+
311
+ # Create and run the proxy
312
+ logger.info("Starting MCP Proxy")
313
+ proxy = McpProxy(config=config)
314
+ await proxy.run()
315
+ return 0
316
+
317
+ except KeyboardInterrupt:
318
+ logger.info("Received keyboard interrupt, shutting down")
319
+ return 0
320
+
321
+ except ConfigurationError as e:
322
+ logger.error(f"Configuration error: {e}")
323
+ print(f"\nError: {e}", file=sys.stderr)
324
+ print("\nUse --help for usage information", file=sys.stderr)
325
+ return 1
326
+
327
+ except Exception as e:
328
+ logger.error(f"Fatal error: {e}", exc_info=True)
329
+ print(f"\nFatal error: {e}", file=sys.stderr)
330
+ return 1
331
+
332
+
333
+ def cli_main() -> None:
334
+ """CLI entry point wrapper for setuptools."""
335
+ exit_code = asyncio.run(main())
336
+ sys.exit(exit_code)
337
+
338
+
339
+ if __name__ == "__main__":
340
+ cli_main()
@@ -0,0 +1,8 @@
1
+ """Authentication module for MCP Proxy.
2
+
3
+ This module handles OAuth DCR and token management.
4
+ """
5
+
6
+ from .manager import AuthenticationManagerImpl
7
+
8
+ __all__ = ["AuthenticationManagerImpl"]