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
24 changes: 9 additions & 15 deletions mcp_fuzzer/cli/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from ..exceptions import ArgumentValidationError
from ..config import load_config_file
from ..transport.factory import create_transport
from ..transport.catalog import build_driver
from ..exceptions import MCPError, TransportError
from ..env import ENVIRONMENT_VARIABLES, ValidationType

Expand All @@ -25,9 +25,10 @@ def __init__(self):

def validate_arguments(self, args: argparse.Namespace) -> None:
"""Validate CLI arguments for fuzzing operations."""
is_utility_command = getattr(args, "check_env", False) or getattr(
args, "validate_config", None
) is not None
is_utility_command = (
getattr(args, "check_env", False)
or getattr(args, "validate_config", None) is not None
)

if not is_utility_command and not getattr(args, "endpoint", None):
raise ArgumentValidationError(
Expand Down Expand Up @@ -67,17 +68,14 @@ def validate_config_file(self, path: str) -> None:
"""Validate a config file and print success message."""
load_config_file(path)
success_msg = (
"[green]:heavy_check_mark: Configuration file "
f"'{path}' is valid[/green]"
f"[green]:heavy_check_mark: Configuration file '{path}' is valid[/green]"
)
self.console.print(emoji.emojize(success_msg, language="alias"))


def check_environment_variables(self) -> bool:
"""Print environment variable status and return validation result."""
self.console.print("[bold]Environment variables check:[/bold]")


all_valid = True
for env_var in ENVIRONMENT_VARIABLES:
name = env_var["name"]
Expand Down Expand Up @@ -154,17 +152,13 @@ def _get_validation_error_msg(
)
elif validation_type == ValidationType.NUMERIC:
return (
"[red]:heavy_multiplication_x: "
f"{name}={value} (must be numeric)[/red]"
f"[red]:heavy_multiplication_x: {name}={value} (must be numeric)[/red]"
)
return (
"[red]:heavy_multiplication_x: "
f"{name}={value} (invalid value)[/red]"
)
return f"[red]:heavy_multiplication_x: {name}={value} (invalid value)[/red]"

def validate_transport(self, args: Any) -> None:
try:
_ = create_transport(
_ = build_driver(
args.protocol,
args.endpoint,
timeout=args.timeout,
Expand Down
4 changes: 2 additions & 2 deletions mcp_fuzzer/client/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from ..exceptions import MCPError
from .settings import ClientSettings
from .base import MCPFuzzerClient
from .transport import create_transport_with_auth
from .transport import build_driver_with_auth

# For backward compatibility
UnifiedMCPFuzzerClient = MCPFuzzerClient
Expand Down Expand Up @@ -47,7 +47,7 @@ def __init__(self, protocol, endpoint, timeout):
"auth_manager": config.get("auth_manager"),
}

transport = create_transport_with_auth(args, client_args)
transport = build_driver_with_auth(args, client_args)

safety_enabled = config.get("safety_enabled", True)
safety_system = None
Expand Down
15 changes: 8 additions & 7 deletions mcp_fuzzer/client/tool_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
DEFAULT_MAX_TOTAL_FUZZING_TIME,
DEFAULT_FORCE_KILL_TIMEOUT,
)
from ..transport.interfaces import JsonRpcAdapter


class ToolClient:
"""Client for fuzzing MCP tools."""
Expand All @@ -40,6 +42,7 @@ def __init__(
max_concurrency: Maximum number of concurrent operations
"""
self.transport = transport
self._rpc = JsonRpcAdapter(transport)
self.auth_manager = auth_manager or AuthManager()
self.enable_safety = enable_safety
if not enable_safety:
Expand All @@ -60,7 +63,7 @@ async def _get_tools_from_server(self) -> list[dict[str, Any]]:
List of tool definitions or empty list if failed.
"""
try:
tools = await self.transport.get_tools()
tools = await self._rpc.get_tools()
if not tools:
self._logger.warning("Server returned an empty list of tools.")
return []
Expand Down Expand Up @@ -93,7 +96,7 @@ async def _fuzz_single_tool_with_timeout(
try:
tool_task = asyncio.create_task(
self.fuzz_tool(tool, runs_per_tool, tool_timeout=tool_timeout),
name=f"fuzz_tool_{tool_name}"
name=f"fuzz_tool_{tool_name}",
)

try:
Expand Down Expand Up @@ -180,9 +183,7 @@ async def fuzz_tool(

# Call the tool with the generated arguments
try:
result = await self.transport.call_tool(
tool_name, args_for_call
)
result = await self._rpc.call_tool(tool_name, args_for_call)
results.append(
{
"args": sanitized_args,
Expand Down Expand Up @@ -311,11 +312,11 @@ async def _process_fuzz_results(
fuzz_results: list[dict[str, Any]],
) -> list[dict[str, Any]]:
"""Process fuzz results with safety checks and tool calls.

Args:
tool_name: Name of the tool being fuzzed
fuzz_results: List of fuzz results from the fuzzer

Returns:
List of processed results with tool call outcomes
"""
Expand Down
4 changes: 2 additions & 2 deletions mcp_fuzzer/client/transport/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .factory import create_transport_with_auth
from .factory import build_driver_with_auth

__all__ = ["create_transport_with_auth"]
__all__ = ["build_driver_with_auth"]
9 changes: 5 additions & 4 deletions mcp_fuzzer/client/transport/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@

from rich.console import Console

from ...transport.factory import create_transport as base_create_transport
from ...transport.catalog import build_driver as base_build_driver

logger = logging.getLogger(__name__)


def create_transport_with_auth(args: Any, client_args: dict[str, Any]):
def build_driver_with_auth(args: Any, client_args: dict[str, Any]):
"""Create a transport with authentication headers when available."""
try:
auth_headers = None
Expand Down Expand Up @@ -46,7 +47,7 @@ def create_transport_with_auth(args: Any, client_args: dict[str, Any]):
args.protocol.upper(),
args.endpoint,
)
transport = base_create_transport(
transport = base_build_driver(
args.protocol,
args.endpoint,
**factory_kwargs,
Expand All @@ -59,4 +60,4 @@ def create_transport_with_auth(args: Any, client_args: dict[str, Any]):
sys.exit(1)


__all__ = ["create_transport_with_auth"]
__all__ = ["build_driver_with_auth"]
17 changes: 11 additions & 6 deletions mcp_fuzzer/config/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@

from .manager import config
from ..exceptions import ConfigFileError, MCPError
from ..transport.custom import register_custom_transport
from ..transport.base import TransportProtocol
from ..transport.catalog.custom_catalog import register_custom_driver
from ..transport.interfaces import TransportDriver
import importlib
Comment on lines +16 to 18
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Custom transport loading correctly targets the new TransportDriver interface

The loader now cleanly aligns with the redesigned transport layer: it imports TransportDriver, validates custom classes with issubclass(transport_class, TransportDriver), and registers them through register_custom_driver, with the schema text updated to reference build_driver. This tightens type safety around the new driver abstraction; just be aware this is a breaking change for any external custom transports that previously subclassed the old protocol base and will now need to adopt TransportDriver.

Also applies to: 137-257, 330-376

🤖 Prompt for AI Agents
In mcp_fuzzer/config/loader.py around lines 16-18 (and similarly at 137-257 and
330-376), the loader must validate and register custom transport classes against
the new TransportDriver interface: ensure importlib is used to load the module,
import TransportDriver at the top, call issubclass(transport_class,
TransportDriver) when validating discovered classes, and pass the validated
class to register_custom_driver (ensuring schema references build_driver if
applicable); update any old protocol-base checks to use TransportDriver so
external custom transports are rejected or adapted until they subclass the new
interface.


logger = logging.getLogger(__name__)


def find_config_file(
config_path: str | None = None,
search_paths: list[str] | None = None,
Expand Down Expand Up @@ -61,6 +62,7 @@ def find_config_file(

return None


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

Expand Down Expand Up @@ -99,6 +101,7 @@ def load_config_file(file_path: str) -> dict[str, Any]:
f"Unexpected error reading configuration file {file_path}: {str(e)}"
)


def apply_config_file(
config_path: str | None = None,
search_paths: list[str] | None = None,
Expand Down Expand Up @@ -130,6 +133,7 @@ def apply_config_file(
config.update(config_data)
return True


def get_config_schema() -> dict[str, Any]:
"""Get the configuration schema.

Expand Down Expand Up @@ -240,7 +244,7 @@ def get_config_schema() -> dict[str, Any]:
"factory": {
"type": "string",
"description": "Dotted path to factory function "
"(e.g., pkg.mod.create_transport)",
"(e.g., pkg.mod.build_driver)",
},
"config_schema": {
"type": "object",
Expand Down Expand Up @@ -322,6 +326,7 @@ def get_config_schema() -> dict[str, Any]:
},
}


def load_custom_transports(config_data: dict[str, Any]) -> None:
"""Load and register custom transports from configuration.

Expand All @@ -340,9 +345,9 @@ def load_custom_transports(config_data: dict[str, Any]) -> None:
transport_class = getattr(module, class_name)
if not isinstance(transport_class, type):
raise ConfigFileError(f"{module_path}.{class_name} is not a class")
if not issubclass(transport_class, TransportProtocol):
if not issubclass(transport_class, TransportDriver):
raise ConfigFileError(
f"{module_path}.{class_name} must subclass TransportProtocol"
f"{module_path}.{class_name} must subclass TransportDriver"
)

# Register the transport
Expand All @@ -362,7 +367,7 @@ def load_custom_transports(config_data: dict[str, Any]) -> None:
if not callable(factory_fn):
raise ConfigFileError(f"Factory '{factory_path}' is not callable")

register_custom_transport(
register_custom_driver(
name=transport_name,
transport_class=transport_class,
description=description,
Expand Down
6 changes: 3 additions & 3 deletions mcp_fuzzer/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Shared event contract definitions for runtime components.

This module exposes a lightweight protocol that both the runtime `ProcessManager`
and transport `TransportManager` use to dispatch lifecycle events. Observers can
and transport `ProcessSupervisor` use to dispatch lifecycle events. Observers can
subscribe to these events to monitor state changes without depending on
internal implementation details.

Expand All @@ -20,9 +20,9 @@
- ``signal`` / ``signal_all`` (ProcessManager): emitted when signals are sent.
payload keys: ``pid``, ``signal``, ``process_name``, ``result`` (``signal_all``
adds ``results`` and ``failures``).
- ``signal_failed`` (TransportManager): emitted when signal dispatch fails.
- ``signal_failed`` (ProcessSupervisor): emitted when signal dispatch fails.
payload keys: ``pid`` and ``error``.
- ``oversized_output`` (TransportManager): emitted whenever stdio output exceeds
- ``oversized_output`` (ProcessSupervisor): emitted whenever stdio output exceeds
the configured cap. Payload keys: ``pid``, ``size``, ``limit``.

Future event producers should keep the payloads shallow (``dict[str, Any]``) to
Expand Down
84 changes: 60 additions & 24 deletions mcp_fuzzer/transport/__init__.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,64 @@
from .base import TransportProtocol
from .http import HTTPTransport
from .sse import SSETransport
from .stdio import StdioTransport
from .streamable_http import StreamableHTTPTransport
from .factory import create_transport
from .manager import TransportManager, TransportProcessState
from .custom import (
CustomTransportRegistry,
register_custom_transport,
create_custom_transport,
list_custom_transports,
"""Transport subsystem composed of interfaces, drivers, catalogs, and controllers."""

from .interfaces import (
TransportDriver,
DriverState,
ParsedEndpoint,
DriverBaseBehavior,
HttpClientBehavior,
ResponseParserBehavior,
LifecycleBehavior,
TransportError,
NetworkError,
PayloadValidationError,
JsonRpcAdapter,
)
from .drivers import (
HttpDriver,
SseDriver,
StdioDriver,
StreamHttpDriver,
)
from .catalog import (
DriverCatalog,
driver_catalog,
build_driver,
EndpointResolver,
CustomDriverCatalog,
register_custom_driver,
build_custom_driver,
list_custom_drivers,
custom_driver_catalog,
)
from .controller.coordinator import TransportCoordinator
from .controller.process_supervisor import ProcessSupervisor, ProcessState
Comment on lines +33 to +34
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

cat mcp_fuzzer/transport/controller/__init__.py 2>/dev/null || echo "File not found"

Repository: Agent-Hellboy/mcp-server-fuzzer

Length of output: 711


Import from .controller package to leverage its re-export interface.

The controller package's __init__.py explicitly re-exports TransportCoordinator, ProcessSupervisor, and ProcessState via __all__ and lazy-loading through __getattr__. Importing from .controller instead of directly from submodules will align with the package's intended interface and eliminate unnecessary direct submodule coupling.

-from .controller.coordinator import TransportCoordinator
-from .controller.process_supervisor import ProcessSupervisor, ProcessState
+from .controller import TransportCoordinator, ProcessSupervisor, ProcessState
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from .controller.coordinator import TransportCoordinator
from .controller.process_supervisor import ProcessSupervisor, ProcessState
from .controller import TransportCoordinator, ProcessSupervisor, ProcessState
🤖 Prompt for AI Agents
In mcp_fuzzer/transport/__init__.py around lines 33-34, the file currently
imports TransportCoordinator, ProcessSupervisor, and ProcessState directly from
submodules; change these imports to import from the package re-export interface
by importing them from .controller instead (e.g. from .controller import
TransportCoordinator, ProcessSupervisor, ProcessState) so the code uses the
controller package's __all__ and __getattr__ lazy-loading and removes direct
submodule coupling.


__all__ = [
"TransportProtocol",
"HTTPTransport",
"SSETransport",
"StdioTransport",
"StreamableHTTPTransport",
"TransportManager",
"TransportProcessState",
"create_transport",
"CustomTransportRegistry",
"register_custom_transport",
"create_custom_transport",
"list_custom_transports",
"TransportDriver",
"DriverState",
"ParsedEndpoint",
"DriverBaseBehavior",
"HttpClientBehavior",
"ResponseParserBehavior",
"LifecycleBehavior",
"TransportError",
"NetworkError",
"PayloadValidationError",
"JsonRpcAdapter",
"HttpDriver",
"SseDriver",
"StdioDriver",
"StreamHttpDriver",
"DriverCatalog",
"driver_catalog",
"build_driver",
"EndpointResolver",
"CustomDriverCatalog",
"register_custom_driver",
"build_custom_driver",
"list_custom_drivers",
"custom_driver_catalog",
"TransportCoordinator",
"ProcessSupervisor",
"ProcessState",
]
24 changes: 24 additions & 0 deletions mcp_fuzzer/transport/catalog/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Driver catalog, builders, and custom driver helpers."""

from .catalog import DriverCatalog
from .builder import driver_catalog, build_driver
from .resolver import EndpointResolver
from .custom_catalog import (
CustomDriverCatalog,
register_custom_driver,
build_custom_driver,
list_custom_drivers,
custom_driver_catalog,
)

__all__ = [
"DriverCatalog",
"driver_catalog",
"build_driver",
"EndpointResolver",
"CustomDriverCatalog",
"register_custom_driver",
"build_custom_driver",
"list_custom_drivers",
"custom_driver_catalog",
]
Loading
Loading