11// Copyright (c) Microsoft Corporation.
22// Licensed under the MIT License.
33
4+ using System . CommandLine . Parsing ;
45using System . Runtime . InteropServices ;
56using Azure . Mcp . Core . Areas . Tools . Options ;
67using Azure . Mcp . Core . Commands ;
8+ using Azure . Mcp . Core . Extensions ;
9+ using Azure . Mcp . Core . Models ;
10+ using Azure . Mcp . Core . Models . Command ;
711using Azure . Mcp . Core . Models . Option ;
812using Microsoft . AspNetCore . Mvc . ModelBinding ;
913using Microsoft . Extensions . Logging ;
@@ -23,7 +27,7 @@ public sealed class ToolsListCommand(ILogger<ToolsListCommand> logger) : BaseCom
2327 """
2428 List all available commands and their tools in a hierarchical structure. This command returns detailed information
2529 about each command, including its name, description, full command path, available subcommands, and all supported
26- arguments. Use this to explore the CLI's functionality or to build interactive command interfaces .
30+ arguments. Use --name-only to return only tool names, and --namespace to filter by specific namespaces .
2731 """ ;
2832
2933 public override string Title => CommandTitle ;
@@ -41,14 +45,19 @@ arguments. Use this to explore the CLI's functionality or to build interactive c
4145 protected override void RegisterOptions ( Command command )
4246 {
4347 base . RegisterOptions ( command ) ;
44- command . Options . Add ( ToolsListOptionDefinitions . Namespaces ) ;
48+ command . Options . Add ( ToolsListOptionDefinitions . NamespaceMode ) ;
49+ command . Options . Add ( ToolsListOptionDefinitions . Namespace ) ;
50+ command . Options . Add ( ToolsListOptionDefinitions . NameOnly ) ;
4551 }
4652
4753 protected override ToolsListOptions BindOptions ( ParseResult parseResult )
4854 {
55+ var namespaces = parseResult . GetValueOrDefault < string [ ] > ( ToolsListOptionDefinitions . Namespace . Name ) ?? [ ] ;
4956 return new ToolsListOptions
5057 {
51- Namespaces = parseResult . GetValueOrDefault ( ToolsListOptionDefinitions . Namespaces )
58+ NamespaceMode = parseResult . GetValueOrDefault ( ToolsListOptionDefinitions . NamespaceMode ) ,
59+ NameOnly = parseResult . GetValueOrDefault ( ToolsListOptionDefinitions . NameOnly ) ,
60+ Namespaces = namespaces . ToList ( )
5261 } ;
5362 }
5463
@@ -59,15 +68,17 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
5968 var factory = context . GetService < CommandFactory > ( ) ;
6069 var options = BindOptions ( parseResult ) ;
6170
62- // If the --namespaces flag is set, return distinct top‑level namespaces (child groups beneath root 'azmcp').
63- if ( options . Namespaces )
71+ // If the --namespace-mode flag is set, return distinct top‑level namespaces (e.g. child groups beneath root 'azmcp').
72+ if ( options . NamespaceMode )
6473 {
6574 var ignored = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) { "server" , "tools" } ;
6675 var surfaced = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) { "extension" } ;
6776 var rootGroup = factory . RootGroup ; // azmcp
6877
6978 var namespaceCommands = rootGroup . SubGroup
7079 . Where ( g => ! ignored . Contains ( g . Name ) && ! surfaced . Contains ( g . Name ) )
80+ // Apply namespace filtering if specified
81+ . Where ( g => options . Namespaces . Count == 0 || options . Namespaces . Contains ( g . Name , StringComparer . OrdinalIgnoreCase ) )
7182 . Select ( g => new CommandInfo
7283 {
7384 Name = g . Name ,
@@ -82,6 +93,10 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
8293 // For commands in the surfaced list, each command is exposed as a separate tool in the namespace mode.
8394 foreach ( var name in surfaced )
8495 {
96+ // Apply namespace filtering for surfaced commands too
97+ if ( options . Namespaces . Count > 0 && ! options . Namespaces . Contains ( name , StringComparer . OrdinalIgnoreCase ) )
98+ continue ;
99+
85100 var subgroup = rootGroup . SubGroup . FirstOrDefault ( g => string . Equals ( g . Name , name , StringComparison . OrdinalIgnoreCase ) ) ;
86101 if ( subgroup is not null )
87102 {
@@ -91,13 +106,49 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
91106 }
92107 }
93108
109+ // If --name-only is also specified, return only the names
110+ if ( options . NameOnly )
111+ {
112+ var namespaceNames = namespaceCommands . Select ( nc => nc . Command ) . ToList ( ) ;
113+ var result = new ToolNamesResult ( namespaceNames ) ;
114+ context . Response . Results = ResponseResult . Create ( result , ModelsJsonContext . Default . ToolNamesResult ) ;
115+ return context . Response ;
116+ }
117+
94118 context . Response . Results = ResponseResult . Create ( namespaceCommands , ModelsJsonContext . Default . ListCommandInfo ) ;
95119 return context . Response ;
96120 }
97121
98- var tools = await Task . Run ( ( ) => CommandFactory . GetVisibleCommands ( factory . AllCommands )
99- . Select ( kvp => CreateCommand ( kvp . Key , kvp . Value ) )
100- . ToList ( ) ) ;
122+ // If the --name-only flag is set (without namespace mode), return only tool names
123+ if ( options . NameOnly )
124+ {
125+ // Get all visible commands and extract their tokenized names (full command paths)
126+ var allToolNames = CommandFactory . GetVisibleCommands ( factory . AllCommands )
127+ . Select ( kvp => kvp . Key ) // Use the tokenized key instead of just the command name
128+ . Where ( name => ! string . IsNullOrEmpty ( name ) ) ;
129+
130+ // Apply namespace filtering if specified (using underscore separator for tokenized names)
131+ allToolNames = ApplyNamespaceFilterToNames ( allToolNames , options . Namespaces , CommandFactory . Separator ) ;
132+
133+ var toolNames = await Task . Run ( ( ) => allToolNames
134+ . OrderBy ( name => name , StringComparer . OrdinalIgnoreCase )
135+ . ToList ( ) ) ;
136+
137+ var result = new ToolNamesResult ( toolNames ) ;
138+ context . Response . Results = ResponseResult . Create ( result , ModelsJsonContext . Default . ToolNamesResult ) ;
139+ return context . Response ;
140+ }
141+
142+ // Get all tools with full details
143+ var allTools = CommandFactory . GetVisibleCommands ( factory . AllCommands )
144+ . Select ( kvp => CreateCommand ( kvp . Key , kvp . Value ) ) ;
145+
146+ // Apply namespace filtering if specified
147+ var filteredToolNames = ApplyNamespaceFilterToNames ( allTools . Select ( t => t . Command ) , options . Namespaces , ' ' ) ;
148+ var filteredToolNamesSet = filteredToolNames . ToHashSet ( StringComparer . OrdinalIgnoreCase ) ;
149+ allTools = allTools . Where ( tool => filteredToolNamesSet . Contains ( tool . Command ) ) ;
150+
151+ var tools = await Task . Run ( ( ) => allTools . ToList ( ) ) ;
101152
102153 context . Response . Results = ResponseResult . Create ( tools , ModelsJsonContext . Default . ListCommandInfo ) ;
103154 return context . Response ;
@@ -111,6 +162,19 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
111162 }
112163 }
113164
165+ private static IEnumerable < string > ApplyNamespaceFilterToNames ( IEnumerable < string > names , List < string > namespaces , char separator )
166+ {
167+ if ( namespaces . Count == 0 )
168+ {
169+ return names ;
170+ }
171+
172+ var namespacePrefixes = namespaces . Select ( ns => $ "{ ns } { separator } ") . ToList ( ) ;
173+
174+ return names . Where ( name =>
175+ namespacePrefixes . Any ( prefix => name . StartsWith ( prefix , StringComparison . OrdinalIgnoreCase ) ) ) ;
176+ }
177+
114178 private static CommandInfo CreateCommand ( string tokenizedName , IBaseCommand command )
115179 {
116180 var commandDetails = command . GetCommand ( ) ;
@@ -122,17 +186,20 @@ private static CommandInfo CreateCommand(string tokenizedName, IBaseCommand comm
122186 required : arg . Required ) )
123187 . ToList ( ) ;
124188
189+ var fullCommand = tokenizedName . Replace ( CommandFactory . Separator , ' ' ) ;
190+
125191 return new CommandInfo
126192 {
127193 Id = command . Id ,
128194 Name = commandDetails . Name ,
129195 Description = commandDetails . Description ?? string . Empty ,
130- Command = tokenizedName . Replace ( CommandFactory . Separator , ' ' ) ,
196+ Command = fullCommand ,
131197 Options = optionInfos ,
132198 Metadata = command . Metadata
133199 } ;
134200 }
135201
202+ public record ToolNamesResult ( List < string > Names ) ;
136203 private void searchCommandInCommandGroup ( string commandPrefix , CommandGroup searchedGroup , List < CommandInfo > foundCommands )
137204 {
138205 var commands = CommandFactory . GetVisibleCommands ( searchedGroup . Commands ) . Select ( kvp =>
0 commit comments