mcp-proxy-adapter 6.4.44__py3-none-any.whl → 6.4.45__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.
Files changed (24) hide show
  1. mcp_proxy_adapter/core/proxy_registration.py +100 -68
  2. mcp_proxy_adapter/examples/bugfix_certificate_config.py +284 -0
  3. mcp_proxy_adapter/examples/cert_manager_bugfix.py +203 -0
  4. mcp_proxy_adapter/examples/config_builder.py +574 -0
  5. mcp_proxy_adapter/examples/config_cli.py +283 -0
  6. mcp_proxy_adapter/examples/create_test_configs.py +169 -266
  7. mcp_proxy_adapter/examples/generate_certificates_bugfix.py +374 -0
  8. mcp_proxy_adapter/examples/generate_certificates_cli.py +406 -0
  9. mcp_proxy_adapter/examples/generate_certificates_fixed.py +313 -0
  10. mcp_proxy_adapter/examples/generate_certificates_framework.py +366 -0
  11. mcp_proxy_adapter/examples/generate_certificates_openssl.py +391 -0
  12. mcp_proxy_adapter/examples/required_certificates.py +210 -0
  13. mcp_proxy_adapter/examples/run_full_test_suite.py +117 -13
  14. mcp_proxy_adapter/examples/run_security_tests_fixed.py +42 -26
  15. mcp_proxy_adapter/examples/security_test_client.py +332 -85
  16. mcp_proxy_adapter/examples/test_config_builder.py +450 -0
  17. mcp_proxy_adapter/examples/update_config_certificates.py +136 -0
  18. mcp_proxy_adapter/version.py +1 -1
  19. {mcp_proxy_adapter-6.4.44.dist-info → mcp_proxy_adapter-6.4.45.dist-info}/METADATA +81 -1
  20. {mcp_proxy_adapter-6.4.44.dist-info → mcp_proxy_adapter-6.4.45.dist-info}/RECORD +23 -11
  21. mcp_proxy_adapter-6.4.45.dist-info/entry_points.txt +12 -0
  22. mcp_proxy_adapter-6.4.44.dist-info/entry_points.txt +0 -2
  23. {mcp_proxy_adapter-6.4.44.dist-info → mcp_proxy_adapter-6.4.45.dist-info}/WHEEL +0 -0
  24. {mcp_proxy_adapter-6.4.44.dist-info → mcp_proxy_adapter-6.4.45.dist-info}/top_level.txt +0 -0
@@ -30,14 +30,16 @@ sys.path.insert(0, str(current_dir))
30
30
 
31
31
  # Import mcp_security_framework components
32
32
  try:
33
- from mcp_security_framework import SecurityManager, SSLConfig
33
+ from mcp_security_framework import SecurityManager, SecurityConfig, AuthConfig, PermissionConfig, SSLConfig
34
34
  from mcp_security_framework.schemas.config import SSLConfig as SSLConfigSchema
35
35
 
36
36
  _MCP_SECURITY_AVAILABLE = True
37
37
  print("✅ mcp_security_framework available")
38
- except ImportError:
39
- _MCP_SECURITY_AVAILABLE = False
40
- print("⚠️ mcp_security_framework not available, falling back to standard SSL")
38
+ except ImportError as e:
39
+ print(f"❌ CRITICAL ERROR: mcp_security_framework is required but not available!")
40
+ print(f" Import error: {e}")
41
+ print("❌ Please install mcp_security_framework: pip install mcp_security_framework")
42
+ raise ImportError("mcp_security_framework is required for security tests") from e
41
43
 
42
44
  # Import cryptography components
43
45
  try:
@@ -75,23 +77,19 @@ class SecurityTestClient:
75
77
  self.cert_manager = None
76
78
  self._security_available = _MCP_SECURITY_AVAILABLE
77
79
  self._crypto_available = _CRYPTOGRAPHY_AVAILABLE
