Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ doc_ref/
# C extensions
*REDESIGN*.md
*FLOW.md
*USAGE.md
*.so
notes.md
/reports/
Expand Down
20 changes: 11 additions & 9 deletions mcp_fuzzer/cli/config_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import logging
from typing import Any

from ..config import config as global_config, load_config_file, apply_config_file
from ..exceptions import ConfigFileError
from ..client.adapters import config_mediator
from ..client.settings import CliConfig
from ..client.transport.auth_port import resolve_auth_port
from .parser import create_argument_parser
Expand Down Expand Up @@ -60,7 +60,7 @@ def _transfer_config_to_args(args: argparse.Namespace) -> None:
]

for config_key, args_key in mapping:
config_value = global_config.get(config_key)
config_value = config_mediator.get(config_key)
default_value = defaults_parser.get_default(args_key)
if default_value is argparse.SUPPRESS: # pragma: no cover
default_value = None
Expand All @@ -73,17 +73,19 @@ def build_cli_config(args: argparse.Namespace) -> CliConfig:
"""Merge CLI args, config files, and resolved auth."""
if args.config:
try:
loaded = load_config_file(args.config)
global_config.update(loaded)
loaded = config_mediator.load_file(args.config)
config_mediator.update(loaded)
except Exception as exc:
raise ConfigFileError(
f"Failed to load configuration file '{args.config}': {exc}"
)
else:
try:
apply_config_file()
except Exception as exc:
logging.debug(f"Error loading default configuration file: {exc}")
# apply_file() returns False if config loading fails (doesn't raise)
if not config_mediator.apply_file():
logging.debug(
"Default configuration file not found or failed to load "
"(this is normal if no config file exists)"
)

_transfer_config_to_args(args)
auth_manager = resolve_auth_port(args)
Expand Down Expand Up @@ -135,7 +137,7 @@ def build_cli_config(args: argparse.Namespace) -> CliConfig:
"auth_manager": auth_manager,
}

global_config.update(merged)
config_mediator.update(merged)
return CliConfig(args=args, merged=merged)


Expand Down
4 changes: 2 additions & 2 deletions mcp_fuzzer/cli/startup_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ def print_startup_info(args: argparse.Namespace, config: dict | None = None) ->
try:
# Load and display the config file content
import json
from ..config import load_config_file
raw_config = load_config_file(args.config)
from ..client.adapters import config_mediator
raw_config = config_mediator.load_file(args.config)
config_json = json.dumps(raw_config, indent=2, sort_keys=True)
console.print(f"[dim]{config_json}[/dim]")
console.print()
Expand Down
4 changes: 2 additions & 2 deletions mcp_fuzzer/cli/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from rich.console import Console

from ..exceptions import ArgumentValidationError
from ..config import load_config_file
from ..client.adapters import config_mediator
from ..transport.factory import create_transport
from ..exceptions import MCPError, TransportError
from ..env import ENVIRONMENT_VARIABLES, ValidationType
Expand Down Expand Up @@ -65,7 +65,7 @@ def validate_arguments(self, args: argparse.Namespace) -> None:

