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
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()
|