80
+
81
+ # Authentication configuration
82
+ self.auth_enabled = False
83
+ self.auth_methods = []
84
+ self.api_keys = {}
85
+ self.roles_file = None
78
86
 
79
87
  if self._security_available:
80
- try:
81
- # Initialize SSL manager with default config
82
- ssl_config = SSLConfig(
83
- enabled=True,
84
- cert_file=None,
85
- key_file=None,
86
- ca_cert_file=None,
87
- verify_mode="CERT_NONE", # For testing
88
- min_tls_version="TLSv1.2",
89
- )
90
- self.ssl_manager = SSLManager(ssl_config)
91
- print("✅ SSL Manager initialized with mcp_security_framework")
92
- except Exception as e:
93
- print(f"⚠️ Failed to initialize SSL Manager: {e}")
94
- self._security_available = False
88
+ # For testing purposes, we don't initialize SecurityManager
89
+ # because we're testing server configurations, not the framework itself
90
+ # SecurityManager will be used only when actually needed by the server
91
+ self.ssl_manager = None
92
+ print("✅ mcp_security_framework available for testing")
95
93
 
96
94
  if not self._security_available:
97
95
  print("ℹ️ Using standard SSL library for testing")
@@ -114,7 +112,7 @@ class SecurityTestClient:
114
112
  },
115
113
  "user": {
116
114
  "cert": "certs/user_cert.pem",
117
- "key": "certs/user_key.pem",
115
+ "key": "keys/user_key.pem",
118
116
  },
119
117
  "readonly": {
120
118
  "cert": "certs/readonly_cert.pem",
@@ -122,52 +120,48 @@ class SecurityTestClient:
122
120
  },
123
121
  }
124
122
 
125
- async def __aenter__(self):
126
- """Async context manager entry."""
127
- timeout = ClientTimeout(total=30)
128
- # Create SSL context for HTTPS connections
129
- ssl_context = self.create_ssl_context()
130
- connector = TCPConnector(ssl=ssl_context)
131
- self.session = ClientSession(timeout=timeout, connector=connector)
132
- return self
133
-
134
123
  def create_ssl_context_for_mtls(self) -> ssl.SSLContext:
135
124
  """Create SSL context for mTLS connections."""
136
- if self.ssl_manager and self._security_available:
137
- try:
138
- # Use mcp_security_framework for mTLS
139
- cert_file = "./certs/user_cert.pem"
140
- key_file = "./certs/user_key.pem"
141
- ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
142
-
143
- return self.ssl_manager.create_client_context(
144
- ca_cert_file=ca_cert_file if os.path.exists(ca_cert_file) else None,
145
- client_cert_file=cert_file if os.path.exists(cert_file) else None,
146
- client_key_file=key_file if os.path.exists(key_file) else None,
147
- verify_mode="CERT_NONE", # For testing
148
- min_version="TLSv1.2",
149
- )
150
- except Exception as e:
151
- print(
152
- f"⚠️ Failed to create mTLS context with mcp_security_framework: {e}"
153
- )
154
- print("ℹ️ Falling back to standard SSL")
125
+ # For mTLS, we need client certificates - check if they exist
126
+ cert_file = "./certs/user_cert.pem"
127
+ key_file = "./keys/user_key.pem"
128
+ ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
129
+
130
+ # CRITICAL: For mTLS, certificates are REQUIRED
131
+ if not os.path.exists(cert_file):
132
+ raise FileNotFoundError(f"CRITICAL ERROR: mTLS client certificate not found: {cert_file}")
133
+ if not os.path.exists(key_file):
134
+ raise FileNotFoundError(f"CRITICAL ERROR: mTLS client key not found: {key_file}")
135
+
136
+ # For testing, we use standard SSL library
137
+ # SecurityManager is not initialized for testing purposes
155
138
 
156
- # Fallback to standard SSL
139
+ # Use standard SSL library for testing
157
140
  ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
