diff --git a/fuzzforge-common/src/fuzzforge_common/hub/client.py b/fuzzforge-common/src/fuzzforge_common/hub/client.py index cb12dcf..5ffb43b 100644 --- a/fuzzforge-common/src/fuzzforge_common/hub/client.py +++ b/fuzzforge-common/src/fuzzforge_common/hub/client.py @@ -539,6 +539,7 @@ class HubClient: async def start_persistent_session( self, config: HubServerConfig, + extra_volumes: list[str] | None = None, ) -> PersistentSession: """Start a persistent Docker container and initialise MCP session. @@ -546,6 +547,7 @@ class HubClient: called, allowing multiple tool calls on the same session. :param config: Server configuration (must be Docker type). + :param extra_volumes: Additional host:container volume mounts to inject. :returns: The created persistent session. :raises HubClientError: If the container cannot be started. @@ -590,6 +592,9 @@ class HubClient: for volume in config.volumes: cmd.extend(["-v", os.path.expanduser(volume)]) + for extra_vol in (extra_volumes or []): + cmd.extend(["-v", extra_vol]) + for key, value in config.environment.items(): cmd.extend(["-e", f"{key}={value}"]) diff --git a/fuzzforge-common/src/fuzzforge_common/hub/executor.py b/fuzzforge-common/src/fuzzforge_common/hub/executor.py index ab3b293..37205e1 100644 --- a/fuzzforge-common/src/fuzzforge_common/hub/executor.py +++ b/fuzzforge-common/src/fuzzforge_common/hub/executor.py @@ -345,13 +345,14 @@ class HubExecutor: # Persistent session management # ------------------------------------------------------------------ - async def start_persistent_server(self, server_name: str) -> dict[str, Any]: + async def start_persistent_server(self, server_name: str, extra_volumes: list[str] | None = None) -> dict[str, Any]: """Start a persistent container session for a server. The container stays running between tool calls, allowing stateful interactions (e.g., radare2 sessions, long-running fuzzing). :param server_name: Name of the hub server to start. + :param extra_volumes: Additional host:container volume mounts to inject. :returns: Session status dictionary. :raises ValueError: If server not found. @@ -362,7 +363,7 @@ class HubExecutor: msg = f"Server '{server_name}' not found" raise ValueError(msg) - session = await self._client.start_persistent_session(server.config) + session = await self._client.start_persistent_session(server.config, extra_volumes=extra_volumes) # Auto-discover tools on the new session try: diff --git a/fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py b/fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py index bd37906..33d724a 100644 --- a/fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py +++ b/fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py @@ -172,9 +172,16 @@ async def execute_hub_tool( :return: Tool execution result. Example identifiers: + - "hub:binwalk-mcp:binwalk_scan" + - "hub:yara-mcp:yara_scan_with_rules" - "hub:nmap:nmap_scan" - - "nmap:nmap_scan" - - "hub:nuclei:nuclei_scan" + + FILE ACCESS — if set_project_assets was called, the assets directory is + mounted read-only inside the container at two standard paths: + - /app/uploads/ (used by binwalk, and tools with UPLOAD_DIR) + - /app/samples/ (used by yara, capa, and tools with SAMPLES_DIR) + Always use /app/uploads/ or /app/samples/ when + passing file paths to hub tools — do NOT use the host path. """ try: @@ -353,7 +360,22 @@ async def start_hub_server(server_name: str) -> dict[str, Any]: try: executor = _get_hub_executor() - result = await executor.start_persistent_server(server_name) + # Inject project assets as Docker volume mounts (same logic as execute_hub_tool). + extra_volumes: list[str] = [] + try: + storage = get_storage() + project_path = get_project_path() + assets_path = storage.get_project_assets_path(project_path) + if assets_path: + assets_str = str(assets_path) + extra_volumes = [ + f"{assets_str}:/app/uploads:ro", + f"{assets_str}:/app/samples:ro", + ] + except Exception: # noqa: BLE001 - never block server start due to asset injection failure + extra_volumes = [] + + result = await executor.start_persistent_server(server_name, extra_volumes=extra_volumes or None) return { "success": True,