@@ -111,10 +111,11 @@ impl MetaTool for ListAllToolsTool {
111111 }
112112
113113 fn description ( & self ) -> & ' static str {
114- "List every tool installed in the caller's resolved Space, without \
115- the current FeatureSet filter applied. Use this to see what the \
116- workspace could expose before composing a custom FeatureSet. \
117- Returns an array of {server_id, qualified_name, description, available}."
114+ "Operator/diagnostic: list every tool installed in the caller's resolved \
115+ Space (ignores FeatureSet filter on the roster). Each entry includes \
116+ server_available (seen on the connected server) and invokable (callable \
117+ via mcpmux_invoke_tool with current grants). Agents should prefer \
118+ mcpmux_search_tools for discovery — only invokable tools can be invoked."
118119 }
119120
120121 fn input_schema ( & self ) -> Value {
@@ -130,8 +131,26 @@ impl MetaTool for ListAllToolsTool {
130131 }
131132
132133 async fn call ( & self , call : MetaToolCall < ' _ > ) -> Result < CallToolResult , MetaToolError > {
134+ let resolved = caller_resolution ( & call) . await ?;
133135 let space_id = caller_space_id ( & call) . await ?;
134136 let server_filter = call. args . get ( "server_id" ) . and_then ( |v| v. as_str ( ) ) ;
137+
138+ let invokable = call
139+ . ctx
140+ . feature_service
141+ . get_invokable_tools_for_grants (
142+ & space_id. to_string ( ) ,
143+ & resolved. feature_set_ids ,
144+ call. session_id ,
145+ )
146+ . await
147+ . map_err ( |e| MetaToolError :: Internal ( e. to_string ( ) ) ) ?;
148+ let invokable_names: HashSet < String > = invokable
149+ . iter ( )
150+ . filter ( |f| f. feature_type == FeatureType :: Tool )
151+ . map ( |f| f. qualified_name ( ) )
152+ . collect ( ) ;
153+
135154 let features = call
136155 . ctx
137156 . server_feature_repo
@@ -142,15 +161,26 @@ impl MetaTool for ListAllToolsTool {
142161 . filter ( |f| f. feature_type == FeatureType :: Tool )
143162 . filter ( |f| server_filter. is_none_or ( |sid| f. server_id == sid) )
144163 . map ( |f| {
164+ let qualified_name = f. qualified_name ( ) ;
145165 json ! ( {
146166 "server_id" : f. server_id,
147- "qualified_name" : f . qualified_name( ) ,
167+ "qualified_name" : qualified_name,
148168 "description" : f. description,
149- "available" : f. is_available,
169+ "server_available" : f. is_available,
170+ "invokable" : invokable_names. contains( & qualified_name) ,
150171 } )
151172 } )
152173 . collect ( ) ;
153- Ok ( text_result ( json ! ( { "tools" : tools } ) ) )
174+ let total_invokable = tools
175+ . iter ( )
176+ . filter ( |t| t. get ( "invokable" ) == Some ( & json ! ( true ) ) )
177+ . count ( ) ;
178+ Ok ( text_result ( json ! ( {
179+ "tools" : tools,
180+ "total_installed" : tools. len( ) ,
181+ "total_invokable" : total_invokable,
182+ "hint" : "Use mcpmux_search_tools for agent discovery. Only invokable tools can be invoked with current FeatureSet grants." ,
183+ } ) ) )
154184 }
155185}
156186
@@ -435,6 +465,47 @@ impl MetaTool for SearchToolsTool {
435465// mcpmux_get_tool_schema — read
436466// ---------------------------------------------------------------------------
437467
468+ /// Parse the `tools` argument from `mcpmux_get_tool_schema` call args.
469+ ///
470+ /// Accepts a qualified name string, a string array, or a JSON-encoded array
471+ /// string (common when agents double-serialize through MCP clients).
472+ fn parse_tool_schema_names ( value : Option < & Value > ) -> Result < Vec < String > , MetaToolError > {
473+ let Some ( value) = value else {
474+ return Err ( MetaToolError :: InvalidArgument (
475+ "missing or invalid `tools` — expected string or string array" . into ( ) ,
476+ ) ) ;
477+ } ;
478+
479+ match value {
480+ Value :: String ( s) => {
481+ if let Ok ( Value :: Array ( arr) ) = serde_json:: from_str ( s) {
482+ return names_from_json_array ( & arr) ;
483+ }
484+ Ok ( vec ! [ s. clone( ) ] )
485+ }
486+ Value :: Array ( arr) => names_from_json_array ( arr) ,
487+ _ => Err ( MetaToolError :: InvalidArgument (
488+ "missing or invalid `tools` — expected string or string array" . into ( ) ,
489+ ) ) ,
490+ }
491+ }
492+
493+ /// Collect non-empty qualified tool names from a JSON string array.
494+ fn names_from_json_array ( arr : & [ Value ] ) -> Result < Vec < String > , MetaToolError > {
495+ let names: Vec < String > = arr
496+ . iter ( )
497+ . filter_map ( |v| v. as_str ( ) . map ( str:: trim) )
498+ . filter ( |s| !s. is_empty ( ) )
499+ . map ( str:: to_string)
500+ . collect ( ) ;
501+ if names. is_empty ( ) {
502+ return Err ( MetaToolError :: InvalidArgument (
503+ "`tools` must contain at least one qualified name" . into ( ) ,
504+ ) ) ;
505+ }
506+ Ok ( names)
507+ }
508+
438509pub struct GetToolSchemaTool ;
439510
440511#[ async_trait]
@@ -445,8 +516,10 @@ impl MetaTool for GetToolSchemaTool {
445516
446517 fn description ( & self ) -> & ' static str {
447518 "Load input schemas for one or more qualified tool names before \
448- invoking via mcpmux_invoke_tool. Pass tools as a string or array. \
449- Set compact: true to omit descriptions."
519+ invoking via mcpmux_invoke_tool. Pass tools as a single qualified \
520+ name string or a string array (e.g. [\" github_list_issues\" ]). \
521+ Set compact: true to omit descriptions. Tools must be invokable \
522+ with current grants — use mcpmux_search_tools to discover names."
450523 }
451524
452525 fn input_schema ( & self ) -> Value {
@@ -469,24 +542,7 @@ impl MetaTool for GetToolSchemaTool {
469542 let resolved = caller_resolution ( & call) . await ?;
470543 let space_id = caller_space_id ( & call) . await ?;
471544
472- let tool_names: Vec < String > = match call. args . get ( "tools" ) {
473- Some ( Value :: String ( s) ) => vec ! [ s. clone( ) ] ,
474- Some ( Value :: Array ( arr) ) => arr
475- . iter ( )
476- . filter_map ( |v| v. as_str ( ) . map ( String :: from) )
477- . collect ( ) ,
478- _ => {
479- return Err ( MetaToolError :: InvalidArgument (
480- "missing or invalid `tools` — expected string or string array" . into ( ) ,
481- ) ) ;
482- }
483- } ;
484-
485- if tool_names. is_empty ( ) {
486- return Err ( MetaToolError :: InvalidArgument (
487- "`tools` must contain at least one qualified name" . into ( ) ,
488- ) ) ;
489- }
545+ let tool_names = parse_tool_schema_names ( call. args . get ( "tools" ) ) ?;
490546
491547 let compact = call
492548 . args
@@ -518,7 +574,32 @@ impl MetaTool for GetToolSchemaTool {
518574 compact,
519575 ) ;
520576
521- Ok ( text_result ( json ! ( { "schemas" : schemas } ) ) )
577+ let found_names: HashSet < String > = schemas
578+ . iter ( )
579+ . filter_map ( |s| {
580+ s. get ( "qualified_name" )
581+ . and_then ( |v| v. as_str ( ) )
582+ . map ( str:: to_string)
583+ } )
584+ . collect ( ) ;
585+ let missing: Vec < & String > = tool_names
586+ . iter ( )
587+ . filter ( |name| !found_names. contains ( * name) )
588+ . collect ( ) ;
589+
590+ if missing. is_empty ( ) {
591+ return Ok ( text_result ( json ! ( { "schemas" : schemas } ) ) ) ;
592+ }
593+
594+ let missing_list: Vec < & str > = missing. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
595+ Ok ( text_result ( json ! ( {
596+ "schemas" : schemas,
597+ "missing" : missing_list,
598+ "message" : format!(
599+ "{} tool(s) not invokable or unknown with current grants → use mcpmux_search_tools to discover allowed names" ,
600+ missing. len( )
601+ ) ,
602+ } ) ) )
522603 }
523604}
524605
0 commit comments