158
141
  # For mTLS testing - client needs to present certificate to server
159
142
  ssl_context.check_hostname = False
160
143
  ssl_context.verify_mode = ssl.CERT_NONE # Don't verify server cert for testing
161
- # Load client certificate and key for mTLS
162
- cert_file = "./certs/user_cert.pem"
163
- key_file = "./certs/user_key.pem"
164
- ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
165
- if os.path.exists(cert_file) and os.path.exists(key_file):
166
- ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file)
144
+ # Load client certificate and key for mTLS (we already checked they exist)
145
+ ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file)
167
146
  if os.path.exists(ca_cert_file):
168
147
  ssl_context.load_verify_locations(cafile=ca_cert_file)
169
148
  return ssl_context
170
149
 
150
+ async def __aenter__(self):
151
+ """Async context manager entry."""
152
+ timeout = ClientTimeout(total=30)
153
+ # Create SSL context only for HTTPS URLs
154
+ if self.base_url.startswith('https://'):
155
+ ssl_context = self.create_ssl_context()
156
+ connector = TCPConnector(ssl=ssl_context)
157
+ else:
158
+ # For HTTP URLs, use default connector without SSL
159
+ connector = TCPConnector()
160
+
161
+ # Create session
162
+ self.session = ClientSession(timeout=timeout, connector=connector)
163
+ return self
164
+
171
165
  async def __aexit__(self, exc_type, exc_val, exc_tb):
172
166
  """Async context manager exit."""
173
167
  if self.session:
@@ -180,31 +174,18 @@ class SecurityTestClient:
180
174
  ca_cert_file: Optional[str] = None,
181
175
  ) -> ssl.SSLContext:
182
176
  """Create SSL context for client."""
183
- if self.ssl_manager and self._security_available:
184
- try:
185
- # Use mcp_security_framework for SSL context creation
186
- return self.ssl_manager.create_client_context(
187
- ca_cert_file=(
188
- ca_cert_file
189
- if ca_cert_file and os.path.exists(ca_cert_file)
190
- else None
191
- ),
192
- client_cert_file=(
193
- cert_file if cert_file and os.path.exists(cert_file) else None
194
- ),
195
- client_key_file=(
196
- key_file if key_file and os.path.exists(key_file) else None
197
- ),
198
- verify_mode="CERT_NONE", # For testing
199
- min_version="TLSv1.2",
200
- )
201
- except Exception as e:
202
- print(
203
- f"⚠️ Failed to create SSL context with mcp_security_framework: {e}"
204
- )
205
- print("ℹ️ Falling back to standard SSL")
177
+ # If certificates are provided, they must exist
178
+ if cert_file and not os.path.exists(cert_file):
179
+ raise FileNotFoundError(f"CRITICAL ERROR: SSL certificate not found: {cert_file}")
180
+ if key_file and not os.path.exists(key_file):
181
+ raise FileNotFoundError(f"CRITICAL ERROR: SSL key not found: {key_file}")
182
+ if ca_cert_file and not os.path.exists(ca_cert_file):
183
+ raise FileNotFoundError(f"CRITICAL ERROR: SSL CA certificate not found: {ca_cert_file}")
184
+
185
+ # For testing, we use standard SSL library
186
+ # SecurityManager is not initialized for testing purposes
206
187
 
207
- # Fallback to standard SSL
188
+ # Use standard SSL library for testing
208
189
  ssl_context = ssl.create_default_context()
209
190
  # For testing with self-signed certificates
210
191
  ssl_context.check_hostname = False
@@ -396,9 +377,9 @@ class SecurityTestClient:
396
377
  async def test_authentication(self) -> TestResult:
397
378
  """Test authentication."""
398
379
  if "api_key" in self.auth_methods:
