2020
2121mcp : FastMCP = FastMCP ()
2222
23+ # Name of the convention tool that hub servers can implement to provide
24+ # rich usage context for AI agents (known issues, workflow tips, rules, etc.).
25+ _AGENT_CONTEXT_TOOL = "get_agent_context"
26+
2327# Global hub executor instance (lazy initialization)
2428_hub_executor : HubExecutor | None = None
2529
2630
31+ async def _fetch_agent_context (
32+ executor : HubExecutor ,
33+ server_name : str ,
34+ tools : list [Any ],
35+ ) -> str | None :
36+ """Call get_agent_context if the server provides it.
37+
38+ Returns the context string, or None if the server doesn't implement
39+ the convention or the call fails.
40+ """
41+ if not any (t .name == _AGENT_CONTEXT_TOOL for t in tools ):
42+ return None
43+ try :
44+ result = await executor .execute_tool (
45+ identifier = f"hub:{ server_name } :{ _AGENT_CONTEXT_TOOL } " ,
46+ arguments = {},
47+ )
48+ if result .success and result .result :
49+ content = result .result .get ("content" , [])
50+ if content and isinstance (content , list ):
51+ text : str = content [0 ].get ("text" , "" )
52+ return text
53+ except Exception : # noqa: BLE001, S110 - best-effort context fetch
54+ pass
55+ return None
56+
57+
2758def _get_hub_executor () -> HubExecutor :
2859 """Get or create the hub executor instance.
2960
@@ -50,19 +81,25 @@ def _get_hub_executor() -> HubExecutor:
5081
5182
5283@mcp .tool
53- async def list_hub_servers () -> dict [str , Any ]:
84+ async def list_hub_servers (category : str | None = None ) -> dict [str , Any ]:
5485 """List all registered MCP hub servers.
5586
5687 Returns information about configured hub servers, including
5788 their connection type, status, and discovered tool count.
5889
90+ :param category: Optional category to filter by (e.g. "binary-analysis",
91+ "web-security", "reconnaissance"). Only servers in this category
92+ are returned.
5993 :return: Dictionary with list of hub servers.
6094
6195 """
6296 try :
6397 executor = _get_hub_executor ()
6498 servers = executor .list_servers ()
6599
100+ if category :
101+ servers = [s for s in servers if s .get ("category" ) == category ]
102+
66103 return {
67104 "servers" : servers ,
68105 "count" : len (servers ),
@@ -93,7 +130,14 @@ async def discover_hub_tools(server_name: str | None = None) -> dict[str, Any]:
93130
94131 if server_name :
95132 tools = await executor .discover_server_tools (server_name )
96- return {
133+
134+ # Convention: auto-fetch agent context if server provides it.
135+ agent_context = await _fetch_agent_context (executor , server_name , tools )
136+
137+ # Hide the convention tool from the agent's tool list.
138+ visible_tools = [t for t in tools if t .name != "get_agent_context" ]
139+
140+ result : dict [str , Any ] = {
97141 "server" : server_name ,
98142 "tools" : [
99143 {
@@ -102,15 +146,24 @@ async def discover_hub_tools(server_name: str | None = None) -> dict[str, Any]:
102146 "description" : t .description ,
103147 "parameters" : [p .model_dump () for p in t .parameters ],
104148 }
105- for t in tools
149+ for t in visible_tools
106150 ],
107- "count" : len (tools ),
151+ "count" : len (visible_tools ),
108152 }
153+ if agent_context :
154+ result ["agent_context" ] = agent_context
155+ return result
109156 else :
110157 results = await executor .discover_all_tools ()
111158 all_tools = []
159+ contexts : dict [str , str ] = {}
112160 for server , tools in results .items ():
161+ ctx = await _fetch_agent_context (executor , server , tools )
162+ if ctx :
163+ contexts [server ] = ctx
113164 for tool in tools :
165+ if tool .name == "get_agent_context" :
166+ continue
114167 all_tools .append ({
115168 "identifier" : tool .identifier ,
116169 "name" : tool .name ,
@@ -119,11 +172,14 @@ async def discover_hub_tools(server_name: str | None = None) -> dict[str, Any]:
119172 "parameters" : [p .model_dump () for p in tool .parameters ],
120173 })
121174
122- return {
175+ result = {
123176 "servers_discovered" : len (results ),
124177 "tools" : all_tools ,
125178 "count" : len (all_tools ),
126179 }
180+ if contexts :
181+ result ["agent_contexts" ] = contexts
182+ return result
127183
128184 except Exception as e :
129185 if isinstance (e , ToolError ):
@@ -183,6 +239,11 @@ async def execute_hub_tool(
183239 Always use /app/uploads/<filename> or /app/samples/<filename> when
184240 passing file paths to hub tools — do NOT use the host path.
185241
242+ Tool outputs are persisted to a writable shared volume:
243+ - /app/output/ (writable — extraction results, reports, etc.)
244+ Files written here survive container destruction and are available
245+ to subsequent tool calls. The host path is .fuzzforge/output/.
246+
186247 """
187248 try :
188249 executor = _get_hub_executor ()
@@ -191,6 +252,7 @@ async def execute_hub_tool(
191252 # Mounts the assets directory at the standard paths used by hub tools:
192253 # /app/uploads — binwalk, and other tools that use UPLOAD_DIR
193254 # /app/samples — yara, capa, and other tools that use SAMPLES_DIR
255+ # /app/output — writable volume for tool outputs (persists across calls)
194256 extra_volumes : list [str ] = []
195257 try :
196258 storage = get_storage ()
@@ -202,6 +264,9 @@ async def execute_hub_tool(
202264 f"{ assets_str } :/app/uploads:ro" ,
203265 f"{ assets_str } :/app/samples:ro" ,
204266 ]
267+ output_path = storage .get_project_output_path (project_path )
268+ if output_path :
269+ extra_volumes .append (f"{ output_path !s} :/app/output:rw" )
205270 except Exception : # noqa: BLE001 - never block tool execution due to asset injection failure
206271 extra_volumes = []
207272
@@ -212,6 +277,20 @@ async def execute_hub_tool(
212277 extra_volumes = extra_volumes or None ,
213278 )
214279
280+ # Record execution history for list_executions / get_execution_results.
281+ try :
282+ storage = get_storage ()
283+ project_path = get_project_path ()
284+ storage .record_execution (
285+ project_path = project_path ,
286+ server_name = result .server_name ,
287+ tool_name = result .tool_name ,
288+ arguments = arguments or {},
289+ result = result .to_dict (),
290+ )
291+ except Exception : # noqa: BLE001, S110 - never fail the tool call due to recording issues
292+ pass
293+
215294 return result .to_dict ()
216295
217296 except Exception as e :
@@ -372,6 +451,9 @@ async def start_hub_server(server_name: str) -> dict[str, Any]:
372451 f"{ assets_str } :/app/uploads:ro" ,
373452 f"{ assets_str } :/app/samples:ro" ,
374453 ]
454+ output_path = storage .get_project_output_path (project_path )
455+ if output_path :
456+ extra_volumes .append (f"{ output_path !s} :/app/output:rw" )
375457 except Exception : # noqa: BLE001 - never block server start due to asset injection failure
376458 extra_volumes = []
377459
0 commit comments