mcp-proxy-adapter 6.4.47__py3-none-any.whl → 6.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ """
2
+ Simplified configuration builder for MCP Proxy Adapter.
3
+
4
+ Author: Vasiliy Zdanovskiy
5
+ email: vasilyvz@gmail.com
6
+ """
7
+
8
+ import json
9
+ import uuid
10
+ from enum import Enum
11
+ from typing import Dict, List, Optional, Any
12
+
13
+
14
+ class Protocol(Enum):
15
+ """Supported protocols."""
16
+ HTTP = "http"
17
+ HTTPS = "https"
18
+ MTLS = "mtls"
19
+
20
+
21
+ class AuthMethod(Enum):
22
+ """Authentication methods."""
23
+ NONE = "none"
24
+ TOKEN = "token"
25
+ TOKEN_ROLES = "token_roles"
26
+
27
+
28
+ class ConfigBuilder:
29
+ """Simplified configuration builder."""
30
+
31
+ def __init__(self):
32
+ """Initialize the configuration builder."""
33
+ self._reset_to_defaults()
34
+
35
+ def _reset_to_defaults(self):
36
+ """Reset configuration to default values."""
37
+ self.config = {
38
+ "uuid": str(uuid.uuid4()),
39
+ "server": {
40
+ "host": "0.0.0.0",
41
+ "port": 8000,
42
+ "protocol": "http",
43
+ "debug": False,
44
+ "log_level": "INFO"
45
+ },
46
+ "logging": {
47
+ "level": "INFO",
48
+ "file": None,
49
+ "log_dir": "./logs",
50
+ "log_file": "mcp_proxy_adapter.log",
51
+ "max_size": 10,
52
+ "backup_count": 5,
53
+ "console_output": True,
54
+ "json_format": False
55
+ },
56
+ "security": {
57
+ "enabled": False,
58
+ "tokens": {
59
+ "admin": "admin-secret-key",
60
+ "user": "user-secret-key",
61
+ "readonly": "readonly-secret-key"
62
+ },
63
+ "roles": {
64
+ "admin": ["read", "write", "delete", "admin"],
65
+ "user": ["read", "write"],
66
+ "readonly": ["read"]
67
+ },
68
+ "roles_file": None
69
+ },
70
+ "debug": {
71
+ "enabled": False,
72
+ "log_level": "DEBUG",
73
+ "trace_requests": False,
74
+ "trace_responses": False
75
+ }
76
+ }
77
+
78
+ def set_protocol(self, protocol: Protocol, cert_dir: str = "./certs", key_dir: str = "./keys"):
79
+ """Set protocol configuration (HTTP, HTTPS, or mTLS)."""
80
+ self.config["server"]["protocol"] = protocol.value
81
+
82
+ if protocol == Protocol.HTTP:
83
+ # HTTP - no SSL
84
+ pass
85
+
86
+ elif protocol == Protocol.HTTPS:
87
+ # HTTPS - server SSL only
88
+ # SSL configuration will be handled by the server based on protocol
89
+ pass
90
+
91
+ elif protocol == Protocol.MTLS:
92
+ # mTLS - server SSL + client certificates
93
+ # SSL configuration will be handled by the server based on protocol
94
+ pass
95
+
96
+ return self
97
+
98
+ def set_auth(self, auth_method: AuthMethod, api_keys: Optional[Dict[str, str]] = None, roles: Optional[Dict[str, List[str]]] = None):
99
+ """Set authentication configuration."""
100
+ if auth_method == AuthMethod.NONE:
101
+ self.config["security"]["enabled"] = False
102
+ self.config["security"]["tokens"] = {}
103
+ self.config["security"]["roles"] = {}
104
+ self.config["security"]["roles_file"] = None
105
+
106
+ elif auth_method == AuthMethod.TOKEN:
107
+ self.config["security"]["enabled"] = True
108
+ self.config["security"]["tokens"] = api_keys or {
109
+ "admin": "admin-secret-key",
110
+ "user": "user-secret-key"
111
+ }
112
+ self.config["security"]["roles"] = {}
113
+ self.config["security"]["roles_file"] = None
114
+
115
+ elif auth_method == AuthMethod.TOKEN_ROLES:
116
+ self.config["security"]["enabled"] = True
117
+ self.config["security"]["tokens"] = api_keys or {
118
+ "admin": "admin-secret-key",
119
+ "user": "user-secret-key",
120
+ "readonly": "readonly-secret-key"
121
+ }
122
+ self.config["security"]["roles"] = roles or {
123
+ "admin": ["read", "write", "delete", "admin"],
124
+ "user": ["read", "write"],
125
+ "readonly": ["read"]
126
+ }
127
+ self.config["security"]["roles_file"] = "configs/roles.json"
128
+
129
+ return self
130
+
131
+ def set_server(self, host: str = "0.0.0.0", port: int = 8000):
132
+ """Set server configuration."""
133
+ self.config["server"]["host"] = host
134
+ self.config["server"]["port"] = port
135
+ return self
136
+
137
+ def set_roles_file(self, roles_file: str):
138
+ """Set roles file path."""
139
+ self.config["security"]["roles_file"] = roles_file
140
+ return self
141
+
142
+ def build(self) -> Dict[str, Any]:
143
+ """Build and return the configuration."""
144
+ return self.config.copy()
145
+
146
+ def save(self, file_path: str) -> None:
147
+ """Save configuration to file."""
148
+ with open(file_path, 'w', encoding='utf-8') as f:
149
+ json.dump(self.config, f, indent=2, ensure_ascii=False)
150
+
151
+
152
+ class ConfigFactory:
153
+ """Factory for creating common configurations."""
154
+
155
+ @staticmethod
156
+ def create_http_config(port: int = 8000) -> Dict[str, Any]:
157
+ """Create HTTP configuration."""
158
+ return (ConfigBuilder()
159
+ .set_protocol(Protocol.HTTP)
160
+ .set_server(port=port)
161
+ .build())
162
+
163
+ @staticmethod
164
+ def create_http_token_config(port: int = 8001) -> Dict[str, Any]:
165
+ """Create HTTP with token authentication configuration."""
166
+ return (ConfigBuilder()
167
+ .set_protocol(Protocol.HTTP)
168
+ .set_auth(AuthMethod.TOKEN)
169
+ .set_server(port=port)
170
+ .build())
171
+
172
+ @staticmethod
173
+ def create_http_token_roles_config(port: int = 8002) -> Dict[str, Any]:
174
+ """Create HTTP with token and roles configuration."""
175
+ return (ConfigBuilder()
176
+ .set_protocol(Protocol.HTTP)
177
+ .set_auth(AuthMethod.TOKEN_ROLES)
178
+ .set_server(port=port)
179
+ .build())
180
+
181
+ @staticmethod
182
+ def create_https_config(port: int = 8003) -> Dict[str, Any]:
183
+ """Create HTTPS configuration."""
184
+ return (ConfigBuilder()
185
+ .set_protocol(Protocol.HTTPS)
186
+ .set_server(port=port)
187
+ .build())
188
+
189
+ @staticmethod
190
+ def create_https_token_config(port: int = 8004) -> Dict[str, Any]:
191
+ """Create HTTPS with token authentication configuration."""
192
+ return (ConfigBuilder()
193
+ .set_protocol(Protocol.HTTPS)
194
+ .set_auth(AuthMethod.TOKEN)
195
+ .set_server(port=port)
196
+ .build())
197
+
198
+ @staticmethod
199
+ def create_https_token_roles_config(port: int = 8005) -> Dict[str, Any]:
200
+ """Create HTTPS with token and roles configuration."""
201
+ return (ConfigBuilder()
202
+ .set_protocol(Protocol.HTTPS)
203
+ .set_auth(AuthMethod.TOKEN_ROLES)
204
+ .set_server(port=port)
205
+ .build())
206
+
207
+ @staticmethod
208
+ def create_mtls_config(port: int = 8006) -> Dict[str, Any]:
209
+ """Create mTLS configuration."""
210
+ return (ConfigBuilder()
211
+ .set_protocol(Protocol.MTLS)
212
+ .set_server(port=port)
213
+ .build())
214
+
215
+ @staticmethod
216
+ def create_mtls_token_config(port: int = 8007) -> Dict[str, Any]:
217
+ """Create mTLS with token authentication configuration."""
218
+ return (ConfigBuilder()
219
+ .set_protocol(Protocol.MTLS)
220
+ .set_auth(AuthMethod.TOKEN)
221
+ .set_server(port=port)
222
+ .build())
223
+
224
+ @staticmethod
225
+ def create_mtls_token_roles_config(port: int = 8008) -> Dict[str, Any]:
226
+ """Create mTLS with token and roles configuration."""
227
+ return (ConfigBuilder()
228
+ .set_protocol(Protocol.MTLS)
229
+ .set_auth(AuthMethod.TOKEN_ROLES)
230
+ .set_server(port=port)
231
+ .build())
232
+
233
+
234
+ def create_config_from_flags(protocol: str, token: bool = False, roles: bool = False, port: int = 8000) -> Dict[str, Any]:
235
+ """
236
+ Create configuration from command line flags.
237
+
238
+ Args:
239
+ protocol: Protocol type (http, https, mtls)
240
+ token: Enable token authentication
241
+ roles: Enable role-based access control
242
+ port: Server port
243
+
244
+ Returns:
245
+ Configuration dictionary
246
+ """
247
+ protocol_map = {
248
+ "http": Protocol.HTTP,
249
+ "https": Protocol.HTTPS,
250
+ "mtls": Protocol.MTLS
251
+ }
252
+
253
+ if protocol not in protocol_map:
254
+ raise ValueError(f"Unsupported protocol: {protocol}")
255
+
256
+ builder = ConfigBuilder().set_protocol(protocol_map[protocol]).set_server(port=port)
257
+
258
+ if roles:
259
+ builder.set_auth(AuthMethod.TOKEN_ROLES)
260
+ elif token:
261
+ builder.set_auth(AuthMethod.TOKEN)
262
+ else:
263
+ builder.set_auth(AuthMethod.NONE)
264
+
265
+ return builder.build()
266
+
267
+
268
+ if __name__ == "__main__":
269
+ # Example usage
270
+ config = create_config_from_flags("http", token=True, port=8001)
271
+ print(json.dumps(config, indent=2))
@@ -27,9 +27,7 @@ class SecurityTestRunner:
27
27
 
