@@ -152,6 +152,7 @@ class Settings(BaseSettings):
152152 app_name : str = "MCP_Gateway"
153153 host : str = "127.0.0.1"
154154 port : PositiveInt = Field (default = 4444 , ge = 1 , le = 65535 )
155+ client_mode : bool = False
155156 docs_allow_basic_auth : bool = False # Allow basic auth for docs
156157 database_url : str = "sqlite:///./mcp.db"
157158
@@ -448,28 +449,30 @@ def validate_secrets(cls, v: Any, info: ValidationInfo) -> SecretStr:
448449 value = str (v )
449450
450451 # Check for default/weak secrets
451- weak_secrets = ["my-test-key" , "my-test-salt" , "changeme" , "secret" , "password" ]
452- if value .lower () in weak_secrets :
453- logger .warning (f"🔓 SECURITY WARNING - { field_name } : Default/weak secret detected! Please set a strong, unique value for production." )
452+ if not info .data .get ("client_mode" ):
453+ weak_secrets = ["my-test-key" , "my-test-salt" , "changeme" , "secret" , "password" ]
454+ if value .lower () in weak_secrets :
455+ logger .warning (f"🔓 SECURITY WARNING - { field_name } : Default/weak secret detected! Please set a strong, unique value for production." )
454456
455- # Check minimum length
456- if len (value ) < 32 :
457- logger .warning (f"⚠️ SECURITY WARNING - { field_name } : Secret should be at least 32 characters long. Current length: { len (value )} " )
457+ # Check minimum length
458+ if len (value ) < 32 :
459+ logger .warning (f"⚠️ SECURITY WARNING - { field_name } : Secret should be at least 32 characters long. Current length: { len (value )} " )
458460
459- # Basic entropy check (at least 10 unique characters)
460- if len (set (value )) < 10 :
461- logger .warning (f"🔑 SECURITY WARNING - { field_name } : Secret has low entropy. Consider using a more random value." )
461+ # Basic entropy check (at least 10 unique characters)
462+ if len (set (value )) < 10 :
463+ logger .warning (f"🔑 SECURITY WARNING - { field_name } : Secret has low entropy. Consider using a more random value." )
462464
463465 # Always return SecretStr to keep it secret-safe
464466 return v if isinstance (v , SecretStr ) else SecretStr (value )
465467
466468 @field_validator ("basic_auth_password" )
467469 @classmethod
468- def validate_admin_password (cls , v : str | SecretStr ) -> SecretStr :
470+ def validate_admin_password (cls , v : str | SecretStr , info : ValidationInfo ) -> SecretStr :
469471 """Validate admin password meets security requirements.
470472
471473 Args:
472474 v: The admin password value to validate.
475+ info: ValidationInfo containing field data.
473476
474477 Returns:
475478 SecretStr: The validated admin password value, wrapped as SecretStr.
@@ -480,35 +483,37 @@ def validate_admin_password(cls, v: str | SecretStr) -> SecretStr:
480483 else :
481484 value = v
482485
483- if value == "changeme" : # nosec B105 - checking for default value
484- logger .warning ("🔓 SECURITY WARNING: Default admin password detected! Please change the BASIC_AUTH_PASSWORD immediately." )
486+ if not info .data .get ("client_mode" ):
487+ if value == "changeme" : # nosec B105 - checking for default value
488+ logger .warning ("🔓 SECURITY WARNING: Default admin password detected! Please change the BASIC_AUTH_PASSWORD immediately." )
485489
486- # Note: We can't access password_min_length here as it's not set yet during validation
487- # Using default value of 8 to match the field default
488- min_length = 8 # This matches the default in password_min_length field
489- if len (value ) < min_length :
490- logger .warning (f"⚠️ SECURITY WARNING: Admin password should be at least { min_length } characters long. Current length: { len (value )} " )
490+ # Note: We can't access password_min_length here as it's not set yet during validation
491+ # Using default value of 8 to match the field default
492+ min_length = 8 # This matches the default in password_min_length field
493+ if len (value ) < min_length :
494+ logger .warning (f"⚠️ SECURITY WARNING: Admin password should be at least { min_length } characters long. Current length: { len (value )} " )
491495
492- # Check password complexity
493- has_upper = any (c .isupper () for c in value )
494- has_lower = any (c .islower () for c in value )
495- has_digit = any (c .isdigit () for c in value )
496- has_special = bool (re .search (r'[!@#$%^&*(),.?":{}|<>]' , value ))
496+ # Check password complexity
497+ has_upper = any (c .isupper () for c in value )
498+ has_lower = any (c .islower () for c in value )
499+ has_digit = any (c .isdigit () for c in value )
500+ has_special = bool (re .search (r'[!@#$%^&*(),.?":{}|<>]' , value ))
497501
498- complexity_score = sum ([has_upper , has_lower , has_digit , has_special ])
499- if complexity_score < 3 :
500- logger .warning ("🔐 SECURITY WARNING: Admin password has low complexity. Should contain at least 3 of: uppercase, lowercase, digits, special characters" )
502+ complexity_score = sum ([has_upper , has_lower , has_digit , has_special ])
503+ if complexity_score < 3 :
504+ logger .warning ("🔐 SECURITY WARNING: Admin password has low complexity. Should contain at least 3 of: uppercase, lowercase, digits, special characters" )
501505
502506 # Always return SecretStr to keep it secret-safe
503507 return v if isinstance (v , SecretStr ) else SecretStr (value )
504508
505509 @field_validator ("allowed_origins" )
506510 @classmethod
507- def validate_cors_origins (cls , v : Any ) -> set [str ] | None :
511+ def validate_cors_origins (cls , v : Any , info : ValidationInfo ) -> set [str ] | None :
508512 """Validate CORS allowed origins.
509513
510514 Args:
511515 v: The set of allowed origins to validate.
516+ info: ValidationInfo containing field data.
512517
513518 Returns:
514519 set: The validated set of allowed origins.
@@ -522,35 +527,38 @@ def validate_cors_origins(cls, v: Any) -> set[str] | None:
522527 raise ValueError ("allowed_origins must be a set or list of strings" )
523528
524529 dangerous_origins = ["*" , "null" , "" ]
525- for origin in v :
526- if origin in dangerous_origins :
527- logger .warning (f"🌐 SECURITY WARNING: Dangerous CORS origin '{ origin } ' detected. Consider specifying explicit origins instead of wildcards." )
530+ if not info .data .get ("client_mode" ):
531+ for origin in v :
532+ if origin in dangerous_origins :
533+ logger .warning (f"🌐 SECURITY WARNING: Dangerous CORS origin '{ origin } ' detected. Consider specifying explicit origins instead of wildcards." )
528534
529- # Validate URL format
530- if not origin .startswith (("http://" , "https://" )) and origin not in dangerous_origins :
531- logger .warning (f"⚠️ SECURITY WARNING: Invalid origin format '{ origin } '. Origins should start with http:// or https://" )
535+ # Validate URL format
536+ if not origin .startswith (("http://" , "https://" )) and origin not in dangerous_origins :
537+ logger .warning (f"⚠️ SECURITY WARNING: Invalid origin format '{ origin } '. Origins should start with http:// or https://" )
532538
533539 return set ({str (origin ) for origin in v })
534540
535541 @field_validator ("database_url" )
536542 @classmethod
537- def validate_database_url (cls , v : str ) -> str :
543+ def validate_database_url (cls , v : str , info : ValidationInfo ) -> str :
538544 """Validate database connection string security.
539545
540546 Args:
541547 v: The database URL to validate.
548+ info: ValidationInfo containing field data.
542549
543550 Returns:
544551 str: The validated database URL.
545552 """
546553 # Check for hardcoded passwords in non-SQLite databases
547- if not v .startswith ("sqlite" ):
548- if "password" in v and any (weak in v for weak in ["password" , "123" , "admin" , "test" ]):
549- logger .warning ("Potentially weak database password detected. Consider using a stronger password." )
554+ if not info .data .get ("client_mode" ):
555+ if not v .startswith ("sqlite" ):
556+ if "password" in v and any (weak in v for weak in ["password" , "123" , "admin" , "test" ]):
557+ logger .warning ("Potentially weak database password detected. Consider using a stronger password." )
550558
551- # Warn about SQLite in production
552- if v .startswith ("sqlite" ):
553- logger .info ("Using SQLite database. Consider PostgreSQL or MySQL for production." )
559+ # Warn about SQLite in production
560+ if v .startswith ("sqlite" ):
561+ logger .info ("Using SQLite database. Consider PostgreSQL or MySQL for production." )
554562
555563 return v
556564
@@ -561,6 +569,9 @@ def validate_security_combinations(self) -> Self:
561569 Returns:
562570 Itself.
563571 """
572+ if self .client_mode :
573+ return self
574+
564575 # Check for dangerous combinations - only log warnings, don't raise errors
565576 if not self .auth_required and self .mcpgateway_ui_enabled :
566577 logger .warning ("🔓 SECURITY WARNING: Admin UI is enabled without authentication. Consider setting AUTH_REQUIRED=true for production." )
@@ -1526,9 +1537,12 @@ def log_summary(self) -> None:
15261537
15271538
15281539@lru_cache ()
1529- def get_settings () -> Settings :
1540+ def get_settings (** kwargs : Any ) -> Settings :
15301541 """Get cached settings instance.
15311542
1543+ Args:
1544+ **kwargs: Keyword arguments to pass to the Settings setup.
1545+
15321546 Returns:
15331547 Settings: A cached instance of the Settings class.
15341548
@@ -1543,7 +1557,7 @@ def get_settings() -> Settings:
15431557 """
15441558 # Instantiate a fresh Pydantic Settings object,
15451559 # loading from env vars or .env exactly once.
1546- cfg = Settings ()
1560+ cfg = Settings (** kwargs )
15471561 # Validate that transport_type is correct; will
15481562 # raise if mis-configured.
15491563 cfg .validate_transport ()
@@ -1565,7 +1579,24 @@ def generate_settings_schema() -> dict[str, Any]:
15651579 return Settings .model_json_schema (mode = "validation" )
15661580
15671581
1568- settings = get_settings ()
1582+ # Lazy "instance" of settings
1583+ class LazySettingsWrapper :
1584+ """Lazily initialize settings singleton on getattr"""
1585+
1586+ def __getattr__ (self , key : str ) -> Any :
1587+ """Get the real settings object and forward to it
1588+
1589+ Args:
1590+ key: The key to fetch from settings
1591+
1592+ Returns:
1593+ Any: The value of the attribute on the settings
1594+ """
1595+ return getattr (get_settings (), key )
1596+
1597+
1598+ settings = LazySettingsWrapper ()
1599+
15691600
15701601if __name__ == "__main__" :
15711602 if "--schema" in sys .argv :
0 commit comments