@@ -2371,6 +2371,40 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
23712371 let skills = load_skills_from_roots ( & roots) ?;
23722372 Ok ( render_skills_report ( & skills) )
23732373 }
2374+ Some ( args) if args. starts_with ( "list " ) => {
2375+ let filter = args[ "list " . len ( ) ..] . trim ( ) . to_lowercase ( ) ;
2376+ let roots = discover_skill_roots ( cwd) ;
2377+ let skills = load_skills_from_roots ( & roots) ?;
2378+ let filtered: Vec < _ > = skills
2379+ . into_iter ( )
2380+ . filter ( |s| s. name . to_lowercase ( ) . contains ( & filter) )
2381+ . collect ( ) ;
2382+ Ok ( render_skills_report ( & filtered) )
2383+ }
2384+ Some ( "show" | "info" | "describe" ) => {
2385+ let roots = discover_skill_roots ( cwd) ;
2386+ let skills = load_skills_from_roots ( & roots) ?;
2387+ Ok ( render_skills_report ( & skills) )
2388+ }
2389+ Some ( args)
2390+ if args. starts_with ( "show " )
2391+ || args. starts_with ( "info " )
2392+ || args. starts_with ( "describe " ) =>
2393+ {
2394+ let name = args
2395+ . splitn ( 2 , ' ' )
2396+ . nth ( 1 )
2397+ . unwrap_or_default ( )
2398+ . trim ( )
2399+ . to_lowercase ( ) ;
2400+ let roots = discover_skill_roots ( cwd) ;
2401+ let skills = load_skills_from_roots ( & roots) ?;
2402+ let matched: Vec < _ > = skills
2403+ . into_iter ( )
2404+ . filter ( |s| s. name . to_lowercase ( ) == name)
2405+ . collect ( ) ;
2406+ Ok ( render_skills_report ( & matched) )
2407+ }
23742408 Some ( "install" ) => Ok ( render_skills_usage ( Some ( "install" ) ) ) ,
23752409 Some ( args) if args. starts_with ( "install " ) => {
23762410 let target = args[ "install " . len ( ) ..] . trim ( ) ;
@@ -2402,6 +2436,40 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
24022436 let skills = load_skills_from_roots ( & roots) ?;
24032437 Ok ( render_skills_report_json ( & skills) )
24042438 }
2439+ Some ( args) if args. starts_with ( "list " ) => {
2440+ let filter = args[ "list " . len ( ) ..] . trim ( ) . to_lowercase ( ) ;
2441+ let roots = discover_skill_roots ( cwd) ;
2442+ let skills = load_skills_from_roots ( & roots) ?;
2443+ let filtered: Vec < _ > = skills
2444+ . into_iter ( )
2445+ . filter ( |s| s. name . to_lowercase ( ) . contains ( & filter) )
2446+ . collect ( ) ;
2447+ Ok ( render_skills_report_json ( & filtered) )
2448+ }
2449+ Some ( "show" | "info" | "describe" ) => {
2450+ let roots = discover_skill_roots ( cwd) ;
2451+ let skills = load_skills_from_roots ( & roots) ?;
2452+ Ok ( render_skills_report_json ( & skills) )
2453+ }
2454+ Some ( args)
2455+ if args. starts_with ( "show " )
2456+ || args. starts_with ( "info " )
2457+ || args. starts_with ( "describe " ) =>
2458+ {
2459+ let name = args
2460+ . splitn ( 2 , ' ' )
2461+ . nth ( 1 )
2462+ . unwrap_or_default ( )
2463+ . trim ( )
2464+ . to_lowercase ( ) ;
2465+ let roots = discover_skill_roots ( cwd) ;
2466+ let skills = load_skills_from_roots ( & roots) ?;
2467+ let matched: Vec < _ > = skills
2468+ . into_iter ( )
2469+ . filter ( |s| s. name . to_lowercase ( ) == name)
2470+ . collect ( ) ;
2471+ Ok ( render_skills_report_json ( & matched) )
2472+ }
24052473 Some ( "install" ) => Ok ( render_skills_usage_json ( Some ( "install" ) ) ) ,
24062474 Some ( args) if args. starts_with ( "install " ) => {
24072475 let target = args[ "install " . len ( ) ..] . trim ( ) ;
@@ -2419,10 +2487,20 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
24192487#[ must_use]
24202488pub fn classify_skills_slash_command ( args : Option < & str > ) -> SkillSlashDispatch {
24212489 match normalize_optional_args ( args) {
2422- None | Some ( "list" | "help" | "-h" | "--help" ) => SkillSlashDispatch :: Local ,
2490+ None | Some ( "list" | "help" | "-h" | "--help" | "show" | "info" | "describe" ) => {
2491+ SkillSlashDispatch :: Local
2492+ }
24232493 Some ( args) if args == "install" || args. starts_with ( "install " ) => {
24242494 SkillSlashDispatch :: Local
24252495 }
2496+ Some ( args)
2497+ if args. starts_with ( "list " )
2498+ || args. starts_with ( "show " )
2499+ || args. starts_with ( "info " )
2500+ || args. starts_with ( "describe " ) =>
2501+ {
2502+ SkillSlashDispatch :: Local
2503+ }
24262504 Some ( args) => SkillSlashDispatch :: Invoke ( format ! ( "${}" , args. trim_start_matches( '/' ) ) ) ,
24272505 }
24282506}
@@ -2596,10 +2674,44 @@ fn render_mcp_report_for(
25962674 ) ) ,
25972675 }
25982676 }
2677+ Some ( args) if args. split_whitespace ( ) . next ( ) == Some ( "list" ) && args. contains ( ' ' ) => {
2678+ // `mcp list <filter>` — list does not accept arguments; treat as unsupported action.
2679+ Ok ( render_mcp_unsupported_action_text (
2680+ args,
2681+ "list accepts no filter argument; use `claw mcp list`" ,
2682+ ) )
2683+ }
2684+ Some ( args) if matches ! ( args. split_whitespace( ) . next( ) , Some ( "info" | "describe" ) ) => {
2685+ Ok ( render_mcp_unsupported_action_text (
2686+ args,
2687+ "use `claw mcp show <server>` to inspect a server" ,
2688+ ) )
2689+ }
25992690 Some ( args) => Ok ( render_mcp_usage ( Some ( args) ) ) ,
26002691 }
26012692}
26022693
2694+ fn render_mcp_unsupported_action_text ( action : & str , hint : & str ) -> String {
2695+ format ! (
2696+ "MCP\n Error unsupported action '{action}'\n Hint {hint}\n Usage /mcp [list|show <server>|help]"
2697+ )
2698+ }
2699+
2700+ fn render_mcp_unsupported_action_json ( action : & str , hint : & str ) -> Value {
2701+ json ! ( {
2702+ "kind" : "mcp" ,
2703+ "action" : "error" ,
2704+ "ok" : false ,
2705+ "error_kind" : "unsupported_action" ,
2706+ "requested_action" : action,
2707+ "hint" : hint,
2708+ "usage" : {
2709+ "slash_command" : "/mcp [list|show <server>|help]" ,
2710+ "direct_cli" : "claw mcp [list|show <server>|help]" ,
2711+ } ,
2712+ } )
2713+ }
2714+
26032715fn render_mcp_report_json_for (
26042716 loader : & ConfigLoader ,
26052717 cwd : & Path ,
@@ -2680,6 +2792,18 @@ fn render_mcp_report_json_for(
26802792 } ) ) ,
26812793 }
26822794 }
2795+ Some ( args) if args. split_whitespace ( ) . next ( ) == Some ( "list" ) && args. contains ( ' ' ) => {
2796+ Ok ( render_mcp_unsupported_action_json (
2797+ args,
2798+ "list accepts no filter argument; use `claw mcp list`" ,
2799+ ) )
2800+ }
2801+ Some ( args) if matches ! ( args. split_whitespace( ) . next( ) , Some ( "info" | "describe" ) ) => {
2802+ Ok ( render_mcp_unsupported_action_json (
2803+ args,
2804+ "use `claw mcp show <server>` to inspect a server" ,
2805+ ) )
2806+ }
26832807 Some ( args) => Ok ( render_mcp_usage_json ( Some ( args) ) ) ,
26842808 }
26852809}
@@ -4619,6 +4743,32 @@ mod tests {
46194743 assert ! ( agents_error. contains( " Usage /agents [list|help]" ) ) ;
46204744 }
46214745
4746+ #[ test]
4747+ fn skills_show_and_list_filter_do_not_invoke_model ( ) {
4748+ // `show`, `info`, `list <filter>` must route to Local, not Invoke.
4749+ // Regression for: `claw skills show plan` unexpectedly spawned a model session.
4750+ for token in & [ "show" , "info" , "describe" ] {
4751+ assert_eq ! (
4752+ classify_skills_slash_command( Some ( token) ) ,
4753+ SkillSlashDispatch :: Local ,
4754+ "`skills {token}` alone must be Local"
4755+ ) ;
4756+ }
4757+ for prefix in & [ "show " , "info " , "list " , "describe " ] {
4758+ let arg = format ! ( "{prefix}plan" ) ;
4759+ assert_eq ! (
4760+ classify_skills_slash_command( Some ( & arg) ) ,
4761+ SkillSlashDispatch :: Local ,
4762+ "`skills {arg}` must be Local, not Invoke"
4763+ ) ;
4764+ }
4765+ // Bare invocable tokens still dispatch to Invoke.
4766+ assert_eq ! (
4767+ classify_skills_slash_command( Some ( "plan" ) ) ,
4768+ SkillSlashDispatch :: Invoke ( "$plan" . to_string( ) ) ,
4769+ ) ;
4770+ }
4771+
46224772 #[ test]
46234773 fn accepts_skills_invocation_arguments_for_prompt_dispatch ( ) {
46244774 assert_eq ! (
@@ -4641,6 +4791,38 @@ mod tests {
46414791 ) ;
46424792 }
46434793
4794+ #[ test]
4795+ fn mcp_unsupported_actions_return_typed_error_not_generic_help ( ) {
4796+ // `mcp info <name>` and `mcp list <filter>` must return typed errors, not raw help.
4797+ // Regression for #504: these previously fell through to render_mcp_usage with
4798+ // unexpected=arg, giving no machine-readable error_kind.
4799+ use crate :: handle_mcp_slash_command_json;
4800+ use std:: path:: PathBuf ;
4801+ let cwd = PathBuf :: from ( "/tmp" ) ;
4802+
4803+ let info_json = handle_mcp_slash_command_json ( Some ( "info nonexistent" ) , & cwd)
4804+ . expect ( "info nonexistent should not error at IO level" ) ;
4805+ assert_eq ! ( info_json[ "kind" ] , "mcp" ) ;
4806+ assert_eq ! ( info_json[ "ok" ] , false ) ;
4807+ assert_eq ! ( info_json[ "error_kind" ] , "unsupported_action" ) ;
4808+ assert ! ( info_json[ "hint" ]
4809+ . as_str( )
4810+ . unwrap_or_default( )
4811+ . contains( "show" ) ) ;
4812+
4813+ let list_filter_json = handle_mcp_slash_command_json ( Some ( "list nonexistent" ) , & cwd)
4814+ . expect ( "list nonexistent should not error at IO level" ) ;
4815+ assert_eq ! ( list_filter_json[ "kind" ] , "mcp" ) ;
4816+ assert_eq ! ( list_filter_json[ "ok" ] , false ) ;
4817+ assert_eq ! ( list_filter_json[ "error_kind" ] , "unsupported_action" ) ;
4818+
4819+ let describe_json = handle_mcp_slash_command_json ( Some ( "describe myserver" ) , & cwd)
4820+ . expect ( "describe myserver should not error at IO level" ) ;
4821+ assert_eq ! ( describe_json[ "kind" ] , "mcp" ) ;
4822+ assert_eq ! ( describe_json[ "ok" ] , false ) ;
4823+ assert_eq ! ( describe_json[ "error_kind" ] , "unsupported_action" ) ;
4824+ }
4825+
46444826 #[ test]
46454827 fn rejects_invalid_mcp_arguments ( ) {
46464828 let show_error = parse_error_message ( "/mcp show alpha beta" ) ;
0 commit comments