28
28
  def __init__(self):
29
29
  self.project_root = Path(__file__).parent.parent.parent
30
- self.configs_dir = (
31
- self.project_root / "mcp_proxy_adapter" / "examples" / "configs"
32
- )
30
+ self.configs_dir = self.project_root / "configs"
33
31
  self.server_processes = {}
34
32
  self.test_results = []
35
33
 
@@ -137,23 +135,21 @@ class SecurityTestRunner:
137
135
  ]
138
136
  try:
139
137
  # Get remaining config for client setup
140
- auth_enabled = (
141
- config.get("security", {}).get("auth", {}).get("enabled", False)
142
- )
143
- auth_methods = config.get("security", {}).get("auth", {}).get("methods", [])
138
+ auth_enabled = config.get("security", {}).get("enabled", False)
139
+ # For new simplified structure, if security is enabled, we use token auth
140
+ auth_methods = ["api_key"] if auth_enabled else []
144
141
  # Create test client with correct protocol
145
- protocol = (
146
- "https" if config.get("ssl", {}).get("enabled", False) else "http"
147
- )
142
+ server_protocol = config.get("server", {}).get("protocol", "http")
143
+ protocol = "https" if server_protocol in ["https", "mtls"] else "http"
148
144
  client = SecurityTestClient(base_url=f"{protocol}://localhost:{port}")