def validate_config_file(self, path: str) -> None:
"""Validate a config file and print success message."""
load_config_file(path)
config_mediator.load_file(path)
success_msg = (
"[green]:heavy_check_mark: Configuration file "
f"'{path}' is valid[/green]"
Expand Down
67 changes: 66 additions & 1 deletion mcp_fuzzer/client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,72 @@
"""Public client exports."""

from .base import MCPFuzzerClient
from .adapters import ConfigAdapter, config_mediator
from .constants import (
CONTENT_TYPE_HEADER,
DEFAULT_FORCE_KILL_TIMEOUT,
DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT,
DEFAULT_HTTP_ACCEPT,
DEFAULT_MAX_TOTAL_FUZZING_TIME,
DEFAULT_MAX_TOOL_TIME,
DEFAULT_PROTOCOL_RUNS_PER_TYPE,
DEFAULT_PROTOCOL_VERSION,
DEFAULT_TIMEOUT,
DEFAULT_TOOL_RUNS,
DEFAULT_TOOL_TIMEOUT,
JSON_CONTENT_TYPE,
MCP_PROTOCOL_VERSION_HEADER,
MCP_SESSION_ID_HEADER,
PROCESS_CLEANUP_TIMEOUT,
PROCESS_FORCE_KILL_TIMEOUT,
PROCESS_TERMINATION_TIMEOUT,
PROCESS_WAIT_TIMEOUT,
SAFETY_ENV_ALLOWLIST,
SAFETY_HEADER_DENYLIST,
SAFETY_LOCAL_HOSTS,
SAFETY_NO_NETWORK_DEFAULT,
SAFETY_PROXY_ENV_DENYLIST,
SSE_CONTENT_TYPE,
WATCHDOG_DEFAULT_CHECK_INTERVAL,
WATCHDOG_EXTRA_BUFFER,
WATCHDOG_MAX_HANG_ADDITIONAL,
)
from .ports import ConfigPort

UnifiedMCPFuzzerClient = MCPFuzzerClient

__all__ = ["MCPFuzzerClient", "UnifiedMCPFuzzerClient"]
__all__ = [
"MCPFuzzerClient",
"UnifiedMCPFuzzerClient",
"ConfigPort",
"ConfigAdapter",
"config_mediator",
# Constants
"DEFAULT_PROTOCOL_VERSION",
"CONTENT_TYPE_HEADER",
"JSON_CONTENT_TYPE",
"SSE_CONTENT_TYPE",
"DEFAULT_HTTP_ACCEPT",
"MCP_SESSION_ID_HEADER",
"MCP_PROTOCOL_VERSION_HEADER",
"WATCHDOG_DEFAULT_CHECK_INTERVAL",
"WATCHDOG_EXTRA_BUFFER",
"WATCHDOG_MAX_HANG_ADDITIONAL",
"SAFETY_LOCAL_HOSTS",
"SAFETY_NO_NETWORK_DEFAULT",
"SAFETY_HEADER_DENYLIST",
"SAFETY_PROXY_ENV_DENYLIST",
"SAFETY_ENV_ALLOWLIST",
"DEFAULT_TOOL_RUNS",
"DEFAULT_PROTOCOL_RUNS_PER_TYPE",
"DEFAULT_TIMEOUT",
"DEFAULT_TOOL_TIMEOUT",
"DEFAULT_MAX_TOOL_TIME",
"DEFAULT_MAX_TOTAL_FUZZING_TIME",
"DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT",
"DEFAULT_FORCE_KILL_TIMEOUT",
"PROCESS_TERMINATION_TIMEOUT",
"PROCESS_FORCE_KILL_TIMEOUT",
"PROCESS_CLEANUP_TIMEOUT",
"PROCESS_WAIT_TIMEOUT",
]
11 changes: 11 additions & 0 deletions mcp_fuzzer/client/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python3
"""Adapter implementations for Port and Adapter pattern.

Adapters implement the ports (interfaces) by adapting external modules.
This is where the mediation between modules happens.
"""

from .config_adapter import ConfigAdapter, config_mediator

__all__ = ["ConfigAdapter", "config_mediator"]

113 changes: 113 additions & 0 deletions mcp_fuzzer/client/adapters/config_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Configuration adapter implementation for Port and Adapter pattern.

This module implements the ConfigPort interface by adapting the config module.
This is the adapter that mediates all configuration access.
"""

from __future__ import annotations

from typing import Any

from ...config import (
apply_config_file,
get_config_schema,
load_config_file,
)
from ...config.core.manager import config as global_config
from ..ports.config_port import ConfigPort


class ConfigAdapter(ConfigPort):
"""Adapter that implements ConfigPort by delegating to the config module.

This adapter acts as a mediator between other modules and the config module,
implementing the Port and Adapter (Hexagonal Architecture) pattern.
"""

def __init__(self, config_instance: Any = None):
"""Initialize the config adapter.

Args:
config_instance: Optional configuration instance to use.
If None, uses the global config instance.
"""
self._config = config_instance or global_config

def get(self, key: str, default: Any = None) -> Any:
"""Get a configuration value by key.

Args:
key: Configuration key
default: Default value if key not found

Returns:
Configuration value or default
"""
return self._config.get(key, default)

def set(self, key: str, value: Any) -> None:
"""Set a configuration value.

Args:
key: Configuration key
value: Configuration value
"""
self._config.set(key, value)

def update(self, config_dict: dict[str, Any]) -> None:
"""Update configuration with values from a dictionary.

Args:
config_dict: Dictionary of configuration values to update
"""
self._config.update(config_dict)

def load_file(self, file_path: str) -> dict[str, Any]:
"""Load configuration from a file.

Args:
file_path: Path to configuration file

Returns:
Dictionary containing loaded configuration

Raises:
ConfigFileError: If file cannot be loaded
"""
return load_config_file(file_path)

def apply_file(
self,
config_path: str | None = None,
search_paths: list[str] | None = None,
file_names: list[str] | None = None,
) -> bool:
"""Load and apply configuration from a file.

Args:
config_path: Explicit path to config file
search_paths: List of directories to search
file_names: List of file names to search for

Returns:
True if configuration was loaded and applied, False otherwise
"""
return apply_config_file(
config_path=config_path,
search_paths=search_paths,
file_names=file_names,
)
Comment on lines +80 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential inconsistency with custom config_instance.

When ConfigAdapter is initialized with a custom config_instance, the apply_file() method still delegates to the module-level apply_config_file(), which updates the global config rather than self._config. This creates inconsistent behavior where get/set/update operate on the custom instance but apply_file bypasses it.

Consider injecting the config instance into the loader or documenting this as intentional behavior.

     def apply_file(
         self,
         config_path: str | None = None,
         search_paths: list[str] | None = None,
         file_names: list[str] | None = None,
     ) -> bool:
-        return apply_config_file(
-            config_path=config_path,
-            search_paths=search_paths,
-            file_names=file_names,
-        )
+        from ...config.loading import ConfigLoader
+        loader = ConfigLoader(config_instance=self._config)
+        return loader.apply(
+            config_path=config_path,
+            search_paths=search_paths,
+            file_names=file_names,
+        )

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In mcp_fuzzer/client/adapters/config_adapter.py around lines 80 to 100,
apply_file() calls the module-level apply_config_file which mutates the global
config rather than the instance passed to ConfigAdapter, causing inconsistency
with methods that operate on self._config; modify apply_file to either call a
loader that accepts and populates self._config (pass self._config into
apply_config_file or a new helper) or, after calling apply_config_file,
replace/update self._config with the loaded configuration object so all
get/set/update operations use the same instance.


def get_schema(self) -> dict[str, Any]:
"""Get the JSON schema for configuration validation.

Returns:
JSON schema dictionary
"""
return get_config_schema()


# Global instance for convenience (acts as the mediator)
config_mediator: ConfigPort = ConfigAdapter()

Loading
Loading