Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.CommandLine.Parsing;
using Azure.Mcp.Core.Areas.Tools.Options;
using Azure.Mcp.Core.Commands;
using Azure.Mcp.Core.Extensions;
using Azure.Mcp.Core.Models;
using Azure.Mcp.Core.Models.Command;
using Microsoft.Extensions.Logging;

namespace Azure.Mcp.Core.Areas.Tools.Commands;

[HiddenCommand]
public sealed class ToolsListNamesCommand(ILogger<ToolsListNamesCommand> logger) : BaseCommand<ToolsListNamesOptions>
{
private const string CommandTitle = "List Tool Names";

public override string Name => "list-names";

public override string Description =>
"""
List all available tool names in the Azure MCP server. This command returns a simple list of tool names
without descriptions or metadata, useful for quick discovery or automated tool enumeration.
""";

public override string Title => CommandTitle;

public override ToolMetadata Metadata => new()
{
Destructive = false,
Idempotent = true,
OpenWorld = false,
ReadOnly = true,
LocalRequired = false,
Secret = false
};

protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.Options.Add(ToolsListOptionDefinitions.Namespace);
}

protected override ToolsListNamesOptions BindOptions(ParseResult parseResult)
{
var options = new ToolsListNamesOptions();
options.Namespace = parseResult.GetValueOrDefault<string>(ToolsListOptionDefinitions.Namespace.Name);
return options;
}

public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult)
{
try
{
var factory = context.GetService<CommandFactory>();
var options = BindOptions(parseResult);

// Get all visible commands and extract their tokenized names (full command paths)
var allToolNames = CommandFactory.GetVisibleCommands(factory.AllCommands)
.Select(kvp => kvp.Key) // Use the tokenized key instead of just the command name
.Where(name => !string.IsNullOrEmpty(name));

// Apply namespace filtering if specified
if (!string.IsNullOrEmpty(options.Namespace))
{
var namespacePrefix = $"azmcp_{options.Namespace}_";
allToolNames = allToolNames.Where(name => name.StartsWith(namespacePrefix, StringComparison.OrdinalIgnoreCase));
}

var toolNames = await Task.Run(() => allToolNames
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
.ToList());

var result = new ToolNamesResult(toolNames);
context.Response.Results = ResponseResult.Create(result, ModelsJsonContext.Default.ToolNamesResult);
return context.Response;
}
catch (Exception ex)
{
logger.LogError(ex, "An exception occurred while processing tool names listing.");
HandleException(context, ex);

return context.Response;
}
}

public record ToolNamesResult(List<string> Names);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.Mcp.Core.Areas.Tools.Options;

public sealed class ToolsListNamesOptions
{
/// <summary>
/// Optional namespace to filter tool names. If provided, only tools from this namespace will be returned.
/// </summary>
public string? Namespace { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ namespace Azure.Mcp.Core.Areas.Tools.Options;
public static class ToolsListOptionDefinitions
{
public const string NamespacesOptionName = "namespaces";
public const string NamespaceOptionName = "namespace";

public static readonly Option<bool> Namespaces = new($"--{NamespacesOptionName}")
{
Description = "If specified, returns a list of top-level service namespaces instead of individual tools.",
Required = false
};

public static readonly Option<string> Namespace = new($"--{NamespaceOptionName}")
{
Description = "Filter tools by namespace (e.g., 'storage', 'keyvault'). If specified, only tools from this namespace will be returned.",
Required = false
};
}
4 changes: 4 additions & 0 deletions core/Azure.Mcp.Core/src/Areas/Tools/ToolsSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public sealed class ToolsSetup : IAreaSetup
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ToolsListCommand>();
services.AddSingleton<ToolsListNamesCommand>();
}

public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
Expand All @@ -24,6 +25,9 @@ public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
var list = serviceProvider.GetRequiredService<ToolsListCommand>();
tools.AddCommand(list.Name, list);

var listNames = serviceProvider.GetRequiredService<ToolsListNamesCommand>();
tools.AddCommand(listNames.Name, listNames);

return tools;
}
}
2 changes: 2 additions & 0 deletions core/Azure.Mcp.Core/src/Models/ModelsJsonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.Text.Json.Serialization;
using Azure.Mcp.Core.Areas.Tools.Commands;
using Azure.Mcp.Core.Commands;
using Azure.Mcp.Core.Models.Elicitation;

Expand All @@ -13,6 +14,7 @@ namespace Azure.Mcp.Core.Models;
[JsonSerializable(typeof(ElicitationSchemaRoot))]
[JsonSerializable(typeof(ElicitationSchemaProperty))]
[JsonSerializable(typeof(ToolMetadata))]
[JsonSerializable(typeof(ToolsListNamesCommand.ToolNamesResult))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
public sealed partial class ModelsJsonContext : JsonSerializerContext
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.CommandLine;
using System.CommandLine.Parsing;
using System.Net;
using System.Text.Json;
using Azure.Mcp.Core.Areas;
using Azure.Mcp.Core.Areas.Tools.Commands;
using Azure.Mcp.Core.Areas.Tools.Options;
using Azure.Mcp.Core.Commands;
using Azure.Mcp.Core.Extensions;
using Azure.Mcp.Core.Models.Command;
using Azure.Mcp.Core.Services.Telemetry;
using Azure.Mcp.Core.UnitTests.Areas.Server;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;

namespace Azure.Mcp.Core.UnitTests.Areas.Tools.UnitTests;

public class ToolsListNamesCommandTests
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ToolsListNamesCommand> _logger;
private readonly CommandContext _context;
private readonly ToolsListNamesCommand _command;
private readonly Command _commandDefinition;

public ToolsListNamesCommandTests()
{
var collection = new ServiceCollection();
collection.AddLogging();

var commandFactory = CommandFactoryHelpers.CreateCommandFactory();
collection.AddSingleton(commandFactory);

_serviceProvider = collection.BuildServiceProvider();
_context = new(_serviceProvider);
_logger = Substitute.For<ILogger<ToolsListNamesCommand>>();
_command = new(_logger);
_commandDefinition = _command.GetCommand();
}

[Fact]
public void Constructor_InitializesCommandCorrectly()
{
// Act & Assert
Assert.Equal("list-names", _command.Name);
Assert.Contains("List all available tool names", _command.Description);
Assert.Equal("List Tool Names", _command.Title);
Assert.False(_command.Metadata.Destructive);
Assert.True(_command.Metadata.ReadOnly);
Assert.True(_command.Metadata.Idempotent);
Assert.False(_command.Metadata.Secret);
}

[Fact]
public void CanParseNamespaceOption()
{
// Arrange
var commandDefinition = _command.GetCommand();

// Act
var parseResult = commandDefinition.Parse(["--namespace", "storage"]);

// Assert
Assert.NotNull(parseResult);
Assert.False(parseResult.Errors.Any(), $"Parse errors: {string.Join(", ", parseResult.Errors)}");

var namespaceValue = parseResult.GetValueOrDefault<string>(ToolsListOptionDefinitions.Namespace.Name);
Assert.Equal("storage", namespaceValue);
}

[Fact]
public async Task ExecuteAsync_WithNamespaceOption_FiltersCorrectly()
{
// Arrange
var commandDefinition = _command.GetCommand();
var parseResult = commandDefinition.Parse(["--namespace", "storage"]);

// Act
var response = await _command.ExecuteAsync(_context, parseResult);

// Assert
Assert.NotNull(response);
Assert.Equal(HttpStatusCode.OK, response.Status);
// Note: We're not testing response.Results here since it's null in the test environment
// but we can verify the command executes without errors
}
}