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.
@@ -2,6 +2,7 @@
2
2
  Module with API request handlers.
3
3
  """
4
4
 
5
+ import asyncio
5
6
  import json
6
7
  import time
7
8
  from typing import Any, Dict, List, Optional, Union
@@ -75,7 +76,16 @@ async def execute_command(
75
76
  "permissions": getattr(request.state, "user_permissions", ["read"]),
76
77
  }
77
78
 
78
- result = await command_class.run(**params, context=context)
79
+ # Add timeout to prevent hanging commands
80
+ try:
81
+ result = await asyncio.wait_for(
82
+ command_class.run(**params, context=context),
83
+ timeout=10.0 # 10 seconds timeout
84
+ )
85
+ except asyncio.TimeoutError:
86
+ execution_time = time.time() - start_time
87
+ log.error(f"⏰ Command '{command_name}' timed out after {execution_time:.3f} sec")
88
+ raise InternalError(f"Command execution timed out after 10 seconds")
79
89
 
80
90
  execution_time = time.time() - start_time
81
91
 
@@ -136,7 +146,7 @@ async def handle_json_rpc(
136
146
  request: Optional[Request] = None,
137
147
  ) -> Dict[str, Any]:
138
148
  """
139
- Handles JSON-RPC request.
149
+ Handles JSON-RPC request with support for both standard JSON-RPC and simplified formats.
140
150
 
141
151
  Args:
142
152
  request_data: JSON-RPC request data.