399
- # Use first available API key
400
- api_key = next(iter(self.api_keys.keys()), "test-token-123")
401
- return await self.test_echo_command(self.base_url, "api_key", token=api_key)
380
+ # Use admin API key value, not the key name
381
+ api_key_value = self.api_keys.get("admin", "admin-secret-key")
382
+ return await self.test_echo_command(self.base_url, "api_key", token=api_key_value)
402
383
  elif "certificate" in self.auth_methods:
403
384
  # For certificate auth, test with client certificate
404
385
  return await self.test_echo_command(self.base_url, "certificate")
@@ -413,9 +394,78 @@ class SecurityTestClient:
413
394
 
414
395
  async def test_negative_authentication(self) -> TestResult:
415
396
  """Test negative authentication (should fail)."""
416
- return await self.test_echo_command(
417
- self.base_url, "api_key", token="invalid-token"
418
- )
397
+ start_time = time.time()
398
+ test_name = "Negative Authentication Test"
399
+ try:
400
+ headers = self.create_auth_headers("api_key", token="invalid-token")
401
+ data = {
402
+ "jsonrpc": "2.0",
403
+ "method": "echo",
404
+ "params": {"message": "Should fail with invalid token"},
405
+ "id": 1,
406
+ }
407
+ async with self.session.post(
408
+ f"{self.base_url}/cmd", headers=headers, json=data
409
+ ) as response:
410
+ duration = time.time() - start_time
411
+
412
+ # Check if API key authentication is enabled
413
+ api_key_auth_enabled = self.auth_enabled and "api_key" in self.auth_methods
414
+
415
+ if api_key_auth_enabled:
416
+ # For configurations with API key auth, 401 is expected (success)
417
+ if response.status == 401:
418
+ return TestResult(
419
+ test_name=test_name,
420
+ server_url=self.base_url,
421
+ auth_type="api_key",
422
+ success=True,
423
+ status_code=response.status,
424
+ response_data={"expected": "authentication_failure"},
425
+ duration=duration,
426
+ )
427
+ else:
428
+ return TestResult(
429
+ test_name=test_name,
430
+ server_url=self.base_url,
431
+ auth_type="api_key",
432
+ success=False,
433
+ status_code=response.status,
434
+ error_message=f"Expected 401 Unauthorized, got {response.status}",
435
+ duration=duration,
436
+ )
437
+ else:
438
+ # For configurations without API key auth, 200 is expected (success)
439
+ if response.status == 200:
440
+ return TestResult(
441
+ test_name=test_name,
442
+ server_url=self.base_url,
443
+ auth_type="api_key",
444
+ success=True,
445
+ status_code=response.status,
446
+ response_data={"expected": "no_auth_required"},
447
+ duration=duration,
448
+ )
449
+ else:
450
+ return TestResult(
451
+ test_name=test_name,
452
+ server_url=self.base_url,
453
+ auth_type="api_key",
454
+ success=False,
455
+ status_code=response.status,
456
+ error_message=f"Expected 200 OK (no auth required), got {response.status}",
457
+ duration=duration,
458
+ )
459
+ except Exception as e:
460
+ duration = time.time() - start_time
461
+ return TestResult(
462
+ test_name=test_name,
463
+ server_url=self.base_url,
464
+ auth_type="api_key",
465
+ success=False,
466
+ error_message=f"Negative auth test error: {str(e)}",
467
+ duration=duration,
468
+ )
419
469
 
420
470
  async def test_no_auth_required(self) -> TestResult:
421
471
  """Test that no authentication is required."""
@@ -821,6 +871,203 @@ class SecurityTestClient:
821
871
  print(f" - {result.test_name} ({result.server_url})")
822
872
 
823
873
 
874
+ async def test_health(self) -> TestResult:
875
+ """Test health check endpoint."""
876
+ return await self.test_health_check(self.base_url, "none")
877
+
878
+ async def test_command_execution(self) -> TestResult:
879
+ """Test command execution."""
880
+ if self.auth_enabled and "api_key" in self.auth_methods:
881
+ # Use admin API key value, not the key name
882
+ api_key_value = self.api_keys.get("admin", "admin-secret-key")
883
+ return await self.test_echo_command(self.base_url, "api_key", token=api_key_value)
884
+ else:
885
+ return await self.test_echo_command(self.base_url, "none")
886
+
887
+ async def test_authentication(self) -> TestResult:
888
+ """Test authentication."""
889
+ if "api_key" in self.auth_methods:
890
+ # Use admin API key value, not the key name
891
+ api_key_value = self.api_keys.get("admin", "admin-secret-key")
892
+ return await self.test_echo_command(self.base_url, "api_key", token=api_key_value)
893
+ elif "certificate" in self.auth_methods:
894
+ # For certificate auth, test with client certificate
895
+ return await self.test_echo_command(self.base_url, "certificate")
896
+ else:
897
+ return TestResult(
898
+ test_name="Authentication Test",
899
+ server_url=self.base_url,
900
+ auth_type="none",
901
+ success=False,
902
+ error_message="No authentication method available",
903
+ )
904
+
905
+ async def test_negative_authentication(self) -> TestResult:
906
+ """Test negative authentication (should fail)."""
907
+ start_time = time.time()
908
+ test_name = "Negative Authentication Test"
909
+ try:
910
+ headers = self.create_auth_headers("api_key", token="invalid-token")
911
+ data = {
912
+ "jsonrpc": "2.0",
913
+ "method": "echo",
914
+ "params": {"message": "Should fail with invalid token"},
915
+ "id": 1,
916
+ }
917
+ async with self.session.post(
918
+ f"{self.base_url}/cmd", headers=headers, json=data
919
+ ) as response:
920
+ duration = time.time() - start_time
921
+
922
+ # Check if API key authentication is enabled
923
+ api_key_auth_enabled = self.auth_enabled and "api_key" in self.auth_methods
924
+
925
+ if api_key_auth_enabled:
926
+ # For configurations with API key auth, 401 is expected (success)
927
+ if response.status == 401:
928
+ return TestResult(
929
+ test_name=test_name,
930
+ server_url=self.base_url,
931
+ auth_type="api_key",
932
+ success=True,
933
+ status_code=response.status,
934
+ response_data={"expected": "authentication_failure"},
935
+ duration=duration,
936
+ )
937
+ else:
938
+ return TestResult(
939
+ test_name=test_name,
940
+ server_url=self.base_url,
941
+ auth_type="api_key",
942
+ success=False,
943
+ status_code=response.status,
944
+ error_message=f"Expected 401 Unauthorized, got {response.status}",
945
+ duration=duration,
946
+ )
947
+ else:
948
+ # For configurations without API key auth, 200 is expected (success)
949
+ if response.status == 200:
950
+ return TestResult(
951
+ test_name=test_name,
952
+ server_url=self.base_url,
953
+ auth_type="api_key",
954
+ success=True,
955
+ status_code=response.status,
956
+ response_data={"expected": "no_auth_required"},
957
+ duration=duration,
958
+ )
959
+ else:
960
+ return TestResult(
961
+ test_name=test_name,
962
+ server_url=self.base_url,
963
+ auth_type="api_key",
964
+ success=False,
965
+ status_code=response.status,
966
+ error_message=f"Expected 200 OK (no auth required), got {response.status}",
967
+ duration=duration,
968
+ )
969
+ except Exception as e:
970
+ duration = time.time() - start_time
971
+ return TestResult(
972
+ test_name=test_name,
973
+ server_url=self.base_url,
974
+ auth_type="api_key",
975
+ success=False,
976
+ error_message=f"Negative auth test error: {str(e)}",
977
+ duration=duration,
978
+ )
979
+
980
+ async def test_no_auth_required(self) -> TestResult:
981
+ """Test that no authentication is required."""
982
+ return await self.test_echo_command(self.base_url, "none")
983
+
984
+ async def test_role_based_access(self, server_url: str, auth_type: str, role: str = "admin") -> TestResult:
985
+ """Test role-based access control."""
986
+ if not self.roles_file:
987
+ return TestResult(
988
+ test_name="Role-Based Access Test",
989
+ server_url=server_url,
990
+ auth_type=auth_type,
991
+ success=False,
992
+ error_message="Role-based access error: role is required for role-based access test",
993
+ )
994
+
995
+ # Use admin role for testing
996
+ if auth_type == "api_key":
997
+ api_key_value = self.api_keys.get("admin", "admin-secret-key")
998
+ return await self.test_echo_command(server_url, auth_type, token=api_key_value, role=role)
999
+ else:
1000
+ return await self.test_echo_command(server_url, auth_type, role=role)
1001
+
1002
+ async def test_role_permissions(self, server_url: str, auth_type: str, role: str = "admin", action: str = "read") -> TestResult:
1003
+ """Test role permissions."""
1004
+ if not self.roles_file:
1005
+ return TestResult(
1006
+ test_name="Role Permissions Test",
1007
+ server_url=server_url,
1008
+ auth_type=auth_type,
1009
+ success=False,
1010
+ error_message="Role permissions test error: role is required for role permissions test",
1011
+ )
1012
+
1013
+ # Test with admin role
1014
+ if auth_type == "api_key":
1015
+ api_key_value = self.api_keys.get("admin", "admin-secret-key")
1016
+ return await self.test_echo_command(server_url, auth_type, token=api_key_value, role=role)
1017
+ else:
1018
+ return await self.test_echo_command(server_url, auth_type, role=role)
1019
+
1020
+ async def test_multiple_roles(self, server_url: str, auth_type: str) -> TestResult:
1021
+ """Test multiple roles."""
1022
+ # Test with readonly role (should have read access)
1023
+ if auth_type == "api_key":
1024
+ api_key_value = self.api_keys.get("readonly", "readonly-token-123")
1025
+ result = await self.test_echo_command(server_url, auth_type, token=api_key_value, role="readonly")
1026
+ if result.success:
1027
+ return TestResult(
1028
+ test_name="Multiple Roles Test",
1029
+ server_url=server_url,
1030
+ auth_type=auth_type,
1031
+ success=True,
1032
+ response_data={"message": "Readonly role correctly has read access"},
1033
+ )
1034
+ else:
1035
+ return TestResult(
1036
+ test_name="Multiple Roles Test",
1037
+ server_url=server_url,
1038
+ auth_type=auth_type,
1039
+ success=False,
1040
+ error_message="Readonly role incorrectly denied read access",
1041
+ )
1042
+ elif auth_type == "certificate":
1043
+ # For certificate auth, test with user certificate (should have read access)
1044
+ result = await self.test_echo_command(server_url, auth_type, role="user")
1045
+ if result.success:
1046
+ return TestResult(
1047
+ test_name="Multiple Roles Test",
1048
+ server_url=server_url,
1049
+ auth_type=auth_type,
1050
+ success=True,
1051
+ response_data={"message": "User certificate correctly has read access"},
1052
+ )
1053
+ else:
1054
+ return TestResult(
1055
+ test_name="Multiple Roles Test",
1056
+ server_url=server_url,
1057
+ auth_type=auth_type,
1058
+ success=False,
1059
+ error_message="User certificate incorrectly denied read access",
1060
+ )
1061
+ else:
1062
+ return TestResult(
1063
+ test_name="Multiple Roles Test",
1064
+ server_url=server_url,
1065
+ auth_type=auth_type,
1066
+ success=False,
1067
+ error_message="Multiple roles test not implemented for this auth type",
1068
+ )
1069
+
1070
+
824
1071
  async def main():
825
1072
  """Main function."""
826
1073
  import argparse