145
+ print(f"🔍 DEBUG: Created client with URL: {client.base_url}")
149
146
  client.auth_enabled = auth_enabled
150
147
  client.auth_methods = auth_methods
151
- client.api_keys = (
152
- config.get("security", {}).get("auth", {}).get("api_keys", {})
153
- )
154
- client.roles_file = config.get("security", {}).get("permissions", {}).get("roles_file")
148
+ client.api_keys = config.get("security", {}).get("tokens", {})
149
+ client.roles_file = config.get("security", {}).get("roles_file")
150
+ client.roles = config.get("security", {}).get("roles", {})
155
151
  # For mTLS, override SSL context creation and change working directory
156
- if config_name == "mtls":
152
+ if server_protocol == "mtls":
157
153
  client.create_ssl_context = client.create_ssl_context_for_mtls
158
154
  # Ensure mTLS uses certificate auth
159
155
  client.auth_methods = ["certificate"]
@@ -214,10 +210,10 @@ class SecurityTestRunner:
214
210
  result = await client.test_negative_authentication()
215
211
  results.append(result)
216
212
  except Exception as e:
217
- results.append(
213
+ results.append(
218
214
  TestResult(
219
215
  test_name=f"{config_name}_client_error",
220
- server_url=f"http://localhost:{port}",
216
+ server_url=f"{protocol}://localhost:{port}",
221
217
  auth_type="none",
222
218
  success=False,
223
219
  error_message=str(e),
@@ -228,17 +224,68 @@ class SecurityTestRunner:
228
224
  self.stop_server(config_name, process)
229
225
  return results
230
226
 
227
+ def create_variant_from_full_config(self, full_config_path: Path, protocol: str, auth: str, port: int) -> Path:
228
+ """
229
+ Create a variant configuration from full config.
230
+
231
+ Args:
232
+ full_config_path: Path to the full configuration file
233
+ protocol: Protocol type (http, https, mtls)
234
+ auth: Authentication type (none, token, token_roles)
235
+ port: Server port
236
+
237
+ Returns:
238
+ Path to the temporary configuration file
239
+ """
240
+ import tempfile
241
+ import json
242
+
243
+ # Load the full configuration
244
+ with open(full_config_path, 'r') as f:
245
+ full_config = json.load(f)
246
+
247
+ # Create a copy of the full config
248
+ variant_config = full_config.copy()
249
+
250
+ # Set server port and protocol
251
+ variant_config["server"]["port"] = port
252
+ variant_config["server"]["protocol"] = protocol
253
+
254
+ # Apply protocol configuration
255
+ if protocol in variant_config.get("protocol_variants", {}):
256
+ protocol_config = variant_config["protocol_variants"][protocol]
257
+ variant_config["server"].update(protocol_config["server"])
258
+
259
+ # Apply authentication configuration
260
+ if auth in variant_config.get("auth_variants", {}):
261
+ auth_config = variant_config["auth_variants"][auth]
262
+ variant_config["security"].update(auth_config["security"])
263
+
264
+ # Remove the helper sections
265
+ variant_config.pop("protocol_variants", None)
266
+ variant_config.pop("auth_variants", None)
267
+
268
+ # Create temporary config file
269
+ temp_dir = tempfile.mkdtemp(prefix="full_config_test_")
270
+ config_name = f"{protocol}_{auth}.json"
271
+ config_path = Path(temp_dir) / config_name
272
+
273
+ with open(config_path, 'w') as f:
274
+ json.dump(variant_config, f, indent=2, ensure_ascii=False)
275
+
276
+ return config_path
277
+
231
278
  async def run_all_tests(self):
232
279
  """Run all security tests."""
233
280
  print("🔒 Starting Security Testing Suite")
234
281
  print("=" * 50)
235
282
  # Test configurations
236
283
  configs = [
237
- ("basic_http", "http_simple.json"),
238
- ("http_token", "http_token.json"),
239
- ("https", "https_simple.json"),
240
- ("https_token", "https_token.json"),
241
- ("mtls", "mtls_simple.json"),
284
+ ("basic_http", "http.json"),
285
+ ("http_token", "http_token_roles.json"),
286
+ ("https", "https.json"),
287
+ ("https_token", "https_token_roles.json"),
288
+ ("mtls", "mtls.json"),
242
289
  ]
243
290
  total_tests = 0
244
291
  passed_tests = 0
@@ -280,12 +327,128 @@ class SecurityTestRunner:
280
327
  print(f" Error: {result.error_message}")
281
328
  return passed_tests == total_tests
282
329
 
330
+ async def run_full_config_tests(self, full_config_path: str):
331
+ """Run tests using full configuration with all variants."""
332
+ print("🚀 Full Configuration Variants Testing")
333
+ print("=" * 60)
334
+ print(f"📁 Using full config: {full_config_path}")
335
+
336
+ full_config_file = Path(full_config_path)
337
+ if not full_config_file.exists():
338
+ print(f"❌ Full configuration file not found: {full_config_path}")
339
+ return False
340
+
341
+ # Define all combinations to test
342
+ variants = [
343
+ # HTTP variants
344
+ ("http", "none", 20000),
345
+ ("http", "token", 20001),
346
+ ("http", "token_roles", 20002),
347
+
348
+ # HTTPS variants
349
+ ("https", "none", 20003),
350
+ ("https", "token", 20004),
351
+ ("https", "token_roles", 20005),
352
+
353
+ # mTLS variants
354
+ ("mtls", "none", 20006),
355
+ ("mtls", "token", 20007),
356
+ ("mtls", "token_roles", 20008),
357
+ ]
358
+
359
+ total_tests = 0
360
+ passed_tests = 0
361
+ all_results = []
362
+
363
+ for protocol, auth, port in variants:
364
+ print(f"\n{'='*60}")
365
+ print(f"🧪 Testing {protocol.upper()} with {auth.upper()} authentication")
366
+ print(f"{'='*60}")
367
+
368
+ # Create variant configuration
369
+ config_path = self.create_variant_from_full_config(full_config_file, protocol, auth, port)
370
+
371
+ # Test the variant
372
+ config_name = f"{protocol}_{auth}"
373
+ results = await self.test_server(config_name, config_path)
374
+
375
+ # Count results
376
+ for result in results:
377
+ total_tests += 1
378
+ if result.success:
379
+ passed_tests += 1
380
+ print(f"✅ {result.test_name}: PASS")
381
+ else:
382
+ print(f"❌ {result.test_name}: FAIL - {result.error_message}")
383
+
384
+ all_results.extend(results)
385
+
386
+ # Clean up temporary config
387
+ import shutil
388
+ shutil.rmtree(config_path.parent)
389
+
390
+ # Print final summary
391
+ print(f"\n{'='*60}")
392
+ print("📊 FULL CONFIG TEST SUMMARY")
393
+ print(f"{'='*60}")
394
+ print(f"Total tests: {total_tests}")
395
+ print(f"Passed: {passed_tests}")
396
+ print(f"Failed: {total_tests - passed_tests}")
397
+ print(f"Success rate: {(passed_tests/total_tests)*100:.1f}%")
398
+
399
+ if total_tests - passed_tests > 0:
400
+ print(f"\n❌ Failed tests:")
401
+ for result in all_results:
402
+ if not result.success:
403
+ print(f" • {result.test_name}: {result.error_message}")
404
+
405
+ return passed_tests == total_tests
406
+
407
+ def print_summary(self):
408
+ """Print test summary."""
409
+ if not self.test_results:
410
+ print("📊 No test results to display")
411
+ return
412
+
413
+ total_tests = len(self.test_results)
414
+ passed_tests = sum(1 for result in self.test_results if result.success)
415
+
416
+ print("\n" + "=" * 50)
417
+ print("📊 TEST SUMMARY")
418
+ print("=" * 50)
419
+ print(f"Total tests: {total_tests}")
420
+ print(f"Passed: {passed_tests}")
421
+ print(f"Failed: {total_tests - passed_tests}")
422
+ print(f"Success rate: {(passed_tests/total_tests)*100:.1f}%")
423
+
424
+ if total_tests - passed_tests > 0:
425
+ print(f"\n📋 DETAILED RESULTS")
426
+ print("-" * 30)
427
+ for result in self.test_results:
428
+ status = "✅ PASS" if result.success else "❌ FAIL"
429
+ print(f"{status} {result.test_name}")
430
+ if not result.success and result.error_message:
431
+ print(f" Error: {result.error_message}")
432
+
283
433
 
284
434
  async def main():
285
435
  """Main function."""
436
+ import argparse
437
+
438
+ parser = argparse.ArgumentParser(description="Security Testing Suite for MCP Proxy Adapter")
439
+ parser.add_argument("--full-config", help="Path to full configuration file for variant testing")
440
+ parser.add_argument("--verbose", action="store_true", help="Enable verbose output")
441
+
442
+ args = parser.parse_args()
443
+
286
444
  runner = SecurityTestRunner()
287
445
  try:
288
- success = await runner.run_all_tests()
446
+ if args.full_config:
447
+ # Test full configuration variants
448
+ success = await runner.run_full_config_tests(args.full_config)
449
+ else:
450
+ # Run standard tests
451
+ success = await runner.run_all_tests()
289
452
  sys.exit(0 if success else 1)
290
453
  except KeyboardInterrupt:
291
454
  print("\n⚠️ Testing interrupted by user")
@@ -83,6 +83,7 @@ class SecurityTestClient:
83
83
  self.auth_methods = []
84
84
  self.api_keys = {}
85
85
  self.roles_file = None
86
+ self.roles = {}
86
87
 
87
88
  if self._security_available:
88
89
  # For testing purposes, we don't initialize SecurityManager
@@ -99,7 +100,7 @@ class SecurityTestClient:
99
100
  self.test_tokens = {
100
101
  "admin": "admin-secret-key",
101
102
  "user": "user-secret-key",
102
- "readonly": "readonly-token-123",
103
+ "readonly": "readonly-secret-key",
103
104
  "guest": "guest-token-123",
104
105
  "proxy": "proxy-token-123",
105
106
  "invalid": "invalid-token-999",
@@ -123,9 +124,11 @@ class SecurityTestClient:
123
124
  def create_ssl_context_for_mtls(self) -> ssl.SSLContext:
124
125
  """Create SSL context for mTLS connections."""
125
126
  # 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"
127
+ # Paths are relative to project root (../../ from examples directory)
128
+ # Use admin certificates to match server configuration
129
+ cert_file = "../../certs/admin.crt"
130
+ key_file = "../../certs/client_admin.key"
131
+ ca_cert_file = "../../certs/localhost_server.crt"
129
132
 
130
133
  # CRITICAL: For mTLS, certificates are REQUIRED
131
134
  if not os.path.exists(cert_file):
@@ -152,7 +155,13 @@ class SecurityTestClient:
152
155
  timeout = ClientTimeout(total=30)
153
156
  # Create SSL context only for HTTPS URLs
154
157
  if self.base_url.startswith('https://'):
155
- ssl_context = self.create_ssl_context()
158
+ # Check if this is mTLS (ports 20006, 20007, 20008 are mTLS test ports)
159
+ if any(port in self.base_url for port in ['20006', '20007', '20008']):
160
+ # Use mTLS context with client certificates
161
+ ssl_context = self.create_ssl_context_for_mtls()
162
+ else:
163
+ # Use regular HTTPS context
164
+ ssl_context = self.create_ssl_context()
156
165
  connector = TCPConnector(ssl=ssl_context)
157
166
  else:
158
167
  # For HTTP URLs, use default connector without SSL
@@ -209,6 +218,11 @@ class SecurityTestClient:
209
218
  # Provide both common header styles to maximize compatibility
210
219
  headers["X-API-Key"] = token
211
220
  headers["Authorization"] = f"Bearer {token}"
221
+
222
+ # Add role header if provided
223
+ role = kwargs.get("role")
224
+ if role:
225
+ headers["X-Role"] = role
212
226
  elif auth_type == "basic":
213
227
  username = kwargs.get("username")
214
228
  password = kwargs.get("password")
@@ -983,7 +997,7 @@ class SecurityTestClient:
983
997
 
984
998
  async def test_role_based_access(self, server_url: str, auth_type: str, role: str = "admin") -> TestResult:
985
999
  """Test role-based access control."""
986
- if not self.roles_file:
1000
+ if not self.roles_file and not self.roles:
987
1001
  return TestResult(
988
1002
  test_name="Role-Based Access Test",
989
1003
  server_url=server_url,
@@ -1001,7 +1015,7 @@ class SecurityTestClient:
1001
1015
 
1002
1016
  async def test_role_permissions(self, server_url: str, auth_type: str, role: str = "admin", action: str = "read") -> TestResult:
1003
1017
  """Test role permissions."""
1004
- if not self.roles_file:
1018
+ if not self.roles_file and not self.roles:
1005
1019
  return TestResult(
1006
1020
  test_name="Role Permissions Test",
1007
1021
  server_url=server_url,