@@ -148,40 +158,66 @@ async def handle_json_rpc(
148
158
  # Create request logger if request_id is provided
149
159
  log = RequestLogger(__name__, request_id) if request_id else logger
150
160
 
151
- # Check JSON-RPC version
152
- if request_data.get("jsonrpc") != "2.0":
153
- return _create_error_response(
154
- InvalidRequestError("Invalid Request. Expected jsonrpc: 2.0"),
155
- request_data.get("id"),
156
- )
157
-
158
- # Get method and parameters
159
- method = request_data.get("method")
160
- params = request_data.get("params", {})
161
- json_rpc_id = request_data.get("id")
162
-
163
- if not method:
164
- return _create_error_response(
165
- InvalidRequestError("Invalid Request. Method is required"), json_rpc_id
166
- )
161
+ # Support both standard JSON-RPC and simplified formats
162
+ method = None
163
+ params = {}
164
+ json_rpc_id = None
165
+
166
+ # Check if it's a standard JSON-RPC request
167
+ if "jsonrpc" in request_data:
168
+ # Standard JSON-RPC format
169
+ if request_data.get("jsonrpc") != "2.0":
170
+ return _create_error_response(
171
+ InvalidRequestError("Invalid Request. Expected jsonrpc: 2.0"),
172
+ request_data.get("id"),
173
+ )
174
+
175
+ method = request_data.get("method")
176
+ params = request_data.get("params", {})
177
+ json_rpc_id = request_data.get("id")
178
+
179
+ if not method:
180
+ return _create_error_response(
181
+ InvalidRequestError("Invalid Request. Method is required"), json_rpc_id
182
+ )
183
+ else:
184
+ # Simplified format: {"command": "help"} or {"command": "echo", "params": {...}}
185
+ method = request_data.get("command")
186
+ params = request_data.get("params", {})
187
+ json_rpc_id = request_data.get("id", 1) # Default ID for simplified format
188
+
189
+ if not method:
190
+ return _create_error_response(
191
+ InvalidRequestError("Invalid Request. Command is required"), json_rpc_id
192
+ )
193
+
194
+ log.info(f"Using simplified format for command: {method}")
167
195
 
168
196
  log.info(f"Executing JSON-RPC method: {method}")
169
197
 
170
198
  try:
171
- # Execute command
199
+ # Execute command with detailed logging
200
+ log.info(f"🔍 Starting command execution: {method}")
201
+ log.debug(f"📋 Command params: {params}")
202
+
172
203
  result = await execute_command(method, params, request_id, request)
173
204
 
205
+ log.info(f"✅ Command {method} completed successfully")
206
+
174
207
  # Form successful response
175
208
  return {"jsonrpc": "2.0", "result": result, "id": json_rpc_id}
176
209
  except MicroserviceError as e:
177
210
  # Method execution error
178
- log.error(f"Method execution error: {str(e)}")
211
+ log.error(f"Method execution error: {str(e)}")
212
+ log.error(f"📊 Error type: {type(e).__name__}")
179
213
  return _create_error_response(e, json_rpc_id)
180
214
  except Exception as e:
181
215
  # Internal server error
182
- log.exception(f"Unhandled error in JSON-RPC handler: {e}")
216
+ log.exception(f"Unhandled error in JSON-RPC handler: {e}")
217
+ log.error(f"📊 Exception type: {type(e).__name__}")
218
+ log.error(f"📊 Exception details: {repr(e)}")
183
219
  return _create_error_response(
184
- InternalError("Internal error", data={"error": str(e)}), json_rpc_id
220
+ InternalError("Internal error", data={"error": str(e), "error_type": type(e).__name__}), json_rpc_id
185
221
  )
186
222
 
187
223
 
@@ -65,11 +65,10 @@ class UnifiedSecurityMiddleware(BaseHTTPMiddleware):
65
65
  try:
66
66
  security_config = config.get("security", {})
67
67
 
68
- # Check if permissions are enabled - only use mcp_security_framework if needed
69
- permissions_config = security_config.get("permissions", {})
70
- permissions_enabled = permissions_config.get("enabled", False)
68
+ # Check if security is enabled - use mcp_security_framework if needed
69
+ security_enabled = security_config.get("enabled", False)
71
70
 
72
- if permissions_enabled:
71
+ if security_enabled:
73
72
  self.security_integration = create_security_integration(security_config)
74
73
  # Use framework's FastAPI middleware
75
74
  self.framework_middleware = (
@@ -80,7 +79,7 @@ class UnifiedSecurityMiddleware(BaseHTTPMiddleware):
80
79
  # Instead, store the framework middleware for use in dispatch method.
81
80
  logger.info("Framework middleware will be used in dispatch method")
82
81
  else:
83
- logger.info("Permissions disabled, skipping mcp_security_framework integration")
82
+ logger.info("Security disabled, skipping mcp_security_framework integration")
84
83
  self.security_integration = None
85
84
  self.framework_middleware = None
86
85
  except Exception as e:
@@ -114,16 +113,13 @@ class UnifiedSecurityMiddleware(BaseHTTPMiddleware):
114
113
  security_cfg = (
115
114
  self.config.get("security", {}) if isinstance(self.config, dict) else {}
116
115
  )
117
- auth_cfg = security_cfg.get("auth", {})
118
- permissions_cfg = security_cfg.get("permissions", {})
119
- public_paths = set(
120
- auth_cfg.get("public_paths", ["/health", "/docs", "/openapi.json"])
121
- )
116
+ # Use new simplified structure
117
+ public_paths = set(["/health", "/docs", "/openapi.json"])
122
118
  # JSON-RPC endpoint must not be public when API key is required
123
119
  public_paths.discard("/api/jsonrpc")
124
120
  path = request.url.path
125
- methods = set(auth_cfg.get("methods", []))
126
- api_keys: Dict[str, str] = auth_cfg.get("api_keys", {}) or {}
121
+ methods = set(["api_key"]) # Use token-based authentication
122
+ api_keys: Dict[str, str] = security_cfg.get("tokens", {}) or {}
127
123
 
128
124
  # Enforce only for non-public paths when api_key method configured
129
125
  if (
@@ -6,9 +6,12 @@ email: vasilyvz@gmail.com
6
6
  """
7
7
 
8
8
  import json
9
+ import logging
9
10
  import os
10
11
  from typing import Any, Dict, Optional, List
11
12
 
13
+ logger = logging.getLogger(__name__)
14
+
12
15
 
13
16
  class Config:
14
17
  """
@@ -38,6 +41,7 @@ class Config:
38
41
  "server": {
39
42
  "host": "0.0.0.0",
40
43
  "port": 8000,
44
+ "protocol": "http",
41
45
  "debug": False,
42
46
  "log_level": "INFO",
43
47
  },
@@ -65,30 +69,6 @@ class Config:
65
69
  "disabled_commands": [],
66
70
  "custom_commands_path": "./commands",
67
71
  },
68
- "ssl": {
69
- "enabled": False,
70
- "mode": "https_only",
71
- "cert_file": None,
72
- "key_file": None,
73
- "ca_cert": None,
74
- "verify_client": False,
75
- "client_cert_required": False,
76
- "cipher_suites": [
77
- "TLS_AES_256_GCM_SHA384",
78
- "TLS_CHACHA20_POLY1305_SHA256",
79
- ],
80
- "min_tls_version": "TLSv1.2",
81
- "max_tls_version": "1.3",
82
- "token_auth": {
83
- "enabled": False,
84
- "header_name": "Authorization",
85
- "token_prefix": "Bearer",
86
- "tokens_file": "tokens.json",
87
- "token_expiry": 3600,
88
- "jwt_secret": "",
89
- "jwt_algorithm": "HS256",
90
- },
91
- },
92
72
  "roles": {
93
73
  "enabled": False,
94
74
  "config_file": None,
@@ -111,6 +91,7 @@ class Config:
111
91
  "ca_cert": None,
112
92
  "verify_client": False,
113
93
  "client_cert_required": False,
94
+ "chk_hostname": False, # Default to False when SSL is disabled
114
95
  },
115
96
  },
116
97
  "proxy_registration": {
@@ -128,102 +109,18 @@ class Config:
128
109
  },
129
110
  "debug": {"enabled": False, "level": "WARNING"},
130
111
  "security": {
131
- "framework": "mcp_security_framework",
132
112
  "enabled": False,
133
- "debug": False,
134
- "environment": "dev",
135
- "version": "1.0.0",
136
- "auth": {
137
- "enabled": False,
138
- "methods": ["api_key"],
139
- "api_keys": {},
140
- "user_roles": {},
141
- "jwt_secret": "",
142
- "jwt_algorithm": "HS256",
143
- "jwt_expiry_hours": 24,
144
- "certificate_auth": False,
145
- "certificate_roles_oid": "1.3.6.1.4.1.99999.1.1",
146
- "certificate_permissions_oid": "1.3.6.1.4.1.99999.1.2",
147
- "basic_auth": False,
148
- "oauth2_config": None,
149
- "public_paths": ["/health", "/docs", "/openapi.json"],
150
- "security_headers": None,
113
+ "tokens": {
114
+ "admin": "admin-secret-key",
115
+ "user": "user-secret-key",
116
+ "readonly": "readonly-secret-key"
151
117
  },
152
- "ssl": {
153
- "enabled": False,
154
- "cert_file": None,
155
- "key_file": None,
156
- "ca_cert_file": None,
157
- "client_cert_file": None,
158
- "client_key_file": None,
159
- "verify_mode": "CERT_NONE",
160
- "min_tls_version": "TLSv1.2",
161
- "max_tls_version": None,
162
- "cipher_suite": None,
163
- "check_hostname": True,
164
- "check_expiry": True,
165
- "expiry_warning_days": 30,
166
- },
167
- "certificates": {
168
- "enabled": False,
169
- "ca_cert_path": None,
170
- "ca_key_path": None,
171
- "cert_storage_path": "./certs",
172
- "key_storage_path": "./keys",
173
- "default_validity_days": 365,
174
- "key_size": 2048,
175
- "hash_algorithm": "sha256",
176
- "crl_enabled": False,
177
- "crl_path": None,
178
- "crl_url": None,
179
- "crl_validity_days": 30,
180
- "auto_renewal": False,
181
- "renewal_threshold_days": 30,
118
+ "roles": {
119
+ "admin": ["read", "write", "delete", "admin"],
120
+ "user": ["read", "write"],
121
+ "readonly": ["read"]
182
122
  },
183
- "permissions": {
184
- "enabled": False,
185
- "roles_file": None,
186
- "default_role": "guest",
187
- "admin_role": "admin",
188
- "role_hierarchy": {},
189
- "permission_cache_enabled": False,
190
- "permission_cache_ttl": 300,
191
- "wildcard_permissions": False,
192
- "strict_mode": False,
193
- "roles": None,
194
- },
195
- "rate_limit": {
196
- "enabled": False,
197
- "default_requests_per_minute": 60,
198
- "default_requests_per_hour": 1000,
199
- "burst_limit": 2,
200
- "window_size_seconds": 60,
201
- "storage_backend": "memory",
202
- "redis_config": None,
203
- "cleanup_interval": 300,
204
- "exempt_paths": ["/health", "/docs", "/openapi.json"],
205
- "exempt_roles": ["admin"],
206
- },
207
- "logging": {
208
- "enabled": True,
209
- "level": "INFO",
210
- "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
211
- "date_format": "%Y-%m-%d %H:%M:%S",
212
- "file_path": None,
213
- "max_file_size": 10,
214
- "backup_count": 5,
215
- "console_output": True,
216
- "json_format": False,
217
- "include_timestamp": True,
218
- "include_level": True,
219
- "include_module": True,
220
- },
221
- },
222
- "protocols": {
223
- "enabled": True,
224
- "allowed_protocols": ["http", "jsonrpc"],
225
- "default_protocol": "http",
226
- "auto_discovery": True,
123
+ "roles_file": None
227
124
  },
228
125
  }
229
126
 
@@ -238,6 +135,10 @@ class Config:
238
135
 
239
136
  # Load configuration from environment variables
240
137
  self._load_env_variables()
138
+
139
+ # Apply hostname check logic based on SSL configuration
140
+ self._validate_security_config()
141
+ self._apply_hostname_check_logic()
241
142
 
242
143
  def load_from_file(self, config_path: str) -> None:
243
144
  """
@@ -344,6 +245,12 @@ class Config:
344
245
  current = current[part]
345
246
 
346
247
  current[parts[-1]] = value
248
+
249
+ # Special handling for chk_hostname - mark it as user-set
250
+ if key == "transport.ssl.chk_hostname":
251
+ if "ssl" not in self.config_data.get("transport", {}):
252
+ self.config_data["transport"]["ssl"] = {}
253
+ self.config_data["transport"]["ssl"]["_chk_hostname_user_set"] = True
347
254
 
348
255
  def save(self, path: Optional[str] = None) -> None:
349
256
  """
@@ -588,6 +495,58 @@ class Config:
588
495
 
589
496
  return secure_config
590
497
 
498
+ def _validate_security_config(self) -> None:
499
+ """
500
+ Validate security configuration and log warnings for incomplete setup.
501
+ """
502
+ if not self.get("security.enabled", False):
503
+ return
504
+
505
+ # Check if security is enabled but no authentication methods are configured
506
+ tokens = self.get("security.tokens", {})
507
+ roles = self.get("security.roles", {})
508
+ roles_file = self.get("security.roles_file")
509
+
510
+ has_tokens = bool(tokens and any(tokens.values()))
511
+ has_roles = bool(roles and any(roles.values()))
512
+ has_roles_file = bool(roles_file and os.path.exists(roles_file))
513
+
514
+ if not (has_tokens or has_roles or has_roles_file):
515
+ logger.warning(
516
+ "Security is enabled but no authentication methods are configured. "
517
+ "Please configure tokens, roles, or roles_file in the security section."
518
+ )
519
+
520
+ def _apply_hostname_check_logic(self) -> None:
521
+ """
522
+ Apply hostname check logic based on protocol configuration.
523
+ chk_hostname should be True for HTTPS/mTLS protocols, False for HTTP.
524
+ Only set default values if chk_hostname is not explicitly configured.
525
+ """
526
+ protocol = self.get("server.protocol", "http")
527
+ ssl_enabled = self.get("transport.ssl.enabled", False)
528
+
529
+ # Check if chk_hostname is explicitly set by the user
530
+ # We check if it was set by looking for a special flag
531
+ transport_section = self.config_data.get("transport", {})
532
+ ssl_section = transport_section.get("ssl", {})
533
+ chk_hostname_explicitly_set = ssl_section.get("_chk_hostname_user_set", False)
534
+
535
+ # Set chk_hostname based on protocol only if not explicitly set
536
+ if not chk_hostname_explicitly_set:
537
+ if protocol in ["https", "mtls"]:
538
+ # For HTTPS/mTLS, enable hostname checking by default
539
+ self.set("transport.ssl.chk_hostname", True)
540
+ logger.debug(f"Set chk_hostname=True for protocol {protocol} (default)")
541
+ else:
542
+ # For HTTP, disable hostname checking
543
+ self.set("transport.ssl.chk_hostname", False)
544
+ logger.debug(f"Set chk_hostname=False for protocol {protocol} (default)")
545
+ else:
546
+ # Log the explicitly set value
547
+ chk_hostname_value = self.get("transport.ssl.chk_hostname")
548
+ logger.debug(f"Using explicitly set chk_hostname={chk_hostname_value} for protocol {protocol}")
549
+
591
550
 
592
551
  # Singleton instance
593
552
  config = Config()
@@ -56,54 +56,37 @@ class ProtocolManager:
56
56
  f"ProtocolManager._load_config - current_config keys: {list(current_config.keys()) if hasattr(current_config, 'keys') else 'no keys'}"
57
57
  )
58
58
 
59
- # Get protocols configuration
60
- logger.debug(f"ProtocolManager._load_config - before getting protocols")
59
+ # Get server protocol configuration (new simplified structure)
60
+ logger.debug(f"ProtocolManager._load_config - before getting server protocol")
61
61
  try:
62
- self.protocols_config = current_config.get("protocols", {})
63
- logger.debug(
64
- f"ProtocolManager._load_config - protocols_config type: {type(self.protocols_config)}"
65
- )
66
- if hasattr(self.protocols_config, "get"):
67
- logger.debug(
68
- f"ProtocolManager._load_config - protocols_config is dict-like"
69
- )
62
+ server_config = current_config.get("server", {})
63
+ server_protocol = server_config.get("protocol", "http")
64
+ logger.debug(f"ProtocolManager._load_config - server protocol: {server_protocol}")
65
+
66
+ # Set allowed protocols based on server protocol
67
+ if server_protocol == "http":
68
+ self.allowed_protocols = ["http"]
69
+ elif server_protocol == "https":
70
+ self.allowed_protocols = ["https"]
71
+ elif server_protocol == "mtls":
72
+ self.allowed_protocols = ["mtls", "https"] # mTLS also supports HTTPS
70
73
  else:
71
- logger.debug(
72
- f"ProtocolManager._load_config - protocols_config is NOT dict-like: {repr(self.protocols_config)}"
73
- )
74
+ # Fallback to HTTP
75
+ self.allowed_protocols = ["http"]
76
+ logger.warning(f"Unknown server protocol '{server_protocol}', defaulting to HTTP")
77
+
78
+ logger.debug(f"ProtocolManager._load_config - allowed protocols: {self.allowed_protocols}")
79
+
74
80
  except Exception as e:
75
- logger.debug(f"ProtocolManager._load_config - ERROR getting protocols: {e}")
76
- self.protocols_config = {}
81
+ logger.debug(f"ProtocolManager._load_config - ERROR getting server protocol: {e}")
82
+ # Fallback to HTTP
83
+ self.allowed_protocols = ["http"]
77
84
 
78
- self.enabled = (
79
- self.protocols_config.get("enabled", True)
80
- if hasattr(self.protocols_config, "get")
81
- else True
82
- )
83
-
84
- # Get SSL configuration to determine allowed protocols
85
- ssl_enabled = self._is_ssl_enabled(current_config)
86
-
87
- # Set allowed protocols based on SSL configuration
88
- if ssl_enabled:
89
- # If SSL is enabled, allow both HTTP and HTTPS
90
- self.allowed_protocols = self.protocols_config.get(
91
- "allowed_protocols", ["http", "https"]
92
- )
93
- # Ensure HTTPS is in allowed protocols if SSL is enabled
94
- if "https" not in self.allowed_protocols:
95
- self.allowed_protocols.append("https")
96
- else:
97
- # If SSL is disabled, only allow HTTP
98
- self.allowed_protocols = self.protocols_config.get(
99
- "allowed_protocols", ["http"]
100
- )
101
- # Remove HTTPS from allowed protocols if SSL is disabled
102
- if "https" in self.allowed_protocols:
103
- self.allowed_protocols.remove("https")
85
+ # Protocol management is always enabled in new structure
86
+ self.enabled = True
104
87
 
105
88
  logger.debug(
106
- f"Protocol manager loaded config: enabled={self.enabled}, allowed_protocols={self.allowed_protocols}, ssl_enabled={ssl_enabled}"
89
+ f"Protocol manager loaded config: enabled={self.enabled}, allowed_protocols={self.allowed_protocols}"
107
90
  )
108
91
 
109
92
  def _is_ssl_enabled(self, current_config: Dict) -> bool: