mcp-proxy-adapter 4.1.1__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 +32 -0
- mcp_proxy_adapter/api/app.py +290 -33
- mcp_proxy_adapter/api/handlers.py +32 -6
- mcp_proxy_adapter/api/middleware/__init__.py +38 -32
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
- mcp_proxy_adapter/api/middleware/factory.py +243 -0
- mcp_proxy_adapter/api/middleware/logging.py +32 -6
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +201 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
- 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 +19 -4
- mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
- mcp_proxy_adapter/commands/base.py +66 -32
- mcp_proxy_adapter/commands/builtin_commands.py +95 -0
- mcp_proxy_adapter/commands/catalog_manager.py +838 -0
- mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
- mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
- mcp_proxy_adapter/commands/command_registry.py +711 -354
- mcp_proxy_adapter/commands/dependency_manager.py +245 -0
- mcp_proxy_adapter/commands/echo_command.py +81 -0
- mcp_proxy_adapter/commands/health_command.py +8 -1
- mcp_proxy_adapter/commands/help_command.py +21 -14
- mcp_proxy_adapter/commands/hooks.py +200 -167
- mcp_proxy_adapter/commands/key_management_command.py +506 -0
- mcp_proxy_adapter/commands/load_command.py +176 -0
- mcp_proxy_adapter/commands/plugins_command.py +235 -0
- mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
- mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
- mcp_proxy_adapter/commands/reload_command.py +48 -50
- mcp_proxy_adapter/commands/result.py +1 -0
- mcp_proxy_adapter/commands/role_test_command.py +141 -0
- mcp_proxy_adapter/commands/roles_management_command.py +697 -0
- mcp_proxy_adapter/commands/security_command.py +488 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +366 -0
- mcp_proxy_adapter/commands/token_management_command.py +529 -0
- mcp_proxy_adapter/commands/transport_management_command.py +144 -0
- mcp_proxy_adapter/commands/unload_command.py +158 -0
- mcp_proxy_adapter/config.py +394 -14
- mcp_proxy_adapter/core/app_factory.py +410 -0
- mcp_proxy_adapter/core/app_runner.py +272 -0
- mcp_proxy_adapter/core/auth_validator.py +606 -0
- mcp_proxy_adapter/core/certificate_utils.py +1045 -0
- 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/config_converter.py +405 -0
- mcp_proxy_adapter/core/config_validator.py +218 -0
- mcp_proxy_adapter/core/logging.py +19 -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 +385 -0
- mcp_proxy_adapter/core/proxy_client.py +602 -0
- mcp_proxy_adapter/core/proxy_registration.py +522 -0
- mcp_proxy_adapter/core/role_utils.py +426 -0
- mcp_proxy_adapter/core/security_adapter.py +370 -0
- mcp_proxy_adapter/core/security_factory.py +239 -0
- 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/settings.py +1 -0
- mcp_proxy_adapter/core/ssl_utils.py +234 -0
- mcp_proxy_adapter/core/transport_manager.py +292 -0
- mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
- mcp_proxy_adapter/custom_openapi.py +22 -11
- 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 +93 -0
- 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-4.1.1.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/rate_limit.py +0 -152
- mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
- 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 -35
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
- mcp_proxy_adapter/examples/basic_server/server.py +0 -103
- 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 -250
- 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/config.json +0 -35
- 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/hooks.py +0 -230
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
- 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/schemas/base_schema.json +0 -114
- mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
- 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 -217
- mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
- mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,284 @@
|
|
1
|
+
"""
|
2
|
+
Client Manager for MCP Proxy Adapter Framework
|
3
|
+
|
4
|
+
This module provides client management functionality for the MCP Proxy Adapter framework.
|
5
|
+
It handles client creation, connection management, and proxy registration.
|
6
|
+
|
7
|
+
Author: Vasiliy Zdanovskiy
|
8
|
+
email: vasilyvz@gmail.com
|
9
|
+
"""
|
10
|
+
|
11
|
+
import asyncio
|
12
|
+
import json
|
13
|
+
import logging
|
14
|
+
from typing import Dict, Any, Optional, List
|
15
|
+
from pathlib import Path
|
16
|
+
|
17
|
+
from .client import UniversalClient, create_client_from_config
|
18
|
+
|
19
|
+
|
20
|
+
class ClientManager:
|
21
|
+
"""
|
22
|
+
Manages client connections and proxy registrations.
|
23
|
+
|
24
|
+
This class provides functionality for:
|
25
|
+
- Creating and managing client connections
|
26
|
+
- Proxy registration
|
27
|
+
- Connection pooling
|
28
|
+
- Authentication management
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(self, config: Dict[str, Any]):
|
32
|
+
"""
|
33
|
+
Initialize client manager.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
config: Client manager configuration
|
37
|
+
"""
|
38
|
+
self.config = config
|
39
|
+
self.logger = logging.getLogger(__name__)
|
40
|
+
self.clients: Dict[str, UniversalClient] = {}
|
41
|
+
self.connection_pool: Dict[str, UniversalClient] = {}
|
42
|
+
|
43
|
+
# Client manager settings
|
44
|
+
self.max_connections = config.get("max_connections", 10)
|
45
|
+
self.connection_timeout = config.get("connection_timeout", 30)
|
46
|
+
self.retry_attempts = config.get("retry_attempts", 3)
|
47
|
+
self.retry_delay = config.get("retry_delay", 1)
|
48
|
+
|
49
|
+
self.logger.info("Client manager initialized")
|
50
|
+
|
51
|
+
async def create_client(self, client_id: str, config_file: str) -> UniversalClient:
|
52
|
+
"""
|
53
|
+
Create a new client instance.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
client_id: Unique identifier for the client
|
57
|
+
config_file: Path to client configuration file
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
UniversalClient instance
|
61
|
+
"""
|
62
|
+
try:
|
63
|
+
if client_id in self.clients:
|
64
|
+
self.logger.warning(f"Client {client_id} already exists, reusing existing connection")
|
65
|
+
return self.clients[client_id]
|
66
|
+
|
67
|
+
client = create_client_from_config(config_file)
|
68
|
+
self.clients[client_id] = client
|
69
|
+
|
70
|
+
self.logger.info(f"Client {client_id} created successfully")
|
71
|
+
return client
|
72
|
+
|
73
|
+
except Exception as e:
|
74
|
+
self.logger.error(f"Failed to create client {client_id}: {e}")
|
75
|
+
raise
|
76
|
+
|
77
|
+
async def get_client(self, client_id: str) -> Optional[UniversalClient]:
|
78
|
+
"""
|
79
|
+
Get an existing client instance.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
client_id: Client identifier
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
UniversalClient instance or None if not found
|
86
|
+
"""
|
87
|
+
return self.clients.get(client_id)
|
88
|
+
|
89
|
+
async def remove_client(self, client_id: str) -> bool:
|
90
|
+
"""
|
91
|
+
Remove a client instance.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
client_id: Client identifier
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
True if client was removed, False otherwise
|
98
|
+
"""
|
99
|
+
if client_id in self.clients:
|
100
|
+
client = self.clients[client_id]
|
101
|
+
await client.disconnect()
|
102
|
+
del self.clients[client_id]
|
103
|
+
self.logger.info(f"Client {client_id} removed")
|
104
|
+
return True
|
105
|
+
return False
|
106
|
+
|
107
|
+
async def test_client_connection(self, client_id: str) -> bool:
|
108
|
+
"""
|
109
|
+
Test connection for a specific client.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
client_id: Client identifier
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
True if connection is successful, False otherwise
|
116
|
+
"""
|
117
|
+
client = await self.get_client(client_id)
|
118
|
+
if not client:
|
119
|
+
self.logger.error(f"Client {client_id} not found")
|
120
|
+
return False
|
121
|
+
|
122
|
+
try:
|
123
|
+
return await client.test_connection()
|
124
|
+
except Exception as e:
|
125
|
+
self.logger.error(f"Connection test failed for client {client_id}: {e}")
|
126
|
+
return False
|
127
|
+
|
128
|
+
async def register_proxy(self, client_id: str, proxy_config: Dict[str, Any]) -> Dict[str, Any]:
|
129
|
+
"""
|
130
|
+
Register with proxy server using a specific client.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
client_id: Client identifier
|
134
|
+
proxy_config: Proxy registration configuration
|
135
|
+
|
136
|
+
Returns:
|
137
|
+
Registration result
|
138
|
+
"""
|
139
|
+
client = await self.get_client(client_id)
|
140
|
+
if not client:
|
141
|
+
return {"error": f"Client {client_id} not found"}
|
142
|
+
|
143
|
+
try:
|
144
|
+
result = await client.register_proxy(proxy_config)
|
145
|
+
self.logger.info(f"Proxy registration completed for client {client_id}")
|
146
|
+
return result
|
147
|
+
except Exception as e:
|
148
|
+
self.logger.error(f"Proxy registration failed for client {client_id}: {e}")
|
149
|
+
return {"error": str(e)}
|
150
|
+
|
151
|
+
async def execute_command(self, client_id: str, command: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
152
|
+
"""
|
153
|
+
Execute a command using a specific client.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
client_id: Client identifier
|
157
|
+
command: Command name
|
158
|
+
params: Command parameters
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
Command result
|
162
|
+
"""
|
163
|
+
client = await self.get_client(client_id)
|
164
|
+
if not client:
|
165
|
+
return {"error": f"Client {client_id} not found"}
|
166
|
+
|
167
|
+
try:
|
168
|
+
result = await client.execute_command(command, params or {})
|
169
|
+
self.logger.info(f"Command {command} executed for client {client_id}")
|
170
|
+
return result
|
171
|
+
except Exception as e:
|
172
|
+
self.logger.error(f"Command execution failed for client {client_id}: {e}")
|
173
|
+
return {"error": str(e)}
|
174
|
+
|
175
|
+
async def get_client_status(self, client_id: str) -> Dict[str, Any]:
|
176
|
+
"""
|
177
|
+
Get status information for a specific client.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
client_id: Client identifier
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
Client status information
|
184
|
+
"""
|
185
|
+
client = await self.get_client(client_id)
|
186
|
+
if not client:
|
187
|
+
return {"error": f"Client {client_id} not found"}
|
188
|
+
|
189
|
+
try:
|
190
|
+
# Test connection
|
191
|
+
connection_ok = await client.test_connection()
|
192
|
+
|
193
|
+
# Test security features
|
194
|
+
security_features = await client.test_security_features()
|
195
|
+
|
196
|
+
status = {
|
197
|
+
"client_id": client_id,
|
198
|
+
"base_url": client.base_url,
|
199
|
+
"auth_method": client.auth_method,
|
200
|
+
"connection_ok": connection_ok,
|
201
|
+
"security_features": security_features,
|
202
|
+
"session_active": client.session is not None
|
203
|
+
}
|
204
|
+
|
205
|
+
return status
|
206
|
+
except Exception as e:
|
207
|
+
self.logger.error(f"Failed to get status for client {client_id}: {e}")
|
208
|
+
return {"error": str(e)}
|
209
|
+
|
210
|
+
async def list_clients(self) -> List[str]:
|
211
|
+
"""
|
212
|
+
Get list of all client identifiers.
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
List of client identifiers
|
216
|
+
"""
|
217
|
+
return list(self.clients.keys())
|
218
|
+
|
219
|
+
async def cleanup(self):
|
220
|
+
"""Clean up all client connections."""
|
221
|
+
for client_id in list(self.clients.keys()):
|
222
|
+
await self.remove_client(client_id)
|
223
|
+
self.logger.info("All client connections cleaned up")
|
224
|
+
|
225
|
+
async def __aenter__(self):
|
226
|
+
"""Async context manager entry."""
|
227
|
+
return self
|
228
|
+
|
229
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
230
|
+
"""Async context manager exit."""
|
231
|
+
await self.cleanup()
|
232
|
+
|
233
|
+
|
234
|
+
def create_client_manager(config: Dict[str, Any]) -> ClientManager:
|
235
|
+
"""
|
236
|
+
Create a ClientManager instance.
|
237
|
+
|
238
|
+
Args:
|
239
|
+
config: Client manager configuration
|
240
|
+
|
241
|
+
Returns:
|
242
|
+
ClientManager instance
|
243
|
+
"""
|
244
|
+
return ClientManager(config)
|
245
|
+
|
246
|
+
|
247
|
+
# Example usage and testing functions
|
248
|
+
async def test_client_manager():
|
249
|
+
"""Test client manager functionality."""
|
250
|
+
# Example configuration
|
251
|
+
config = {
|
252
|
+
"max_connections": 5,
|
253
|
+
"connection_timeout": 30,
|
254
|
+
"retry_attempts": 3,
|
255
|
+
"retry_delay": 1
|
256
|
+
}
|
257
|
+
|
258
|
+
async with ClientManager(config) as manager:
|
259
|
+
# Create a client
|
260
|
+
client_id = "test_client"
|
261
|
+
config_file = "configs/http_simple.json"
|
262
|
+
|
263
|
+
try:
|
264
|
+
client = await manager.create_client(client_id, config_file)
|
265
|
+
print(f"✅ Client {client_id} created successfully")
|
266
|
+
|
267
|
+
# Test connection
|
268
|
+
connection_ok = await manager.test_client_connection(client_id)
|
269
|
+
print(f"✅ Connection test: {connection_ok}")
|
270
|
+
|
271
|
+
# Get status
|
272
|
+
status = await manager.get_client_status(client_id)
|
273
|
+
print(f"✅ Client status: {json.dumps(status, indent=2)}")
|
274
|
+
|
275
|
+
# Execute command
|
276
|
+
result = await manager.execute_command(client_id, "help")
|
277
|
+
print(f"✅ Command result: {json.dumps(result, indent=2)}")
|
278
|
+
|
279
|
+
except Exception as e:
|
280
|
+
print(f"❌ Test failed: {e}")
|
281
|
+
|
282
|
+
|
283
|
+
if __name__ == "__main__":
|
284
|
+
asyncio.run(test_client_manager())
|
@@ -0,0 +1,384 @@
|
|
1
|
+
"""
|
2
|
+
Client Security Module
|
3
|
+
|
4
|
+
This module provides client-side security integration for MCP Proxy Adapter,
|
5
|
+
using mcp_security_framework utilities for secure connections to servers.
|
6
|
+
|
7
|
+
Author: Vasiliy Zdanovskiy
|
8
|
+
email: vasilyvz@gmail.com
|
9
|
+
"""
|
10
|
+
|
11
|
+
import logging
|
12
|
+
import ssl
|
13
|
+
from typing import Dict, Any, Optional, List, Tuple
|
14
|
+
from pathlib import Path
|
15
|
+
|
16
|
+
# Import framework utilities
|
17
|
+
try:
|
18
|
+
from mcp_security_framework.utils.crypto_utils import (
|
19
|
+
generate_api_key, create_jwt_token, verify_jwt_token
|
20
|
+
)
|
21
|
+
from mcp_security_framework.utils.cert_utils import (
|
22
|
+
parse_certificate, extract_roles_from_certificate,
|
23
|
+
validate_certificate_chain, validate_certificate_format
|
24
|
+
)
|
25
|
+
from mcp_security_framework.core.ssl_manager import SSLManager
|
26
|
+
from mcp_security_framework.schemas.config import SSLConfig, AuthConfig
|
27
|
+
from mcp_security_framework.schemas.models import AuthResult, ValidationResult
|
28
|
+
SECURITY_FRAMEWORK_AVAILABLE = True
|
29
|
+
except ImportError:
|
30
|
+
SECURITY_FRAMEWORK_AVAILABLE = False
|
31
|
+
SSLManager = None
|
32
|
+
SSLConfig = None
|
33
|
+
AuthConfig = None
|
34
|
+
AuthResult = None
|
35
|
+
ValidationResult = None
|
36
|
+
|
37
|
+
from mcp_proxy_adapter.core.logging import logger
|
38
|
+
|
39
|
+
|
40
|
+
class ClientSecurityManager:
|
41
|
+
"""
|
42
|
+
Client-side security manager for MCP Proxy Adapter.
|
43
|
+
|
44
|
+
Provides secure client connections using mcp_security_framework utilities.
|
45
|
+
Handles authentication, certificate management, and SSL/TLS for client connections.
|
46
|
+
"""
|
47
|
+
|
48
|
+
def __init__(self, config: Dict[str, Any]):
|
49
|
+
"""
|
50
|
+
Initialize client security manager.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
config: Security configuration
|
54
|
+
"""
|
55
|
+
if not SECURITY_FRAMEWORK_AVAILABLE:
|
56
|
+
raise ImportError("mcp_security_framework is not available")
|
57
|
+
|
58
|
+
self.config = config
|
59
|
+
self.security_config = config.get("security", {})
|
60
|
+
|
61
|
+
# Initialize SSL manager if needed
|
62
|
+
self.ssl_manager = None
|
63
|
+
if self.security_config.get("ssl", {}).get("enabled", False):
|
64
|
+
ssl_config = self._create_ssl_config()
|
65
|
+
self.ssl_manager = SSLManager(ssl_config)
|
66
|
+
|
67
|
+
logger.info("Client security manager initialized")
|
68
|
+
|
69
|
+
def _create_ssl_config(self) -> SSLConfig:
|
70
|
+
"""Create SSL configuration for client connections."""
|
71
|
+
ssl_section = self.security_config.get("ssl", {})
|
72
|
+
|
73
|
+
return SSLConfig(
|
74
|
+
enabled=ssl_section.get("enabled", False),
|
75
|
+
cert_file=ssl_section.get("client_cert_file"),
|
76
|
+
key_file=ssl_section.get("client_key_file"),
|
77
|
+
ca_cert_file=ssl_section.get("ca_cert_file"),
|
78
|
+
verify_mode=ssl_section.get("verify_mode", "CERT_REQUIRED"),
|
79
|
+
min_tls_version=ssl_section.get("min_tls_version", "TLSv1.2"),
|
80
|
+
check_hostname=ssl_section.get("check_hostname", True),
|
81
|
+
check_expiry=ssl_section.get("check_expiry", True)
|
82
|
+
)
|
83
|
+
|
84
|
+
def create_client_ssl_context(self, server_hostname: Optional[str] = None) -> Optional[ssl.SSLContext]:
|
85
|
+
"""
|
86
|
+
Create SSL context for client connections.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
server_hostname: Server hostname for SNI
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
SSL context or None if SSL not enabled
|
93
|
+
"""
|
94
|
+
if not self.ssl_manager:
|
95
|
+
return None
|
96
|
+
|
97
|
+
try:
|
98
|
+
# Create client SSL context
|
99
|
+
context = self.ssl_manager.create_client_context()
|
100
|
+
|
101
|
+
if server_hostname:
|
102
|
+
# Set server hostname for SNI
|
103
|
+
context.check_hostname = True
|
104
|
+
context.verify_mode = ssl.CERT_REQUIRED
|
105
|
+
|
106
|
+
logger.info(f"Client SSL context created for {server_hostname or 'unknown server'}")
|
107
|
+
return context
|
108
|
+
|
109
|
+
except Exception as e:
|
110
|
+
logger.error(f"Failed to create client SSL context: {e}")
|
111
|
+
return None
|
112
|
+
|
113
|
+
def generate_client_api_key(self, user_id: str, prefix: str = "mcp_proxy") -> str:
|
114
|
+
"""
|
115
|
+
Generate API key for client authentication.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
user_id: User identifier
|
119
|
+
prefix: Key prefix
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
Generated API key
|
123
|
+
"""
|
124
|
+
try:
|
125
|
+
api_key = generate_api_key(prefix=prefix)
|
126
|
+
logger.info(f"Generated API key for user: {user_id}")
|
127
|
+
return api_key
|
128
|
+
except Exception as e:
|
129
|
+
logger.error(f"Failed to generate API key: {e}")
|
130
|
+
raise
|
131
|
+
|
132
|
+
def create_client_jwt_token(self, user_id: str, roles: List[str],
|
133
|
+
secret: str, algorithm: str = "HS256",
|
134
|
+
expiry_hours: int = 24) -> str:
|
135
|
+
"""
|
136
|
+
Create JWT token for client authentication.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
user_id: User identifier
|
140
|
+
roles: User roles
|
141
|
+
secret: JWT secret
|
142
|
+
algorithm: JWT algorithm
|
143
|
+
expiry_hours: Token expiry in hours
|
144
|
+
|
145
|
+
Returns:
|
146
|
+
JWT token
|
147
|
+
"""
|
148
|
+
try:
|
149
|
+
payload = {
|
150
|
+
"user_id": user_id,
|
151
|
+
"roles": roles,
|
152
|
+
"type": "client_proxy"
|
153
|
+
}
|
154
|
+
|
155
|
+
token = create_jwt_token(
|
156
|
+
payload=payload,
|
157
|
+
secret=secret,
|
158
|
+
algorithm=algorithm,
|
159
|
+
expiry_hours=expiry_hours
|
160
|
+
)
|
161
|
+
|
162
|
+
logger.info(f"Created JWT token for user: {user_id}")
|
163
|
+
return token
|
164
|
+
|
165
|
+
except Exception as e:
|
166
|
+
logger.error(f"Failed to create JWT token: {e}")
|
167
|
+
raise
|
168
|
+
|
169
|
+
def validate_server_certificate(self, cert_path: str, ca_cert_path: Optional[str] = None) -> bool:
|
170
|
+
"""
|
171
|
+
Validate server certificate before connection.
|
172
|
+
|
173
|
+
Args:
|
174
|
+
cert_path: Path to server certificate
|
175
|
+
ca_cert_path: Path to CA certificate
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
True if certificate is valid
|
179
|
+
"""
|
180
|
+
try:
|
181
|
+
# Validate certificate format
|
182
|
+
if not validate_certificate_format(cert_path):
|
183
|
+
logger.error(f"Invalid certificate format: {cert_path}")
|
184
|
+
return False
|
185
|
+
|
186
|
+
# Validate certificate chain if CA provided
|
187
|
+
if ca_cert_path:
|
188
|
+
if not validate_certificate_chain(cert_path, ca_cert_path):
|
189
|
+
logger.error(f"Invalid certificate chain: {cert_path}")
|
190
|
+
return False
|
191
|
+
|
192
|
+
# Parse certificate and check basic properties
|
193
|
+
cert_info = parse_certificate(cert_path)
|
194
|
+
if not cert_info:
|
195
|
+
logger.error(f"Failed to parse certificate: {cert_path}")
|
196
|
+
return False
|
197
|
+
|
198
|
+
logger.info(f"Server certificate validated: {cert_path}")
|
199
|
+
return True
|
200
|
+
|
201
|
+
except Exception as e:
|
202
|
+
logger.error(f"Failed to validate server certificate: {e}")
|
203
|
+
return False
|
204
|
+
|
205
|
+
def extract_server_roles(self, cert_path: str) -> List[str]:
|
206
|
+
"""
|
207
|
+
Extract roles from server certificate.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
cert_path: Path to server certificate
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
List of roles extracted from certificate
|
214
|
+
"""
|
215
|
+
try:
|
216
|
+
roles = extract_roles_from_certificate(cert_path)
|
217
|
+
logger.info(f"Extracted roles from server certificate: {roles}")
|
218
|
+
return roles
|
219
|
+
except Exception as e:
|
220
|
+
logger.error(f"Failed to extract roles from certificate: {e}")
|
221
|
+
return []
|
222
|
+
|
223
|
+
def get_client_auth_headers(self, auth_method: str = "api_key", **kwargs) -> Dict[str, str]:
|
224
|
+
"""
|
225
|
+
Get authentication headers for client requests.
|
226
|
+
|
227
|
+
Args:
|
228
|
+
auth_method: Authentication method (api_key, jwt, certificate)
|
229
|
+
**kwargs: Additional parameters
|
230
|
+
|
231
|
+
Returns:
|
232
|
+
Dictionary of authentication headers
|
233
|
+
"""
|
234
|
+
headers = {}
|
235
|
+
|
236
|
+
try:
|
237
|
+
if auth_method == "api_key":
|
238
|
+
api_key = kwargs.get("api_key")
|
239
|
+
if api_key:
|
240
|
+
headers["X-API-Key"] = api_key
|
241
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
242
|
+
|
243
|
+
elif auth_method == "jwt":
|
244
|
+
token = kwargs.get("token")
|
245
|
+
if token:
|
246
|
+
headers["Authorization"] = f"Bearer {token}"
|
247
|
+
|
248
|
+
elif auth_method == "certificate":
|
249
|
+
# Certificate authentication is handled at SSL level
|
250
|
+
headers["X-Auth-Method"] = "certificate"
|
251
|
+
|
252
|
+
# Add common proxy headers
|
253
|
+
headers["X-Proxy-Type"] = "mcp_proxy_adapter"
|
254
|
+
headers["X-Client-Type"] = "proxy_client"
|
255
|
+
|
256
|
+
logger.debug(f"Created auth headers for method: {auth_method}")
|
257
|
+
return headers
|
258
|
+
|
259
|
+
except Exception as e:
|
260
|
+
logger.error(f"Failed to create auth headers: {e}")
|
261
|
+
return {}
|
262
|
+
|
263
|
+
def prepare_client_connection(self, server_config: Dict[str, Any]) -> Tuple[Optional[ssl.SSLContext], Dict[str, str]]:
|
264
|
+
"""
|
265
|
+
Prepare secure client connection to server.
|
266
|
+
|
267
|
+
Args:
|
268
|
+
server_config: Server connection configuration
|
269
|
+
|
270
|
+
Returns:
|
271
|
+
Tuple of (SSL context, auth headers)
|
272
|
+
"""
|
273
|
+
ssl_context = None
|
274
|
+
auth_headers = {}
|
275
|
+
|
276
|
+
try:
|
277
|
+
# Create SSL context if needed
|
278
|
+
if server_config.get("ssl", False):
|
279
|
+
server_hostname = server_config.get("hostname")
|
280
|
+
ssl_context = self.create_client_ssl_context(server_hostname)
|
281
|
+
|
282
|
+
# Create authentication headers
|
283
|
+
auth_method = server_config.get("auth_method", "api_key")
|
284
|
+
auth_headers = self.get_client_auth_headers(
|
285
|
+
auth_method=auth_method,
|
286
|
+
api_key=server_config.get("api_key"),
|
287
|
+
token=server_config.get("token")
|
288
|
+
)
|
289
|
+
|
290
|
+
logger.info(f"Prepared client connection for {server_config.get('hostname', 'unknown')}")
|
291
|
+
return ssl_context, auth_headers
|
292
|
+
|
293
|
+
except Exception as e:
|
294
|
+
logger.error(f"Failed to prepare client connection: {e}")
|
295
|
+
return None, {}
|
296
|
+
|
297
|
+
def validate_server_response(self, response_headers: Dict[str, str]) -> bool:
|
298
|
+
"""
|
299
|
+
Validate server response for security compliance.
|
300
|
+
|
301
|
+
Args:
|
302
|
+
response_headers: Server response headers
|
303
|
+
|
304
|
+
Returns:
|
305
|
+
True if response is valid
|
306
|
+
"""
|
307
|
+
try:
|
308
|
+
# Check for required security headers
|
309
|
+
required_headers = ["Content-Type"]
|
310
|
+
for header in required_headers:
|
311
|
+
if header not in response_headers:
|
312
|
+
logger.warning(f"Missing required header: {header}")
|
313
|
+
|
314
|
+
# Check for security headers
|
315
|
+
security_headers = ["X-Frame-Options", "X-Content-Type-Options"]
|
316
|
+
for header in security_headers:
|
317
|
+
if header in response_headers:
|
318
|
+
logger.debug(f"Found security header: {header}")
|
319
|
+
|
320
|
+
# Validate content type
|
321
|
+
content_type = response_headers.get("Content-Type", "")
|
322
|
+
if "application/json" not in content_type:
|
323
|
+
logger.warning(f"Unexpected content type: {content_type}")
|
324
|
+
|
325
|
+
return True
|
326
|
+
|
327
|
+
except Exception as e:
|
328
|
+
logger.error(f"Failed to validate server response: {e}")
|
329
|
+
return False
|
330
|
+
|
331
|
+
def get_client_certificate_info(self) -> Optional[Dict[str, Any]]:
|
332
|
+
"""
|
333
|
+
Get information about client certificate.
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
Certificate information or None
|
337
|
+
"""
|
338
|
+
try:
|
339
|
+
ssl_config = self.security_config.get("ssl", {})
|
340
|
+
cert_path = ssl_config.get("client_cert_file")
|
341
|
+
|
342
|
+
if not cert_path or not Path(cert_path).exists():
|
343
|
+
return None
|
344
|
+
|
345
|
+
cert_info = parse_certificate(cert_path)
|
346
|
+
if cert_info:
|
347
|
+
roles = extract_roles_from_certificate(cert_path)
|
348
|
+
cert_info["roles"] = roles
|
349
|
+
return cert_info
|
350
|
+
|
351
|
+
return None
|
352
|
+
|
353
|
+
except Exception as e:
|
354
|
+
logger.error(f"Failed to get client certificate info: {e}")
|
355
|
+
return None
|
356
|
+
|
357
|
+
def is_ssl_enabled(self) -> bool:
|
358
|
+
"""Check if SSL is enabled for client connections."""
|
359
|
+
return self.security_config.get("ssl", {}).get("enabled", False)
|
360
|
+
|
361
|
+
def get_supported_auth_methods(self) -> List[str]:
|
362
|
+
"""Get list of supported authentication methods."""
|
363
|
+
return ["api_key", "jwt", "certificate"]
|
364
|
+
|
365
|
+
|
366
|
+
# Factory function for easy integration
|
367
|
+
def create_client_security_manager(config: Dict[str, Any]) -> Optional[ClientSecurityManager]:
|
368
|
+
"""
|
369
|
+
Create client security manager instance.
|
370
|
+
|
371
|
+
Args:
|
372
|
+
config: Configuration dictionary
|
373
|
+
|
374
|
+
Returns:
|
375
|
+
ClientSecurityManager instance or None if framework not available
|
376
|
+
"""
|
377
|
+
try:
|
378
|
+
return ClientSecurityManager(config)
|
379
|
+
except ImportError:
|
380
|
+
logger.warning("mcp_security_framework not available, client security disabled")
|
381
|
+
return None
|
382
|
+
except Exception as e:
|
383
|
+
logger.error(f"Failed to create client security manager: {e}")
|
384
|
+
return None
|