mcp-proxy-adapter 6.9.43__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/__init__.py +47 -0
- mcp_proxy_adapter/__main__.py +13 -0
- mcp_proxy_adapter/api/__init__.py +0 -0
- mcp_proxy_adapter/api/app.py +66 -0
- mcp_proxy_adapter/api/core/__init__.py +18 -0
- mcp_proxy_adapter/api/core/app_factory.py +355 -0
- mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
- mcp_proxy_adapter/api/core/registration_context.py +356 -0
- mcp_proxy_adapter/api/core/registration_manager.py +266 -0
- mcp_proxy_adapter/api/core/registration_tasks.py +84 -0
- mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
- mcp_proxy_adapter/api/handlers.py +181 -0
- mcp_proxy_adapter/api/middleware/__init__.py +21 -0
- mcp_proxy_adapter/api/middleware/base.py +54 -0
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +73 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +76 -0
- mcp_proxy_adapter/api/middleware/factory.py +147 -0
- mcp_proxy_adapter/api/middleware/logging.py +31 -0
- mcp_proxy_adapter/api/middleware/performance.py +51 -0
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +140 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +87 -0
- mcp_proxy_adapter/api/middleware/unified_security.py +223 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +132 -0
- mcp_proxy_adapter/api/openapi/__init__.py +21 -0
- mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
- mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
- mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
- mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
- mcp_proxy_adapter/api/schemas.py +270 -0
- mcp_proxy_adapter/api/tool_integration.py +131 -0
- mcp_proxy_adapter/api/tools.py +163 -0
- mcp_proxy_adapter/cli/__init__.py +12 -0
- mcp_proxy_adapter/cli/commands/__init__.py +15 -0
- mcp_proxy_adapter/cli/commands/client.py +100 -0
- mcp_proxy_adapter/cli/commands/config_generate.py +35 -0
- mcp_proxy_adapter/cli/commands/config_validate.py +74 -0
- mcp_proxy_adapter/cli/commands/generate.py +259 -0
- mcp_proxy_adapter/cli/commands/server.py +174 -0
- mcp_proxy_adapter/cli/commands/sets.py +128 -0
- mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
- mcp_proxy_adapter/cli/examples/__init__.py +8 -0
- mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
- mcp_proxy_adapter/cli/examples/https_token.py +96 -0
- mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
- mcp_proxy_adapter/cli/main.py +63 -0
- mcp_proxy_adapter/cli/parser.py +338 -0
- mcp_proxy_adapter/cli/validators.py +231 -0
- mcp_proxy_adapter/client/jsonrpc_client/__init__.py +9 -0
- mcp_proxy_adapter/client/jsonrpc_client/client.py +42 -0
- mcp_proxy_adapter/client/jsonrpc_client/command_api.py +45 -0
- mcp_proxy_adapter/client/jsonrpc_client/proxy_api.py +224 -0
- mcp_proxy_adapter/client/jsonrpc_client/queue_api.py +60 -0
- mcp_proxy_adapter/client/jsonrpc_client/transport.py +108 -0
- mcp_proxy_adapter/client/proxy.py +123 -0
- mcp_proxy_adapter/commands/__init__.py +66 -0
- mcp_proxy_adapter/commands/auth_validation_command.py +69 -0
- mcp_proxy_adapter/commands/base.py +389 -0
- mcp_proxy_adapter/commands/builtin_commands.py +30 -0
- mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
- mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
- mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
- mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
- mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
- mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
- mcp_proxy_adapter/commands/catalog_manager.py +97 -0
- mcp_proxy_adapter/commands/cert_monitor_command.py +552 -0
- mcp_proxy_adapter/commands/certificate_management_command.py +562 -0
- mcp_proxy_adapter/commands/command_registry.py +298 -0
- mcp_proxy_adapter/commands/config_command.py +102 -0
- mcp_proxy_adapter/commands/dependency_container.py +40 -0
- mcp_proxy_adapter/commands/dependency_manager.py +143 -0
- mcp_proxy_adapter/commands/echo_command.py +48 -0
- mcp_proxy_adapter/commands/health_command.py +142 -0
- mcp_proxy_adapter/commands/help_command.py +175 -0
- mcp_proxy_adapter/commands/hooks.py +172 -0
- mcp_proxy_adapter/commands/key_management_command.py +484 -0
- mcp_proxy_adapter/commands/load_command.py +123 -0
- mcp_proxy_adapter/commands/plugins_command.py +246 -0
- mcp_proxy_adapter/commands/protocol_management_command.py +216 -0
- mcp_proxy_adapter/commands/proxy_registration_command.py +319 -0
- mcp_proxy_adapter/commands/queue_commands.py +750 -0
- mcp_proxy_adapter/commands/registration_status_command.py +76 -0
- mcp_proxy_adapter/commands/registry/__init__.py +18 -0
- mcp_proxy_adapter/commands/registry/command_info.py +103 -0
- mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
- mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
- mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
- mcp_proxy_adapter/commands/reload_command.py +136 -0
- mcp_proxy_adapter/commands/result.py +157 -0
- mcp_proxy_adapter/commands/role_test_command.py +99 -0
- mcp_proxy_adapter/commands/roles_management_command.py +502 -0
- mcp_proxy_adapter/commands/security_command.py +472 -0
- mcp_proxy_adapter/commands/settings_command.py +113 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +306 -0
- mcp_proxy_adapter/commands/token_management_command.py +500 -0
- mcp_proxy_adapter/commands/transport_management_command.py +129 -0
- mcp_proxy_adapter/commands/unload_command.py +92 -0
- mcp_proxy_adapter/config.py +32 -0
- mcp_proxy_adapter/core/__init__.py +8 -0
- mcp_proxy_adapter/core/app_factory.py +560 -0
- mcp_proxy_adapter/core/app_runner.py +318 -0
- mcp_proxy_adapter/core/auth_validator.py +508 -0
- mcp_proxy_adapter/core/certificate/__init__.py +20 -0
- mcp_proxy_adapter/core/certificate/certificate_creator.py +372 -0
- mcp_proxy_adapter/core/certificate/certificate_extractor.py +185 -0
- mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
- mcp_proxy_adapter/core/certificate/certificate_validator.py +388 -0
- mcp_proxy_adapter/core/certificate/ssl_context_manager.py +65 -0
- mcp_proxy_adapter/core/certificate_utils.py +249 -0
- mcp_proxy_adapter/core/client.py +608 -0
- mcp_proxy_adapter/core/client_manager.py +271 -0
- mcp_proxy_adapter/core/client_security.py +411 -0
- mcp_proxy_adapter/core/config/__init__.py +18 -0
- mcp_proxy_adapter/core/config/config.py +237 -0
- mcp_proxy_adapter/core/config/config_factory.py +22 -0
- mcp_proxy_adapter/core/config/config_loader.py +66 -0
- mcp_proxy_adapter/core/config/feature_manager.py +31 -0
- mcp_proxy_adapter/core/config/simple_config.py +116 -0
- mcp_proxy_adapter/core/config/simple_config_generator.py +100 -0
- mcp_proxy_adapter/core/config/simple_config_validator.py +380 -0
- mcp_proxy_adapter/core/config_converter.py +252 -0
- mcp_proxy_adapter/core/config_validator.py +211 -0
- mcp_proxy_adapter/core/crl_utils.py +362 -0
- mcp_proxy_adapter/core/errors.py +276 -0
- mcp_proxy_adapter/core/job_manager.py +54 -0
- mcp_proxy_adapter/core/logging.py +250 -0
- mcp_proxy_adapter/core/mtls_asgi.py +140 -0
- mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
- mcp_proxy_adapter/core/mtls_proxy.py +229 -0
- mcp_proxy_adapter/core/mtls_server.py +154 -0
- mcp_proxy_adapter/core/protocol_manager.py +232 -0
- mcp_proxy_adapter/core/proxy/__init__.py +19 -0
- mcp_proxy_adapter/core/proxy/auth_manager.py +26 -0
- mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +160 -0
- mcp_proxy_adapter/core/proxy/registration_client.py +186 -0
- mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
- mcp_proxy_adapter/core/proxy_client.py +184 -0
- mcp_proxy_adapter/core/proxy_registration.py +80 -0
- mcp_proxy_adapter/core/role_utils.py +103 -0
- mcp_proxy_adapter/core/security_adapter.py +343 -0
- mcp_proxy_adapter/core/security_factory.py +96 -0
- mcp_proxy_adapter/core/security_integration.py +342 -0
- mcp_proxy_adapter/core/server_adapter.py +251 -0
- mcp_proxy_adapter/core/server_engine.py +217 -0
- mcp_proxy_adapter/core/settings.py +260 -0
- mcp_proxy_adapter/core/signal_handler.py +107 -0
- mcp_proxy_adapter/core/ssl_utils.py +161 -0
- mcp_proxy_adapter/core/transport_manager.py +153 -0
- mcp_proxy_adapter/core/unified_config_adapter.py +471 -0
- mcp_proxy_adapter/core/utils.py +101 -0
- mcp_proxy_adapter/core/validation/__init__.py +21 -0
- mcp_proxy_adapter/core/validation/config_validator.py +219 -0
- mcp_proxy_adapter/core/validation/file_validator.py +131 -0
- mcp_proxy_adapter/core/validation/protocol_validator.py +190 -0
- mcp_proxy_adapter/core/validation/security_validator.py +140 -0
- mcp_proxy_adapter/core/validation/validation_result.py +27 -0
- mcp_proxy_adapter/custom_openapi.py +58 -0
- mcp_proxy_adapter/examples/__init__.py +16 -0
- 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 +52 -0
- mcp_proxy_adapter/examples/bugfix_certificate_config.py +261 -0
- mcp_proxy_adapter/examples/cert_manager_bugfix.py +203 -0
- mcp_proxy_adapter/examples/check_config.py +413 -0
- mcp_proxy_adapter/examples/client_usage_example.py +164 -0
- mcp_proxy_adapter/examples/commands/__init__.py +5 -0
- mcp_proxy_adapter/examples/config_builder.py +234 -0
- mcp_proxy_adapter/examples/config_cli.py +282 -0
- mcp_proxy_adapter/examples/create_test_configs.py +174 -0
- mcp_proxy_adapter/examples/debug_request_state.py +130 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +191 -0
- mcp_proxy_adapter/examples/demo_client.py +287 -0
- mcp_proxy_adapter/examples/full_application/__init__.py +13 -0
- mcp_proxy_adapter/examples/full_application/commands/__init__.py +8 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +45 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +52 -0
- mcp_proxy_adapter/examples/full_application/commands/echo_command.py +32 -0
- mcp_proxy_adapter/examples/full_application/commands/help_command.py +54 -0
- mcp_proxy_adapter/examples/full_application/commands/list_command.py +57 -0
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +5 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +29 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +27 -0
- mcp_proxy_adapter/examples/full_application/main.py +264 -0
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +81 -0
- mcp_proxy_adapter/examples/full_application/run_mtls.py +252 -0
- mcp_proxy_adapter/examples/full_application/run_simple.py +152 -0
- mcp_proxy_adapter/examples/full_application/test_minimal_server.py +45 -0
- mcp_proxy_adapter/examples/full_application/test_server.py +163 -0
- mcp_proxy_adapter/examples/full_application/test_simple_server.py +62 -0
- mcp_proxy_adapter/examples/generate_config.py +502 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +335 -0
- mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
- mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
- mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
- mcp_proxy_adapter/examples/queue_server_example.py +85 -0
- mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
- mcp_proxy_adapter/examples/required_certificates.py +208 -0
- mcp_proxy_adapter/examples/run_example.py +77 -0
- mcp_proxy_adapter/examples/run_full_test_suite.py +619 -0
- mcp_proxy_adapter/examples/run_proxy_server.py +153 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +435 -0
- mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
- mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
- mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
- mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
- mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
- mcp_proxy_adapter/examples/security_test_client.py +72 -0
- mcp_proxy_adapter/examples/setup/__init__.py +24 -0
- mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
- mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
- mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
- mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
- mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
- mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +235 -0
- mcp_proxy_adapter/examples/simple_protocol_test.py +125 -0
- mcp_proxy_adapter/examples/test_chk_hostname_automated.py +211 -0
- mcp_proxy_adapter/examples/test_config.py +205 -0
- mcp_proxy_adapter/examples/test_config_builder.py +110 -0
- mcp_proxy_adapter/examples/test_examples.py +308 -0
- mcp_proxy_adapter/examples/test_framework_complete.py +267 -0
- mcp_proxy_adapter/examples/test_mcp_server.py +187 -0
- mcp_proxy_adapter/examples/test_protocol_examples.py +337 -0
- mcp_proxy_adapter/examples/universal_client.py +674 -0
- mcp_proxy_adapter/examples/update_config_certificates.py +135 -0
- mcp_proxy_adapter/examples/validate_generator_compatibility.py +385 -0
- mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +61 -0
- mcp_proxy_adapter/integrations/__init__.py +25 -0
- mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
- mcp_proxy_adapter/main.py +313 -0
- mcp_proxy_adapter/openapi.py +375 -0
- mcp_proxy_adapter/schemas/base_schema.json +114 -0
- mcp_proxy_adapter/schemas/openapi_schema.json +314 -0
- mcp_proxy_adapter/schemas/roles.json +37 -0
- mcp_proxy_adapter/schemas/roles_schema.json +162 -0
- mcp_proxy_adapter/version.py +5 -0
- mcp_proxy_adapter-6.9.43.dist-info/METADATA +739 -0
- mcp_proxy_adapter-6.9.43.dist-info/RECORD +242 -0
- mcp_proxy_adapter-6.9.43.dist-info/WHEEL +5 -0
- mcp_proxy_adapter-6.9.43.dist-info/entry_points.txt +12 -0
- mcp_proxy_adapter-6.9.43.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Author: Vasiliy Zdanovskiy
|
|
3
|
+
email: vasilyvz@gmail.com
|
|
4
|
+
|
|
5
|
+
Registration client for proxy registration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any
|
|
9
|
+
import aiohttp
|
|
10
|
+
|
|
11
|
+
from mcp_proxy_adapter.core.logging import get_global_logger
|
|
12
|
+
from .auth_manager import AuthManager
|
|
13
|
+
from .ssl_manager import SSLManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RegistrationClient:
|
|
17
|
+
"""Client for proxy registration operations."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
client_security,
|
|
22
|
+
registration_config: Dict[str, Any],
|
|
23
|
+
config: Dict[str, Any],
|
|
24
|
+
proxy_url: str,
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Initialize registration client.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
client_security: Client security manager instance
|
|
31
|
+
registration_config: Registration configuration
|
|
32
|
+
config: Application configuration
|
|
33
|
+
proxy_url: Proxy server URL
|
|
34
|
+
"""
|
|
35
|
+
self.client_security = client_security
|
|
36
|
+
self.registration_config = registration_config
|
|
37
|
+
self.config = config
|
|
38
|
+
self.proxy_url = proxy_url
|
|
39
|
+
self.logger = get_global_logger()
|
|
40
|
+
|
|
41
|
+
# Initialize managers
|
|
42
|
+
self.auth_manager = AuthManager(client_security, registration_config)
|
|
43
|
+
self.ssl_manager = SSLManager(
|
|
44
|
+
client_security, registration_config, config, proxy_url
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def _prepare_registration_data(self, server_url: str) -> Dict[str, Any]:
|
|
48
|
+
"""
|
|
49
|
+
Prepare registration data.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
server_url: Server URL to register
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Registration data dictionary
|
|
56
|
+
"""
|
|
57
|
+
# Proxy expects "name" field, use server_id or server_name
|
|
58
|
+
server_name = (
|
|
59
|
+
self.registration_config.get("server_id")
|
|
60
|
+
or self.registration_config.get("server_name")
|
|
61
|
+
or "mcp_proxy_adapter"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"name": server_name,
|
|
66
|
+
"url": server_url,
|
|
67
|
+
"capabilities": self.registration_config.get("capabilities", ["jsonrpc"]),
|
|
68
|
+
"metadata": {
|
|
69
|
+
"server_id": self.registration_config.get("server_id"),
|
|
70
|
+
"server_name": self.registration_config.get("server_name"),
|
|
71
|
+
"description": self.registration_config.get("description", ""),
|
|
72
|
+
"version": self.registration_config.get("version", "1.0.0"),
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async def register(self, server_url: str) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Register server with proxy.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
server_url: Server URL to register
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if registration successful, False otherwise
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
registration_data = self._prepare_registration_data(server_url)
|
|
88
|
+
|
|
89
|
+
# Get SSL context if needed
|
|
90
|
+
ssl_context = self.ssl_manager.get_ssl_context()
|
|
91
|
+
|
|
92
|
+
# Get headers with authentication if needed
|
|
93
|
+
headers = self.auth_manager.get_headers()
|
|
94
|
+
|
|
95
|
+
# Prepare request configuration
|
|
96
|
+
connector = None
|
|
97
|
+
if ssl_context:
|
|
98
|
+
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
|
99
|
+
|
|
100
|
+
# Send registration request
|
|
101
|
+
async with aiohttp.ClientSession(connector=connector) as session:
|
|
102
|
+
register_url = f"{self.proxy_url}/register"
|
|
103
|
+
self.logger.info(f"Attempting to register server with proxy at {register_url}")
|
|
104
|
+
self.logger.debug(f"Registration data: {registration_data}")
|
|
105
|
+
self.logger.debug(f"Headers: {headers}")
|
|
106
|
+
|
|
107
|
+
# Ensure Content-Type header is set
|
|
108
|
+
if "Content-Type" not in headers:
|
|
109
|
+
headers["Content-Type"] = "application/json"
|
|
110
|
+
|
|
111
|
+
async with session.post(
|
|
112
|
+
register_url,
|
|
113
|
+
json=registration_data,
|
|
114
|
+
headers=headers,
|
|
115
|
+
timeout=aiohttp.ClientTimeout(total=30)
|
|
116
|
+
) as response:
|
|
117
|
+
if response.status == 200:
|
|
118
|
+
result = await response.json()
|
|
119
|
+
self.logger.info(f"✅ Successfully registered with proxy. Server key: {result.get('key')}")
|
|
120
|
+
return True
|
|
121
|
+
else:
|
|
122
|
+
error_text = await response.text()
|
|
123
|
+
self.logger.error(
|
|
124
|
+
f"❌ Failed to register with proxy: {response.status} {response.reason}: {error_text}"
|
|
125
|
+
)
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
self.logger.error(f"Registration error: {e}", exc_info=True)
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
async def unregister(self) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
Unregister server from proxy.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
True if unregistration successful, False otherwise
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
server_name = (
|
|
141
|
+
self.registration_config.get("server_id")
|
|
142
|
+
or self.registration_config.get("server_name")
|
|
143
|
+
or "mcp_proxy_adapter"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
unregister_data = {
|
|
147
|
+
"name": server_name,
|
|
148
|
+
"url": "", # Not needed for unregister
|
|
149
|
+
"capabilities": [],
|
|
150
|
+
"metadata": {},
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Get SSL context if needed
|
|
154
|
+
ssl_context = self.ssl_manager.get_ssl_context()
|
|
155
|
+
|
|
156
|
+
# Get headers with authentication if needed
|
|
157
|
+
headers = self.auth_manager.get_headers()
|
|
158
|
+
|
|
159
|
+
# Prepare request configuration
|
|
160
|
+
connector = None
|
|
161
|
+
if ssl_context:
|
|
162
|
+
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
|
163
|
+
|
|
164
|
+
# Send unregistration request
|
|
165
|
+
async with aiohttp.ClientSession(connector=connector) as session:
|
|
166
|
+
unregister_url = f"{self.proxy_url}/unregister"
|
|
167
|
+
|
|
168
|
+
async with session.post(
|
|
169
|
+
unregister_url,
|
|
170
|
+
json=unregister_data,
|
|
171
|
+
headers=headers,
|
|
172
|
+
timeout=aiohttp.ClientTimeout(total=10)
|
|
173
|
+
) as response:
|
|
174
|
+
if response.status == 200:
|
|
175
|
+
self.logger.info("✅ Successfully unregistered from proxy")
|
|
176
|
+
return True
|
|
177
|
+
else:
|
|
178
|
+
error_text = await response.text()
|
|
179
|
+
self.logger.warning(
|
|
180
|
+
f"⚠️ Failed to unregister from proxy: {response.status} {response.reason}: {error_text}"
|
|
181
|
+
)
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
self.logger.error(f"Unregistration error: {e}", exc_info=True)
|
|
186
|
+
return False
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Author: Vasiliy Zdanovskiy
|
|
3
|
+
email: vasilyvz@gmail.com
|
|
4
|
+
|
|
5
|
+
SSL management for proxy registration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ssl
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
from mcp_proxy_adapter.core.logging import get_global_logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SSLManager:
|
|
16
|
+
"""Manager for SSL connections in proxy registration."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, client_security, registration_config: Dict[str, Any], config: Dict[str, Any], proxy_url: str):
|
|
19
|
+
"""
|
|
20
|
+
Initialize SSL manager.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
client_security: Client security manager instance
|
|
24
|
+
registration_config: Registration configuration
|
|
25
|
+
config: Application configuration
|
|
26
|
+
proxy_url: Proxy server URL
|
|
27
|
+
"""
|
|
28
|
+
self.client_security = client_security
|
|
29
|
+
self.registration_config = registration_config
|
|
30
|
+
self.config = config
|
|
31
|
+
self.proxy_url = proxy_url
|
|
32
|
+
self.logger = get_global_logger()
|
|
33
|
+
|
|
34
|
+
def create_ssl_context(self) -> Optional[ssl.SSLContext]:
|
|
35
|
+
"""
|
|
36
|
+
Create SSL context for secure connections using registration SSL configuration.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
SSL context or None if SSL not needed
|
|
40
|
+
"""
|
|
41
|
+
self.logger.debug("_create_ssl_context called")
|
|
42
|
+
|
|
43
|
+
# Decide SSL strictly by proxy URL scheme: use SSL only for https proxy URLs
|
|
44
|
+
try:
|
|
45
|
+
scheme = urlparse(self.proxy_url).scheme if self.proxy_url else "http"
|
|
46
|
+
if scheme.lower() != "https":
|
|
47
|
+
self.logger.debug("Proxy URL is HTTP, skipping SSL context creation for registration")
|
|
48
|
+
return None
|
|
49
|
+
except Exception:
|
|
50
|
+
self.logger.debug("Failed to parse proxy_url, assuming HTTP and skipping SSL context")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
if not self.client_security:
|
|
54
|
+
self.logger.debug("SSL context creation failed: client_security is None")
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
# Check if SSL is enabled for registration
|
|
59
|
+
cert_config = self.registration_config.get("certificate", {})
|
|
60
|
+
ssl_config = self.registration_config.get("ssl", {})
|
|
61
|
+
|
|
62
|
+
# FALLBACK: if no explicit registration SSL/certs provided, reuse global SSL config
|
|
63
|
+
if not cert_config and not ssl_config:
|
|
64
|
+
global_ssl = self.config.get("security", {}).get("ssl", {}) or self.config.get("ssl", {})
|
|
65
|
+
if global_ssl:
|
|
66
|
+
# Map global ssl to registration-style configs
|
|
67
|
+
mapped_cert = {}
|
|
68
|
+
if global_ssl.get("cert_file") and global_ssl.get("key_file"):
|
|
69
|
+
mapped_cert = {
|
|
70
|
+
"cert_file": global_ssl.get("cert_file"),
|
|
71
|
+
"key_file": global_ssl.get("key_file"),
|
|
72
|
+
}
|
|
73
|
+
mapped_ssl = {}
|
|
74
|
+
if global_ssl.get("ca_cert"):
|
|
75
|
+
mapped_ssl["ca_cert"] = global_ssl.get("ca_cert")
|
|
76
|
+
if global_ssl.get("verify_client") is not None:
|
|
77
|
+
mapped_ssl["verify_mode"] = (
|
|
78
|
+
"CERT_REQUIRED" if global_ssl.get("verify_client") else "CERT_NONE"
|
|
79
|
+
)
|
|
80
|
+
cert_config = mapped_cert
|
|
81
|
+
ssl_config = mapped_ssl
|
|
82
|
+
|
|
83
|
+
# Use client security manager to create SSL context
|
|
84
|
+
if cert_config or ssl_config:
|
|
85
|
+
ssl_context = self.client_security.create_ssl_context(
|
|
86
|
+
cert_config=cert_config,
|
|
87
|
+
ssl_config=ssl_config
|
|
88
|
+
)
|
|
89
|
+
if ssl_context:
|
|
90
|
+
self.logger.debug("SSL context created successfully for registration")
|
|
91
|
+
return ssl_context
|
|
92
|
+
else:
|
|
93
|
+
self.logger.warning("Failed to create SSL context for registration")
|
|
94
|
+
return None
|
|
95
|
+
else:
|
|
96
|
+
self.logger.debug("No SSL configuration found for registration")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
self.logger.error(f"Error creating SSL context for registration: {e}")
|
|
101
|
+
return None
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Author: Vasiliy Zdanovskiy
|
|
3
|
+
email: vasilyvz@gmail.com
|
|
4
|
+
|
|
5
|
+
Core mTLS Proxy Client.
|
|
6
|
+
|
|
7
|
+
Provides an asynchronous client for communicating with a proxy-like server over
|
|
8
|
+
mutual TLS. Designed to be used by services built on this framework to:
|
|
9
|
+
- perform health/heartbeat checks
|
|
10
|
+
- register themselves with the proxy
|
|
11
|
+
|
|
12
|
+
This client intentionally avoids framework-specific configuration objects and
|
|
13
|
+
accepts explicit parameters for clarity and portability.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import ssl
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any, Dict, Optional, Tuple
|
|
20
|
+
from urllib.parse import urljoin
|
|
21
|
+
|
|
22
|
+
import aiohttp # type: ignore[import]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class RegistrationRequest:
|
|
27
|
+
"""Data payload for registration calls to the proxy server."""
|
|
28
|
+
|
|
29
|
+
server_id: str
|
|
30
|
+
server_name: str
|
|
31
|
+
description: Optional[str] = None
|
|
32
|
+
extra: Optional[Dict[str, Any]] = None
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
35
|
+
payload: Dict[str, Any] = {
|
|
36
|
+
"server_id": self.server_id,
|
|
37
|
+
"server_name": self.server_name,
|
|
38
|
+
}
|
|
39
|
+
if self.description is not None:
|
|
40
|
+
payload["description"] = self.description
|
|
41
|
+
if self.extra:
|
|
42
|
+
payload.update(self.extra)
|
|
43
|
+
return payload
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ProxyClient:
|
|
47
|
+
"""Asynchronous mTLS HTTP client for communicating with a proxy server.
|
|
48
|
+
|
|
49
|
+
Usage:
|
|
50
|
+
async with ProxyClient(
|
|
51
|
+
base_url="https://your-proxy-host:3004",
|
|
52
|
+
ca_cert_path="/path/to/ca.crt",
|
|
53
|
+
client_cert_path="/path/to/client.crt",
|
|
54
|
+
client_key_path="/path/to/client.key",
|
|
55
|
+
) as client:
|
|
56
|
+
status, health = await client.health()
|
|
57
|
+
status, hb = await client.heartbeat()
|
|
58
|
+
status, reg = await client.register(
|
|
59
|
+
RegistrationRequest(...)
|
|
60
|
+
)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
base_url: str,
|
|
66
|
+
*,
|
|
67
|
+
ca_cert_path: str,
|
|
68
|
+
client_cert_path: str,
|
|
69
|
+
client_key_path: str,
|
|
70
|
+
request_timeout_s: float = 5.0,
|
|
71
|
+
min_tls_version: ssl.TLSVersion = ssl.TLSVersion.TLSv1_2,
|
|
72
|
+
verify_mode: ssl.VerifyMode = ssl.CERT_REQUIRED,
|
|
73
|
+
) -> None:
|
|
74
|
+
if not base_url.startswith("http"):
|
|
75
|
+
raise ValueError("base_url must start with http/https")
|
|
76
|
+
self._base_url: str = base_url.rstrip("/")
|
|
77
|
+
self._ca_cert_path: str = ca_cert_path
|
|
78
|
+
self._client_cert_path: str = client_cert_path
|
|
79
|
+
self._client_key_path: str = client_key_path
|
|
80
|
+
self._request_timeout_s: float = request_timeout_s
|
|
81
|
+
self._min_tls_version: ssl.TLSVersion = min_tls_version
|
|
82
|
+
self._verify_mode: ssl.VerifyMode = verify_mode
|
|
83
|
+
|
|
84
|
+
self._ssl_context: Optional[ssl.SSLContext] = None
|
|
85
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
86
|
+
|
|
87
|
+
async def __aenter__(self) -> "ProxyClient":
|
|
88
|
+
await self._ensure_session()
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
92
|
+
await self.close()
|
|
93
|
+
|
|
94
|
+
async def _ensure_session(self) -> None:
|
|
95
|
+
if self._session is not None:
|
|
96
|
+
return
|
|
97
|
+
ssl_context = self._build_ssl_context()
|
|
98
|
+
timeout = aiohttp.ClientTimeout(total=self._request_timeout_s)
|
|
99
|
+
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
|
100
|
+
self._session = aiohttp.ClientSession(
|
|
101
|
+
timeout=timeout,
|
|
102
|
+
connector=connector,
|
|
103
|
+
)
|
|
104
|
+
self._ssl_context = ssl_context
|
|
105
|
+
|
|
106
|
+
async def close(self) -> None:
|
|
107
|
+
if self._session is not None:
|
|
108
|
+
await self._session.close()
|
|
109
|
+
self._session = None
|
|
110
|
+
self._ssl_context = None
|
|
111
|
+
|
|
112
|
+
def _build_ssl_context(self) -> ssl.SSLContext:
|
|
113
|
+
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
|
114
|
+
ctx.minimum_version = self._min_tls_version
|
|
115
|
+
ctx.verify_mode = self._verify_mode
|
|
116
|
+
ctx.load_verify_locations(self._ca_cert_path)
|
|
117
|
+
ctx.load_cert_chain(self._client_cert_path, self._client_key_path)
|
|
118
|
+
return ctx
|
|
119
|
+
|
|
120
|
+
async def _get_json(self, path: str) -> Tuple[int, Dict[str, Any]]:
|
|
121
|
+
await self._ensure_session()
|
|
122
|
+
assert self._session is not None
|
|
123
|
+
url = urljoin(self._base_url + "/", path.lstrip("/"))
|
|
124
|
+
async with self._session.get(url) as resp:
|
|
125
|
+
status = resp.status
|
|
126
|
+
body_text = await resp.text()
|
|
127
|
+
try:
|
|
128
|
+
data: Dict[str, Any] = (
|
|
129
|
+
json.loads(body_text) if body_text else {}
|
|
130
|
+
)
|
|
131
|
+
except json.JSONDecodeError:
|
|
132
|
+
data = {"raw": body_text}
|
|
133
|
+
return status, data
|
|
134
|
+
|
|
135
|
+
async def _post_json(
|
|
136
|
+
self,
|
|
137
|
+
path: str,
|
|
138
|
+
payload: Dict[str, Any],
|
|
139
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
140
|
+
await self._ensure_session()
|
|
141
|
+
assert self._session is not None
|
|
142
|
+
url = urljoin(self._base_url + "/", path.lstrip("/"))
|
|
143
|
+
headers = {"Content-Type": "application/json"}
|
|
144
|
+
async with self._session.post(
|
|
145
|
+
url,
|
|
146
|
+
headers=headers,
|
|
147
|
+
json=payload,
|
|
148
|
+
) as resp:
|
|
149
|
+
status = resp.status
|
|
150
|
+
body_text = await resp.text()
|
|
151
|
+
try:
|
|
152
|
+
data: Dict[str, Any] = (
|
|
153
|
+
json.loads(body_text) if body_text else {}
|
|
154
|
+
)
|
|
155
|
+
except json.JSONDecodeError:
|
|
156
|
+
data = {"raw": body_text}
|
|
157
|
+
return status, data
|
|
158
|
+
|
|
159
|
+
async def health(
|
|
160
|
+
self,
|
|
161
|
+
path: str = "/health",
|
|
162
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
163
|
+
"""Perform a health check against the proxy."""
|
|
164
|
+
return await self._get_json(path)
|
|
165
|
+
|
|
166
|
+
async def heartbeat(
|
|
167
|
+
self,
|
|
168
|
+
path: str = "/heartbeat",
|
|
169
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
170
|
+
"""Perform a heartbeat (liveness) check against the proxy."""
|
|
171
|
+
return await self._get_json(path)
|
|
172
|
+
|
|
173
|
+
async def register(
|
|
174
|
+
self,
|
|
175
|
+
request: RegistrationRequest,
|
|
176
|
+
*,
|
|
177
|
+
path: str = "/register",
|
|
178
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
179
|
+
"""Register the current service on the proxy server."""
|
|
180
|
+
payload = request.to_dict()
|
|
181
|
+
return await self._post_json(
|
|
182
|
+
path,
|
|
183
|
+
payload,
|
|
184
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for proxy registration functionality with security framework integration.
|
|
3
|
+
|
|
4
|
+
This module handles automatic registration and unregistration of the server
|
|
5
|
+
with the MCP proxy server during startup and shutdown, using mcp_security_framework
|
|
6
|
+
for secure connections and authentication.
|
|
7
|
+
|
|
8
|
+
Author: Vasiliy Zdanovskiy
|
|
9
|
+
email: vasilyvz@gmail.com
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Dict, Any, Optional
|
|
13
|
+
|
|
14
|
+
from mcp_proxy_adapter.core.proxy.proxy_registration_manager import (
|
|
15
|
+
ProxyRegistrationManager,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Global registration manager instance
|
|
19
|
+
_registration_manager: Optional[ProxyRegistrationManager] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def initialize_proxy_registration(config: Dict[str, Any]) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Initialize proxy registration with configuration.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
config: Application configuration
|
|
28
|
+
"""
|
|
29
|
+
global _registration_manager
|
|
30
|
+
_registration_manager = ProxyRegistrationManager(config)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def register_with_proxy(server_url: str) -> bool:
|
|
34
|
+
"""
|
|
35
|
+
Register server with proxy.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
server_url: Server URL to register
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if registration successful, False otherwise
|
|
42
|
+
"""
|
|
43
|
+
if not _registration_manager:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
_registration_manager.set_server_url(server_url)
|
|
47
|
+
return await _registration_manager.register()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def unregister_from_proxy() -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Unregister server from proxy.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if unregistration successful, False otherwise
|
|
56
|
+
"""
|
|
57
|
+
if not _registration_manager:
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
return await _registration_manager.unregister()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_proxy_registration_status() -> Dict[str, Any]:
|
|
64
|
+
"""
|
|
65
|
+
Get current proxy registration status.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dictionary with registration status information
|
|
69
|
+
"""
|
|
70
|
+
if not _registration_manager:
|
|
71
|
+
return {
|
|
72
|
+
"enabled": False,
|
|
73
|
+
"registered": False,
|
|
74
|
+
"proxy_url": None,
|
|
75
|
+
"server_url": None,
|
|
76
|
+
"registration_time": None,
|
|
77
|
+
"client_security_available": False,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return _registration_manager.get_registration_status()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Role Utilities
|
|
3
|
+
|
|
4
|
+
This module provides utilities for working with roles extracted from certificates.
|
|
5
|
+
Includes functions for role extraction, comparison, validation, and normalization.
|
|
6
|
+
|
|
7
|
+
Author: MCP Proxy Adapter Team
|
|
8
|
+
Version: 1.0.0
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RoleUtils:
|
|
16
|
+
"""
|
|
17
|
+
Utilities for working with roles from certificates.
|
|
18
|
+
|
|
19
|
+
Provides methods for extracting, comparing, validating, and normalizing roles.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Custom OID for roles in certificates
|
|
23
|
+
ROLE_EXTENSION_OID = "1.3.6.1.4.1.99999.1"
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def validate_single_role(role: str) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
Validate a single role.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
role: Role string to validate
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
True if role is valid, False otherwise
|
|
35
|
+
"""
|
|
36
|
+
if not isinstance(role, str):
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
# Check if role is not empty after trimming
|
|
40
|
+
if not role.strip():
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
# Check for valid characters (alphanumeric, hyphens, underscores)
|
|
44
|
+
valid_chars = set(
|
|
45
|
+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
|
|
46
|
+
)
|
|
47
|
+
role_chars = set(role.lower())
|
|
48
|
+
|
|
49
|
+
if not role_chars.issubset(valid_chars):
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
# Check length (1-50 characters)
|
|
53
|
+
if len(role) < 1 or len(role) > 50:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def normalize_role(role: str) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Normalize role string.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
role: Role string to normalize
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Normalized role string
|
|
68
|
+
"""
|
|
69
|
+
if not role:
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
# Convert to lowercase and trim whitespace
|
|
73
|
+
normalized = role.lower().strip()
|
|
74
|
+
|
|
75
|
+
# Replace multiple spaces with single space
|
|
76
|
+
normalized = " ".join(normalized.split())
|
|
77
|
+
|
|
78
|
+
# Replace spaces with hyphens
|
|
79
|
+
normalized = normalized.replace(" ", "-")
|
|
80
|
+
|
|
81
|
+
return normalized
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def normalize_roles(roles: List[str]) -> List[str]:
|
|
85
|
+
"""
|
|
86
|
+
Normalize list of roles.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
roles: List of roles to normalize
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
List of normalized roles
|
|
93
|
+
"""
|
|
94
|
+
if not roles:
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
normalized = []
|
|
98
|
+
for role in roles:
|
|
99
|
+
normalized_role = RoleUtils.normalize_role(role)
|
|
100
|
+
if normalized_role and normalized_role not in normalized:
|
|
101
|
+
normalized.append(normalized_role)
|
|
102
|
+
|
|
103
|
+
return normalized
|