mcp-proxy-adapter 6.0.0__py3-none-any.whl → 6.0.1__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_adapter/__main__.py +27 -7
- mcp_proxy_adapter/api/app.py +209 -79
- mcp_proxy_adapter/api/handlers.py +16 -5
- mcp_proxy_adapter/api/middleware/__init__.py +14 -9
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
- mcp_proxy_adapter/api/middleware/factory.py +36 -12
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +84 -18
- mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
- mcp_proxy_adapter/commands/__init__.py +7 -1
- mcp_proxy_adapter/commands/base.py +7 -4
- mcp_proxy_adapter/commands/builtin_commands.py +8 -2
- mcp_proxy_adapter/commands/command_registry.py +8 -0
- mcp_proxy_adapter/commands/echo_command.py +81 -0
- mcp_proxy_adapter/commands/health_command.py +1 -1
- mcp_proxy_adapter/commands/help_command.py +21 -14
- mcp_proxy_adapter/commands/proxy_registration_command.py +326 -185
- mcp_proxy_adapter/commands/role_test_command.py +141 -0
- mcp_proxy_adapter/commands/security_command.py +488 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +234 -351
- mcp_proxy_adapter/commands/token_management_command.py +1 -1
- mcp_proxy_adapter/config.py +323 -40
- mcp_proxy_adapter/core/app_factory.py +410 -0
- mcp_proxy_adapter/core/app_runner.py +272 -0
- mcp_proxy_adapter/core/certificate_utils.py +291 -73
- mcp_proxy_adapter/core/client.py +574 -0
- mcp_proxy_adapter/core/client_manager.py +284 -0
- mcp_proxy_adapter/core/client_security.py +384 -0
- mcp_proxy_adapter/core/logging.py +8 -3
- mcp_proxy_adapter/core/mtls_asgi.py +156 -0
- mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
- mcp_proxy_adapter/core/protocol_manager.py +169 -10
- mcp_proxy_adapter/core/proxy_client.py +602 -0
- mcp_proxy_adapter/core/proxy_registration.py +299 -47
- mcp_proxy_adapter/core/security_adapter.py +12 -15
- mcp_proxy_adapter/core/security_integration.py +286 -0
- mcp_proxy_adapter/core/server_adapter.py +282 -0
- mcp_proxy_adapter/core/server_engine.py +270 -0
- mcp_proxy_adapter/core/ssl_utils.py +13 -12
- mcp_proxy_adapter/core/transport_manager.py +5 -5
- mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
- mcp_proxy_adapter/examples/__init__.py +13 -4
- mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/commands/__init__.py +5 -0
- mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
- mcp_proxy_adapter/examples/debug_request_state.py +112 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
- mcp_proxy_adapter/examples/demo_client.py +275 -0
- mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
- mcp_proxy_adapter/examples/generate_certificates.py +177 -0
- mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
- mcp_proxy_adapter/examples/run_example.py +59 -0
- mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
- mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
- mcp_proxy_adapter/examples/run_security_tests.py +544 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
- mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
- mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
- mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/security_test_client.py +782 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
- mcp_proxy_adapter/examples/test_config.py +148 -0
- mcp_proxy_adapter/examples/test_config_generator.py +86 -0
- mcp_proxy_adapter/examples/test_examples.py +281 -0
- mcp_proxy_adapter/examples/universal_client.py +620 -0
- mcp_proxy_adapter/main.py +66 -148
- mcp_proxy_adapter/utils/config_generator.py +1008 -0
- mcp_proxy_adapter/version.py +5 -2
- mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
- mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
- mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
- mcp_proxy_adapter/api/middleware/auth.py +0 -146
- mcp_proxy_adapter/api/middleware/auth_adapter.py +0 -235
- mcp_proxy_adapter/api/middleware/mtls_adapter.py +0 -305
- mcp_proxy_adapter/api/middleware/mtls_middleware.py +0 -296
- mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
- mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +0 -241
- mcp_proxy_adapter/api/middleware/roles_adapter.py +0 -365
- mcp_proxy_adapter/api/middleware/roles_middleware.py +0 -381
- mcp_proxy_adapter/api/middleware/security.py +0 -376
- mcp_proxy_adapter/api/middleware/token_auth_middleware.py +0 -261
- mcp_proxy_adapter/examples/README.md +0 -124
- mcp_proxy_adapter/examples/basic_server/README.md +0 -60
- mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
- mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
- mcp_proxy_adapter/examples/basic_server/config.json +0 -70
- mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +0 -54
- mcp_proxy_adapter/examples/basic_server/config_http.json +0 -70
- mcp_proxy_adapter/examples/basic_server/config_http_only.json +0 -52
- mcp_proxy_adapter/examples/basic_server/config_https.json +0 -58
- mcp_proxy_adapter/examples/basic_server/config_mtls.json +0 -58
- mcp_proxy_adapter/examples/basic_server/config_ssl.json +0 -46
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
- mcp_proxy_adapter/examples/basic_server/server.py +0 -114
- mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
- mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -566
- mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
- mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +0 -105
- mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +0 -129
- mcp_proxy_adapter/examples/custom_commands/config.json +0 -118
- mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +0 -46
- mcp_proxy_adapter/examples/custom_commands/config_https_only.json +0 -46
- mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +0 -33
- mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +0 -46
- mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +0 -33
- mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +0 -33
- mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
- mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
- mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
- mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
- mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
- mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
- mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
- mcp_proxy_adapter/examples/custom_commands/full_help_response.json +0 -1
- mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +0 -629
- mcp_proxy_adapter/examples/custom_commands/get_openapi.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
- mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +0 -129
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +0 -278
- mcp_proxy_adapter/examples/custom_commands/server.py +0 -252
- mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +0 -75
- mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +0 -299
- mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +0 -278
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
- mcp_proxy_adapter/examples/custom_commands/test_openapi.py +0 -27
- mcp_proxy_adapter/examples/custom_commands/test_registry.py +0 -23
- mcp_proxy_adapter/examples/custom_commands/test_simple.py +0 -19
- mcp_proxy_adapter/examples/custom_project_example/README.md +0 -103
- mcp_proxy_adapter/examples/custom_project_example/README_EN.md +0 -103
- mcp_proxy_adapter/examples/deployment/README.md +0 -49
- mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
- mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
- mcp_proxy_adapter/examples/deployment/config.json +0 -29
- mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
- mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
- mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
- mcp_proxy_adapter/examples/deployment/run.sh +0 -43
- mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
- mcp_proxy_adapter/examples/simple_custom_commands/README.md +0 -149
- mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +0 -149
- mcp_proxy_adapter/schemas/base_schema.json +0 -114
- mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
- mcp_proxy_adapter/schemas/roles_schema.json +0 -162
- mcp_proxy_adapter/tests/__init__.py +0 -0
- mcp_proxy_adapter/tests/api/__init__.py +0 -3
- mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
- mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
- mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
- mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
- mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
- mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
- mcp_proxy_adapter/tests/commands/__init__.py +0 -3
- mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
- mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
- mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
- mcp_proxy_adapter/tests/conftest.py +0 -131
- mcp_proxy_adapter/tests/functional/__init__.py +0 -3
- mcp_proxy_adapter/tests/functional/test_api.py +0 -253
- mcp_proxy_adapter/tests/integration/__init__.py +0 -3
- mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
- mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
- mcp_proxy_adapter/tests/performance/__init__.py +0 -3
- mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
- mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
- mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
- mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
- mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
- mcp_proxy_adapter/tests/test_base_command.py +0 -123
- mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
- mcp_proxy_adapter/tests/test_command_registry.py +0 -281
- mcp_proxy_adapter/tests/test_config.py +0 -127
- mcp_proxy_adapter/tests/test_utils.py +0 -65
- mcp_proxy_adapter/tests/unit/__init__.py +0 -3
- mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
- mcp_proxy_adapter/tests/unit/test_config.py +0 -270
- mcp_proxy_adapter-6.0.0.dist-info/METADATA +0 -201
- mcp_proxy_adapter-6.0.0.dist-info/RECORD +0 -179
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,574 @@
|
|
1
|
+
"""
|
2
|
+
Universal Client for MCP Proxy Adapter Framework
|
3
|
+
|
4
|
+
This module provides a universal client that can connect to MCP Proxy Adapter servers
|
5
|
+
using various authentication methods and protocols. It's designed to be used for
|
6
|
+
proxy registration and general API communication.
|
7
|
+
|
8
|
+
Author: Vasiliy Zdanovskiy
|
9
|
+
email: vasilyvz@gmail.com
|
10
|
+
"""
|
11
|
+
|
12
|
+
import asyncio
|
13
|
+
import json
|
14
|
+
import os
|
15
|
+
import ssl
|
16
|
+
import time
|
17
|
+
from typing import Dict, Any, Optional, List, Union
|
18
|
+
from urllib.parse import urljoin
|
19
|
+
from pathlib import Path
|
20
|
+
|
21
|
+
import aiohttp
|
22
|
+
import requests
|
23
|
+
from requests.exceptions import RequestException
|
24
|
+
|
25
|
+
# Import security framework components
|
26
|
+
try:
|
27
|
+
from mcp_security_framework import SecurityManager, AuthManager, CertificateManager
|
28
|
+
from mcp_security_framework.utils import generate_api_key, create_jwt_token, validate_jwt_token
|
29
|
+
from mcp_security_framework.utils import extract_roles_from_cert, validate_certificate_chain
|
30
|
+
from mcp_security_framework.utils import create_ssl_context, validate_server_certificate
|
31
|
+
SECURITY_FRAMEWORK_AVAILABLE = True
|
32
|
+
except ImportError:
|
33
|
+
SECURITY_FRAMEWORK_AVAILABLE = False
|
34
|
+
|
35
|
+
|
36
|
+
class UniversalClient:
|
37
|
+
"""
|
38
|
+
Universal client that demonstrates all possible secure connection methods.
|
39
|
+
|
40
|
+
Supports:
|
41
|
+
- HTTP/HTTPS connections
|
42
|
+
- API Key authentication
|
43
|
+
- JWT token authentication
|
44
|
+
- Certificate-based authentication
|
45
|
+
- SSL/TLS with custom certificates
|
46
|
+
- Role-based access control
|
47
|
+
- Rate limiting awareness
|
48
|
+
"""
|
49
|
+
|
50
|
+
def __init__(self, config: Dict[str, Any]):
|
51
|
+
"""
|
52
|
+
Initialize universal client with configuration.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
config: Client configuration with security settings
|
56
|
+
"""
|
57
|
+
self.config = config
|
58
|
+
self.base_url = config.get("server_url", "http://localhost:8000")
|
59
|
+
self.timeout = config.get("timeout", 30)
|
60
|
+
self.retry_attempts = config.get("retry_attempts", 3)
|
61
|
+
self.retry_delay = config.get("retry_delay", 1)
|
62
|
+
|
63
|
+
# Security configuration
|
64
|
+
self.security_config = config.get("security", {})
|
65
|
+
self.auth_method = self.security_config.get("auth_method", "none")
|
66
|
+
|
67
|
+
# Initialize security managers if framework is available
|
68
|
+
self.security_manager = None
|
69
|
+
self.auth_manager = None
|
70
|
+
self.cert_manager = None
|
71
|
+
|
72
|
+
if SECURITY_FRAMEWORK_AVAILABLE:
|
73
|
+
self._initialize_security_managers()
|
74
|
+
|
75
|
+
# Session management
|
76
|
+
self.session: Optional[aiohttp.ClientSession] = None
|
77
|
+
self.current_token: Optional[str] = None
|
78
|
+
self.token_expiry: Optional[float] = None
|
79
|
+
|
80
|
+
print(f"Universal client initialized with auth method: {self.auth_method}")
|
81
|
+
|
82
|
+
def _initialize_security_managers(self) -> None:
|
83
|
+
"""Initialize security framework managers."""
|
84
|
+
try:
|
85
|
+
# Initialize security manager
|
86
|
+
self.security_manager = SecurityManager(self.security_config)
|
87
|
+
|
88
|
+
# Initialize auth manager
|
89
|
+
auth_config = self.security_config.get("auth", {})
|
90
|
+
self.auth_manager = AuthManager(auth_config)
|
91
|
+
|
92
|
+
# Initialize certificate manager
|
93
|
+
cert_config = self.security_config.get("certificates", {})
|
94
|
+
self.cert_manager = CertificateManager(cert_config)
|
95
|
+
|
96
|
+
print("Security framework managers initialized successfully")
|
97
|
+
except Exception as e:
|
98
|
+
print(f"Warning: Failed to initialize security managers: {e}")
|
99
|
+
|
100
|
+
async def __aenter__(self):
|
101
|
+
"""Async context manager entry."""
|
102
|
+
await self.connect()
|
103
|
+
return self
|
104
|
+
|
105
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
106
|
+
"""Async context manager exit."""
|
107
|
+
await self.disconnect()
|
108
|
+
|
109
|
+
async def connect(self) -> None:
|
110
|
+
"""Establish connection with authentication."""
|
111
|
+
print(f"Connecting to {self.base_url} with {self.auth_method} authentication...")
|
112
|
+
|
113
|
+
# Create SSL context
|
114
|
+
ssl_context = self._create_ssl_context()
|
115
|
+
|
116
|
+
# Create connector with SSL context
|
117
|
+
connector = None
|
118
|
+
if ssl_context:
|
119
|
+
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
120
|
+
|
121
|
+
# Create session
|
122
|
+
self.session = aiohttp.ClientSession(connector=connector)
|
123
|
+
|
124
|
+
# Perform authentication based on method
|
125
|
+
if self.auth_method == "api_key":
|
126
|
+
await self._authenticate_api_key()
|
127
|
+
elif self.auth_method == "jwt":
|
128
|
+
await self._authenticate_jwt()
|
129
|
+
elif self.auth_method == "certificate":
|
130
|
+
await self._authenticate_certificate()
|
131
|
+
elif self.auth_method == "basic":
|
132
|
+
await self._authenticate_basic()
|
133
|
+
else:
|
134
|
+
print("No authentication required")
|
135
|
+
|
136
|
+
print("Connection established successfully")
|
137
|
+
|
138
|
+
async def disconnect(self) -> None:
|
139
|
+
"""Close connection and cleanup."""
|
140
|
+
if self.session:
|
141
|
+
await self.session.close()
|
142
|
+
self.session = None
|
143
|
+
print("Connection closed")
|
144
|
+
|
145
|
+
async def _authenticate_api_key(self) -> None:
|
146
|
+
"""Authenticate using API key."""
|
147
|
+
api_key_config = self.security_config.get("api_key", {})
|
148
|
+
api_key = api_key_config.get("key")
|
149
|
+
|
150
|
+
if not api_key:
|
151
|
+
raise ValueError("API key not provided in configuration")
|
152
|
+
|
153
|
+
# Store API key for requests
|
154
|
+
self.current_token = api_key
|
155
|
+
print(f"Authenticated with API key: {api_key[:8]}...")
|
156
|
+
|
157
|
+
async def _authenticate_jwt(self) -> None:
|
158
|
+
"""Authenticate using JWT token."""
|
159
|
+
jwt_config = self.security_config.get("jwt", {})
|
160
|
+
|
161
|
+
# Check if we have a stored token that's still valid
|
162
|
+
if self.current_token and self.token_expiry and time.time() < self.token_expiry:
|
163
|
+
print("Using existing JWT token")
|
164
|
+
return
|
165
|
+
|
166
|
+
# Get credentials for JWT
|
167
|
+
username = jwt_config.get("username")
|
168
|
+
password = jwt_config.get("password")
|
169
|
+
secret = jwt_config.get("secret")
|
170
|
+
|
171
|
+
if not all([username, password, secret]):
|
172
|
+
raise ValueError("JWT credentials not provided in configuration")
|
173
|
+
|
174
|
+
# Create JWT token
|
175
|
+
if SECURITY_FRAMEWORK_AVAILABLE:
|
176
|
+
self.current_token = create_jwt_token(
|
177
|
+
username,
|
178
|
+
secret,
|
179
|
+
expiry_hours=jwt_config.get("expiry_hours", 24)
|
180
|
+
)
|
181
|
+
else:
|
182
|
+
# Simple JWT creation (for demonstration)
|
183
|
+
import jwt
|
184
|
+
payload = {
|
185
|
+
"username": username,
|
186
|
+
"exp": time.time() + (jwt_config.get("expiry_hours", 24) * 3600)
|
187
|
+
}
|
188
|
+
self.current_token = jwt.encode(payload, secret, algorithm="HS256")
|
189
|
+
|
190
|
+
self.token_expiry = time.time() + (jwt_config.get("expiry_hours", 24) * 3600)
|
191
|
+
print(f"Authenticated with JWT token: {self.current_token[:20]}...")
|
192
|
+
|
193
|
+
async def _authenticate_certificate(self) -> None:
|
194
|
+
"""Authenticate using client certificate."""
|
195
|
+
cert_config = self.security_config.get("certificate", {})
|
196
|
+
|
197
|
+
cert_file = cert_config.get("cert_file")
|
198
|
+
key_file = cert_config.get("key_file")
|
199
|
+
|
200
|
+
if not cert_file or not key_file:
|
201
|
+
raise ValueError("Certificate files not provided in configuration")
|
202
|
+
|
203
|
+
# Validate certificate
|
204
|
+
if SECURITY_FRAMEWORK_AVAILABLE and self.cert_manager:
|
205
|
+
try:
|
206
|
+
cert_info = self.cert_manager.validate_certificate(cert_file, key_file)
|
207
|
+
print(f"Certificate validated: {cert_info.get('subject', 'Unknown')}")
|
208
|
+
|
209
|
+
# Extract roles from certificate
|
210
|
+
roles = extract_roles_from_cert(cert_file)
|
211
|
+
if roles:
|
212
|
+
print(f"Certificate roles: {roles}")
|
213
|
+
except Exception as e:
|
214
|
+
print(f"Warning: Certificate validation failed: {e}")
|
215
|
+
|
216
|
+
print("Certificate authentication prepared")
|
217
|
+
|
218
|
+
async def _authenticate_basic(self) -> None:
|
219
|
+
"""Authenticate using basic authentication."""
|
220
|
+
basic_config = self.security_config.get("basic", {})
|
221
|
+
username = basic_config.get("username")
|
222
|
+
password = basic_config.get("password")
|
223
|
+
|
224
|
+
if not username or not password:
|
225
|
+
raise ValueError("Basic auth credentials not provided in configuration")
|
226
|
+
|
227
|
+
import base64
|
228
|
+
credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
|
229
|
+
self.current_token = f"Basic {credentials}"
|
230
|
+
print(f"Authenticated with basic auth: {username}")
|
231
|
+
|
232
|
+
def _get_auth_headers(self) -> Dict[str, str]:
|
233
|
+
"""Get authentication headers for requests."""
|
234
|
+
headers = {"Content-Type": "application/json"}
|
235
|
+
|
236
|
+
if not self.current_token:
|
237
|
+
return headers
|
238
|
+
|
239
|
+
if self.auth_method == "api_key":
|
240
|
+
api_key_config = self.security_config.get("api_key", {})
|
241
|
+
header_name = api_key_config.get("header", "X-API-Key")
|
242
|
+
headers[header_name] = self.current_token
|
243
|
+
elif self.auth_method == "jwt":
|
244
|
+
headers["Authorization"] = f"Bearer {self.current_token}"
|
245
|
+
elif self.auth_method == "basic":
|
246
|
+
headers["Authorization"] = self.current_token
|
247
|
+
|
248
|
+
return headers
|
249
|
+
|
250
|
+
def _create_ssl_context(self) -> Optional[ssl.SSLContext]:
|
251
|
+
"""Create SSL context for secure connections."""
|
252
|
+
ssl_config = self.security_config.get("ssl", {})
|
253
|
+
if not ssl_config.get("enabled", False):
|
254
|
+
return None
|
255
|
+
|
256
|
+
try:
|
257
|
+
context: Optional[ssl.SSLContext] = None
|
258
|
+
|
259
|
+
# Try security framework first
|
260
|
+
if self.security_manager:
|
261
|
+
try:
|
262
|
+
context = self.security_manager.create_client_ssl_context()
|
263
|
+
except Exception:
|
264
|
+
context = None
|
265
|
+
|
266
|
+
# Fallback SSL context creation
|
267
|
+
if context is None:
|
268
|
+
context = ssl.create_default_context()
|
269
|
+
|
270
|
+
# Always honor explicit client certificate config for mTLS
|
271
|
+
cert_config = self.security_config.get("certificate", {})
|
272
|
+
if cert_config.get("enabled", False):
|
273
|
+
cert_file = cert_config.get("cert_file")
|
274
|
+
key_file = cert_config.get("key_file")
|
275
|
+
if cert_file and key_file:
|
276
|
+
context.load_cert_chain(certfile=cert_file, keyfile=key_file)
|
277
|
+
|
278
|
+
# Add CA certificate if provided
|
279
|
+
ca_cert_file = ssl_config.get("ca_cert_file") or ssl_config.get("ca_cert")
|
280
|
+
if ca_cert_file and os.path.exists(ca_cert_file):
|
281
|
+
context.load_verify_locations(cafile=ca_cert_file)
|
282
|
+
|
283
|
+
# Configure verification
|
284
|
+
if ssl_config.get("check_hostname", True):
|
285
|
+
context.check_hostname = True
|
286
|
+
context.verify_mode = ssl.CERT_REQUIRED
|
287
|
+
else:
|
288
|
+
context.check_hostname = False
|
289
|
+
context.verify_mode = ssl.CERT_NONE
|
290
|
+
|
291
|
+
return context
|
292
|
+
except Exception as e:
|
293
|
+
print(f"Warning: Failed to create SSL context: {e}")
|
294
|
+
return None
|
295
|
+
|
296
|
+
async def request(
|
297
|
+
self,
|
298
|
+
method: str,
|
299
|
+
endpoint: str,
|
300
|
+
data: Optional[Dict[str, Any]] = None,
|
301
|
+
headers: Optional[Dict[str, str]] = None
|
302
|
+
) -> Dict[str, Any]:
|
303
|
+
"""
|
304
|
+
Make authenticated request to server.
|
305
|
+
|
306
|
+
Args:
|
307
|
+
method: HTTP method (GET, POST, etc.)
|
308
|
+
endpoint: API endpoint
|
309
|
+
data: Request data
|
310
|
+
headers: Additional headers
|
311
|
+
|
312
|
+
Returns:
|
313
|
+
Response data
|
314
|
+
"""
|
315
|
+
url = urljoin(self.base_url, endpoint)
|
316
|
+
|
317
|
+
# Prepare headers
|
318
|
+
request_headers = self._get_auth_headers()
|
319
|
+
if headers:
|
320
|
+
request_headers.update(headers)
|
321
|
+
|
322
|
+
try:
|
323
|
+
for attempt in range(self.retry_attempts):
|
324
|
+
try:
|
325
|
+
async with self.session.request(
|
326
|
+
method,
|
327
|
+
url,
|
328
|
+
json=data,
|
329
|
+
headers=request_headers,
|
330
|
+
timeout=aiohttp.ClientTimeout(total=self.timeout)
|
331
|
+
) as response:
|
332
|
+
result = await response.json()
|
333
|
+
|
334
|
+
# Validate response if security framework available
|
335
|
+
if SECURITY_FRAMEWORK_AVAILABLE and self.security_manager:
|
336
|
+
self.security_manager.validate_server_response(dict(response.headers))
|
337
|
+
|
338
|
+
if response.status >= 400:
|
339
|
+
print(f"Request failed with status {response.status}: {result}")
|
340
|
+
return {"error": result, "status": response.status}
|
341
|
+
|
342
|
+
return result
|
343
|
+
|
344
|
+
except Exception as e:
|
345
|
+
print(f"Request attempt {attempt + 1} failed: {e}")
|
346
|
+
if attempt < self.retry_attempts - 1:
|
347
|
+
await asyncio.sleep(self.retry_delay)
|
348
|
+
else:
|
349
|
+
raise
|
350
|
+
except Exception as e:
|
351
|
+
print(f"Request failed: {e}")
|
352
|
+
raise
|
353
|
+
|
354
|
+
async def get(self, endpoint: str, **kwargs) -> Dict[str, Any]:
|
355
|
+
"""Make GET request."""
|
356
|
+
return await self.request("GET", endpoint, **kwargs)
|
357
|
+
|
358
|
+
async def post(self, endpoint: str, data: Dict[str, Any], **kwargs) -> Dict[str, Any]:
|
359
|
+
"""Make POST request."""
|
360
|
+
return await self.request("POST", endpoint, data=data, **kwargs)
|
361
|
+
|
362
|
+
async def put(self, endpoint: str, data: Dict[str, Any], **kwargs) -> Dict[str, Any]:
|
363
|
+
"""Make PUT request."""
|
364
|
+
return await self.request("PUT", endpoint, data=data, **kwargs)
|
365
|
+
|
366
|
+
async def delete(self, endpoint: str, **kwargs) -> Dict[str, Any]:
|
367
|
+
"""Make DELETE request."""
|
368
|
+
return await self.request("DELETE", endpoint, **kwargs)
|
369
|
+
|
370
|
+
async def test_connection(self) -> bool:
|
371
|
+
"""Test connection to server."""
|
372
|
+
try:
|
373
|
+
result = await self.get("/health")
|
374
|
+
if "error" not in result:
|
375
|
+
print("✅ Connection test successful")
|
376
|
+
return True
|
377
|
+
else:
|
378
|
+
print(f"❌ Connection test failed: {result}")
|
379
|
+
return False
|
380
|
+
except Exception as e:
|
381
|
+
print(f"❌ Connection test failed: {e}")
|
382
|
+
return False
|
383
|
+
|
384
|
+
async def test_security_features(self) -> Dict[str, bool]:
|
385
|
+
"""Test various security features."""
|
386
|
+
results = {}
|
387
|
+
|
388
|
+
# Test basic connectivity
|
389
|
+
results["connectivity"] = await self.test_connection()
|
390
|
+
|
391
|
+
# Test authentication
|
392
|
+
if self.auth_method != "none":
|
393
|
+
try:
|
394
|
+
result = await self.get("/api/auth/status")
|
395
|
+
results["authentication"] = "error" not in result
|
396
|
+
except:
|
397
|
+
results["authentication"] = False
|
398
|
+
|
399
|
+
# Test SSL/TLS
|
400
|
+
if self.base_url.startswith("https"):
|
401
|
+
results["ssl_tls"] = True
|
402
|
+
else:
|
403
|
+
results["ssl_tls"] = False
|
404
|
+
|
405
|
+
return results
|
406
|
+
|
407
|
+
async def register_proxy(self, proxy_config: Dict[str, Any]) -> Dict[str, Any]:
|
408
|
+
"""
|
409
|
+
Register with proxy server.
|
410
|
+
|
411
|
+
Args:
|
412
|
+
proxy_config: Proxy registration configuration
|
413
|
+
|
414
|
+
Returns:
|
415
|
+
Registration result
|
416
|
+
"""
|
417
|
+
try:
|
418
|
+
result = await self.post("/api/jsonrpc", {
|
419
|
+
"jsonrpc": "2.0",
|
420
|
+
"method": "proxy_registration",
|
421
|
+
"params": proxy_config,
|
422
|
+
"id": 1
|
423
|
+
})
|
424
|
+
return result
|
425
|
+
except Exception as e:
|
426
|
+
print(f"Proxy registration failed: {e}")
|
427
|
+
return {"error": str(e)}
|
428
|
+
|
429
|
+
async def execute_command(self, command: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
430
|
+
"""
|
431
|
+
Execute a command on the server.
|
432
|
+
|
433
|
+
Args:
|
434
|
+
command: Command name
|
435
|
+
params: Command parameters
|
436
|
+
|
437
|
+
Returns:
|
438
|
+
Command result
|
439
|
+
"""
|
440
|
+
try:
|
441
|
+
result = await self.post("/api/jsonrpc", {
|
442
|
+
"jsonrpc": "2.0",
|
443
|
+
"method": command,
|
444
|
+
"params": params or {},
|
445
|
+
"id": 1
|
446
|
+
})
|
447
|
+
return result
|
448
|
+
except Exception as e:
|
449
|
+
print(f"Command execution failed: {e}")
|
450
|
+
return {"error": str(e)}
|
451
|
+
|
452
|
+
|
453
|
+
def create_client_from_config(config_file: str) -> UniversalClient:
|
454
|
+
"""
|
455
|
+
Create a UniversalClient instance from a configuration file.
|
456
|
+
|
457
|
+
Args:
|
458
|
+
config_file: Path to configuration file
|
459
|
+
|
460
|
+
Returns:
|
461
|
+
UniversalClient instance
|
462
|
+
"""
|
463
|
+
try:
|
464
|
+
with open(config_file, 'r') as f:
|
465
|
+
config_data = json.load(f)
|
466
|
+
|
467
|
+
# Extract server configuration
|
468
|
+
server_config = config_data.get("server", {})
|
469
|
+
host = server_config.get("host", "127.0.0.1")
|
470
|
+
port = server_config.get("port", 8000)
|
471
|
+
|
472
|
+
# Determine protocol
|
473
|
+
ssl_config = config_data.get("ssl", {})
|
474
|
+
ssl_enabled = ssl_config.get("enabled", False)
|
475
|
+
protocol = "https" if ssl_enabled else "http"
|
476
|
+
|
477
|
+
server_url = f"{protocol}://{host}:{port}"
|
478
|
+
|
479
|
+
# Create client configuration
|
480
|
+
client_config = {
|
481
|
+
"server_url": server_url,
|
482
|
+
"timeout": 30,
|
483
|
+
"retry_attempts": 3,
|
484
|
+
"retry_delay": 1,
|
485
|
+
"security": {
|
486
|
+
"auth_method": "none"
|
487
|
+
}
|
488
|
+
}
|
489
|
+
|
490
|
+
# Add SSL configuration if needed
|
491
|
+
if ssl_enabled:
|
492
|
+
client_config["security"]["ssl"] = {
|
493
|
+
"enabled": True,
|
494
|
+
"check_hostname": False,
|
495
|
+
"verify": False
|
496
|
+
}
|
497
|
+
|
498
|
+
# Add CA certificate if available
|
499
|
+
ca_cert = ssl_config.get("ca_cert")
|
500
|
+
if ca_cert and os.path.exists(ca_cert):
|
501
|
+
client_config["security"]["ssl"]["ca_cert_file"] = ca_cert
|
502
|
+
|
503
|
+
return UniversalClient(client_config)
|
504
|
+
|
505
|
+
except Exception as e:
|
506
|
+
raise ValueError(f"Failed to create client from config: {e}")
|
507
|
+
|
508
|
+
|
509
|
+
# CLI interface for standalone usage
|
510
|
+
async def main():
|
511
|
+
"""Main function for CLI usage."""
|
512
|
+
import argparse
|
513
|
+
|
514
|
+
parser = argparse.ArgumentParser(description="Universal Client for MCP Proxy Adapter")
|
515
|
+
parser.add_argument("--config", help="Path to configuration file")
|
516
|
+
parser.add_argument("--method", help="JSON-RPC method to call")
|
517
|
+
parser.add_argument("--params", help="JSON-RPC parameters (JSON string)")
|
518
|
+
parser.add_argument("--auth-method", help="Authentication method")
|
519
|
+
parser.add_argument("--server-url", help="Server URL")
|
520
|
+
|
521
|
+
args = parser.parse_args()
|
522
|
+
|
523
|
+
if args.config:
|
524
|
+
# Load configuration from file
|
525
|
+
try:
|
526
|
+
client = create_client_from_config(args.config)
|
527
|
+
|
528
|
+
print(f"🚀 Testing --config connection")
|
529
|
+
print("=" * 40)
|
530
|
+
print(f"Universal client initialized with auth method: --config")
|
531
|
+
print(f"Connecting to {client.base_url} with --config authentication...")
|
532
|
+
|
533
|
+
async with client:
|
534
|
+
# Test connection
|
535
|
+
success = await client.test_connection()
|
536
|
+
|
537
|
+
if success:
|
538
|
+
print("No authentication required")
|
539
|
+
print("Connection established successfully")
|
540
|
+
|
541
|
+
if args.method:
|
542
|
+
# Execute JSON-RPC method
|
543
|
+
params = {}
|
544
|
+
if args.params:
|
545
|
+
try:
|
546
|
+
params = json.loads(args.params)
|
547
|
+
except json.JSONDecodeError:
|
548
|
+
print("❌ Invalid JSON parameters")
|
549
|
+
return
|
550
|
+
|
551
|
+
result = await client.execute_command(args.method, params)
|
552
|
+
print(f"✅ Method '{args.method}' executed successfully:")
|
553
|
+
print(json.dumps(result, indent=2))
|
554
|
+
else:
|
555
|
+
# Default to help command
|
556
|
+
result = await client.execute_command("help")
|
557
|
+
print("✅ Help command executed successfully:")
|
558
|
+
print(json.dumps(result, indent=2))
|
559
|
+
else:
|
560
|
+
print("❌ Connection failed")
|
561
|
+
print("Connection closed")
|
562
|
+
|
563
|
+
except FileNotFoundError:
|
564
|
+
print(f"❌ Configuration file not found: {args.config}")
|
565
|
+
except json.JSONDecodeError:
|
566
|
+
print(f"❌ Invalid JSON in configuration file: {args.config}")
|
567
|
+
except Exception as e:
|
568
|
+
print(f"❌ Error loading configuration: {e}")
|
569
|
+
else:
|
570
|
+
print("❌ Configuration file required. Use --config option.")
|
571
|
+
|
572
|
+
|
573
|
+
if __name__ == "__main__":
|
574
|
+
asyncio.run(main())
|