Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b2903b0
Add the option to start Azure MCP server with a list of specific tools
fanyang-mono Oct 2, 2025
800115f
Fix format
fanyang-mono Oct 3, 2025
d81a602
Merge remote-tracking branch 'origin/main' into tool_mode
fanyang-mono Oct 3, 2025
1f7e9a7
Fix tests with new syntax
fanyang-mono Oct 3, 2025
c034ec6
Update CHANGELOG
fanyang-mono Oct 3, 2025
97d08c7
Make namespace mode and registry based tool loader honor --tool swith…
fanyang-mono Oct 6, 2025
fefa503
Update all docs
fanyang-mono Oct 6, 2025
1236b5b
Merge remote-tracking branch 'origin/main' into tool_mode
fanyang-mono Oct 6, 2025
74a92db
Update core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceOptionDefi…
fanyang-mono Oct 7, 2025
f651051
Update docs/azmcp-commands.md
fanyang-mono Oct 7, 2025
4efa111
Fix format
fanyang-mono Oct 7, 2025
46d2cab
Merge remote-tracking branch 'origin/main' into tool_mode
fanyang-mono Oct 7, 2025
4004c2b
Fix format
fanyang-mono Oct 7, 2025
2bde267
Address review feedbacks
fanyang-mono Oct 7, 2025
670a227
When --tool is used, It automatically switches to `all` mode.
fanyang-mono Oct 7, 2025
d623d6f
Update doc to provide clear explanation on --tool behavior
fanyang-mono Oct 7, 2025
0ba9856
Check for substring match instead of exact string match
fanyang-mono Oct 8, 2025
61a6729
Merge remote-tracking branch 'origin/main' into tool_mode
fanyang-mono Oct 8, 2025
8d0a2b7
Fix format
fanyang-mono Oct 8, 2025
f552dd9
Update area name
fanyang-mono Oct 8, 2025
2a2af8d
Disallow --namespace and --tool options being used together
fanyang-mono Oct 8, 2025
2d37dc5
Merge remote-tracking branch 'origin/main' into tool_mode
fanyang-mono Oct 8, 2025
c0373c3
Clean up
fanyang-mono Oct 8, 2025
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
17 changes: 16 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,13 +283,28 @@ Optional `--namespace` and `--mode` parameters can be used to configure differen
}
```

**Specific Tool Mode** (expose only specific tools):

```json
{
"servers": {
"azure-mcp-server": {
"type": "stdio",
"command": "<absolute-path-to>/mcp/servers/Azure.Mcp.Server/src/bin/Debug/net9.0/azmcp[.exe]",
"args": ["server", "start", "--mode", "all", "--tool", "azmcp_storage_account_get", "--tool", "azmcp_subscription_list"]
}
}
}
```

> **Server Mode Summary:**
>
> - **Default Mode**: No additional parameters - exposes all tools individually
> - **Namespace Mode**: `--namespace <service-name>` - expose specific services
> - **Namespace Proxy Mode**: `--mode namespace` - collapse tools by namespace (useful for VS Code's 128 tool limit)
> - **Single Tool Mode**: `--mode single` - single "azure" tool with internal routing
> - **Combined Mode**: Both `--namespace` and `--mode` can be used together
> - **Specific Tool Mode**: `--tool <tool-name>` - expose only specific tools by name (finest granularity)
> - **Combined Mode**: Multiple options can be used together (`--namespace` + `--mode`, `--tool` + `--mode`, etc.)

#### Start from IDE

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public static IServiceCollection AddAzureMcpServer(this IServiceCollection servi
Namespace = serviceStartOptions.Namespace,
ReadOnly = serviceStartOptions.ReadOnly ?? false,
InsecureDisableElicitation = serviceStartOptions.InsecureDisableElicitation,
Tool = serviceStartOptions.Tool,
};

if (serviceStartOptions.Mode == ModeTypes.NamespaceProxy)
Expand Down Expand Up @@ -120,7 +121,8 @@ public static IServiceCollection AddAzureMcpServer(this IServiceCollection servi
var utilityToolLoaderOptions = new ToolLoaderOptions(
Namespace: Discovery.DiscoveryConstants.UtilityNamespaces,
ReadOnly: defaultToolLoaderOptions.ReadOnly,
InsecureDisableElicitation: defaultToolLoaderOptions.InsecureDisableElicitation
InsecureDisableElicitation: defaultToolLoaderOptions.InsecureDisableElicitation,
Tool: defaultToolLoaderOptions.Tool
);

toolLoaders.Add(new CommandFactoryToolLoader(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ protected override void RegisterOptions(Command command)
command.Options.Add(ServiceOptionDefinitions.Transport);
command.Options.Add(ServiceOptionDefinitions.Namespace);
command.Options.Add(ServiceOptionDefinitions.Mode);
command.Options.Add(ServiceOptionDefinitions.Tool);
command.Options.Add(ServiceOptionDefinitions.ReadOnly);
command.Options.Add(ServiceOptionDefinitions.Debug);
command.Options.Add(ServiceOptionDefinitions.EnableInsecureTransports);
Expand All @@ -82,6 +83,7 @@ protected override ServiceStartOptions BindOptions(ParseResult parseResult)
Transport = parseResult.GetValueOrDefault<string>(ServiceOptionDefinitions.Transport.Name) ?? TransportTypes.StdIo,
Namespace = parseResult.GetValueOrDefault<string[]?>(ServiceOptionDefinitions.Namespace.Name),
Mode = parseResult.GetValueOrDefault<string?>(ServiceOptionDefinitions.Mode.Name),
Tool = parseResult.GetValueOrDefault<string[]?>(ServiceOptionDefinitions.Tool.Name),
ReadOnly = parseResult.GetValueOrDefault<bool?>(ServiceOptionDefinitions.ReadOnly.Name),
Debug = parseResult.GetValueOrDefault<bool>(ServiceOptionDefinitions.Debug.Name),
EnableInsecureTransports = parseResult.GetValueOrDefault<bool>(ServiceOptionDefinitions.EnableInsecureTransports.Name),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,19 @@ private static bool IsRawMcpToolInputOption(Option option)
/// <returns>A result containing the list of available tools.</returns>
public ValueTask<ListToolsResult> ListToolsHandler(RequestContext<ListToolsRequestParams> request, CancellationToken cancellationToken)
{
var tools = CommandFactory.GetVisibleCommands(_toolCommands)
var visibleCommands = CommandFactory.GetVisibleCommands(_toolCommands);

// Filter by specific tools if provided
if (_options.Value.Tool != null && _options.Value.Tool.Length > 0)
{
visibleCommands = visibleCommands.Where(kvp =>
{
var toolKey = kvp.Key;
return _options.Value.Tool.Any(tool => tool.Contains(toolKey, StringComparison.OrdinalIgnoreCase));
});
}

var tools = visibleCommands
.Select(kvp => GetTool(kvp.Key, kvp.Value))
.Where(tool => !_options.Value.ReadOnly || (tool.Annotations?.ReadOnlyHint == true))
.ToList();
Expand Down Expand Up @@ -98,6 +110,25 @@ public async ValueTask<CallToolResult> CallToolHandler(RequestContext<CallToolRe
}

var toolName = request.Params.Name;

// Check if tool filtering is enabled and validate the requested tool
if (_options.Value.Tool != null && _options.Value.Tool.Length > 0)
{
if (!_options.Value.Tool.Contains(toolName, StringComparer.OrdinalIgnoreCase))
{
var content = new TextContentBlock
{
Text = $"Tool '{toolName}' is not available. This server is configured to only expose the tools: {string.Join(", ", _options.Value.Tool.Select(t => $"'{t}'"))}",
};

return new CallToolResult
{
Content = [content],
IsError = true,
};
}
}

var activity = Activity.Current;

if (activity != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ public override async ValueTask<ListToolsResult> ListToolsHandler(RequestContext
.Select(t => t.ProtocolTool)
.Where(t => !_options.Value.ReadOnly || (t.Annotations?.ReadOnlyHint == true));

// Filter by specific tools if provided
if (_options.Value.Tool != null && _options.Value.Tool.Length > 0)
{
filteredTools = filteredTools.Where(t => _options.Value.Tool.Contains(t.Name, StringComparer.OrdinalIgnoreCase));
}

foreach (var tool in filteredTools)
{
allToolsResponse.Tools.Add(tool);
Expand Down Expand Up @@ -90,6 +96,26 @@ public override async ValueTask<CallToolResult> CallToolHandler(RequestContext<C
// Initialize the tool client map if not already done
await InitializeAsync(cancellationToken);

// Check if tool filtering is enabled and validate the requested tool
if (_options.Value.Tool != null && _options.Value.Tool.Length > 0)
{
if (!_options.Value.Tool.Contains(request.Params.Name, StringComparer.OrdinalIgnoreCase))
{
var content = new TextContentBlock
{
Text = $"Tool '{request.Params.Name}' is not available. This server is configured to only expose the tools: {string.Join(", ", _options.Value.Tool.Select(t => $"'{t}'"))}",
};

_logger.LogWarning(content.Text);

return new CallToolResult
{
Content = [content],
IsError = true,
};
}
}

if (!_toolClientMap.TryGetValue(request.Params.Name, out var mcpClient) || mcpClient == null)
{
var content = new TextContentBlock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ public override async ValueTask<ListToolsResult> ListToolsHandler(RequestContext
foreach (var server in serverList)
{
var metadata = server.CreateMetadata();

// Filter by specific tools if provided
if (options.Value.Tool != null && options.Value.Tool.Length > 0)
{
if (!options.Value.Tool.Any(tool => tool.Contains(metadata.Name, StringComparison.OrdinalIgnoreCase)))
{
continue;
}
}

var tool = new Tool
{
Name = metadata.Name,
Expand All @@ -96,6 +106,25 @@ public override async ValueTask<CallToolResult> CallToolHandler(RequestContext<C
}

string tool = request.Params.Name;

// Check if tool filtering is enabled and validate the requested tool
if (options.Value.Tool != null && options.Value.Tool.Length > 0)
{
if (!options.Value.Tool.Contains(tool, StringComparer.OrdinalIgnoreCase))
{
return new CallToolResult
{
Content =
[
new TextContentBlock {
Text = $"Tool '{tool}' is not available. Only the following tools are enabled: {string.Join(", ", options.Value.Tool)}"
}
],
IsError = true
};
}
}

var args = request.Params?.Arguments;
string? intent = null;
string? command = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ namespace Azure.Mcp.Core.Areas.Server.Commands.ToolLoading;
/// <param name="Namespace">The namespaces to filter commands by. If null or empty, all commands will be included.</param>
/// <param name="ReadOnly">Whether the tool loader should operate in read-only mode. When true, only tools marked as read-only will be exposed.</param>
/// <param name="InsecureDisableElicitation">Whether elicitation is disabled (insecure mode). When true, elicitation will always be treated as accepted.</param>
public sealed record ToolLoaderOptions(string[]? Namespace = null, bool ReadOnly = false, bool InsecureDisableElicitation = false);
/// <param name="Tool">The specific tool names to filter by. When specified, only these tools will be exposed.</param>
public sealed record ToolLoaderOptions(string[]? Namespace = null, bool ReadOnly = false, bool InsecureDisableElicitation = false, string[]? Tool = null);
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public static class ServiceOptionDefinitions
public const string TransportName = "transport";
public const string NamespaceName = "namespace";
public const string ModeName = "mode";
public const string ToolName = "tool";
public const string ReadOnlyName = "read-only";
public const string DebugName = "debug";
public const string EnableInsecureTransportsName = "enable-insecure-transports";
Expand Down Expand Up @@ -41,6 +42,17 @@ public static class ServiceOptionDefinitions
DefaultValueFactory = _ => (string?)ModeTypes.NamespaceProxy
};

public static readonly Option<string[]?> Tool = new Option<string[]?>(
$"--{ToolName}"
)
{
Description = "Expose only specific tools by name (e.g., 'azmcp storage account list'). Repeat this option to include multiple tools, e.g., --tool \"azmcp storage account list\" --tool \"azmcp network vnet list\".",
Required = false,
Arity = ArgumentArity.OneOrMore,
AllowMultipleArgumentsPerToken = true,
DefaultValueFactory = _ => null
};

public static readonly Option<bool?> ReadOnly = new(
$"--{ReadOnlyName}")
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ public class ServiceStartOptions
[JsonPropertyName("mode")]
public string? Mode { get; set; } = ModeTypes.NamespaceProxy;

/// <summary>
/// Gets or sets the specific tool names to expose.
/// When specified, only these tools will be available.
/// </summary>
[JsonPropertyName("tool")]
public string[]? Tool { get; set; } = null;

/// <summary>
/// Gets or sets whether the server should operate in read-only mode.
/// When true, only tools marked as read-only will be available.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,46 @@

using System.Diagnostics;
using Azure.Mcp.Core.Areas;
using Azure.Mcp.Core.Areas.Group;
using Azure.Mcp.Core.Areas.Server;
using Azure.Mcp.Core.Areas.Subscription;
using Azure.Mcp.Core.Areas.Tools;
using Azure.Mcp.Core.Commands;
using Azure.Mcp.Core.Services.Telemetry;
using Azure.Mcp.Tools.Acr;
using Azure.Mcp.Tools.Aks;
using Azure.Mcp.Tools.AppConfig;
using Azure.Mcp.Tools.AppLens;
using Azure.Mcp.Tools.Authorization;
using Azure.Mcp.Tools.AzureBestPractices;
using Azure.Mcp.Tools.AzureIsv;
using Azure.Mcp.Tools.AzureManagedLustre;
using Azure.Mcp.Tools.AzureTerraformBestPractices;
using Azure.Mcp.Tools.BicepSchema;
using Azure.Mcp.Tools.CloudArchitect;
using Azure.Mcp.Tools.Cosmos;
using Azure.Mcp.Tools.Deploy;
using Azure.Mcp.Tools.EventGrid;
using Azure.Mcp.Tools.Extension;
using Azure.Mcp.Tools.Foundry;
using Azure.Mcp.Tools.FunctionApp;
using Azure.Mcp.Tools.Grafana;
using Azure.Mcp.Tools.KeyVault;
using Azure.Mcp.Tools.Kusto;
using Azure.Mcp.Tools.LoadTesting;
using Azure.Mcp.Tools.Marketplace;
using Azure.Mcp.Tools.Monitor;
using Azure.Mcp.Tools.MySql;
using Azure.Mcp.Tools.Postgres;
using Azure.Mcp.Tools.Quota;
using Azure.Mcp.Tools.Redis;
using Azure.Mcp.Tools.ResourceHealth;
using Azure.Mcp.Tools.Search;
using Azure.Mcp.Tools.ServiceBus;
using Azure.Mcp.Tools.Sql;
using Azure.Mcp.Tools.Storage;
using Azure.Mcp.Tools.VirtualDesktop;
using Azure.Mcp.Tools.Workbooks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
Expand All @@ -21,11 +54,45 @@ internal class CommandFactoryHelpers
public static CommandFactory CreateCommandFactory(IServiceProvider? serviceProvider = default)
{
IAreaSetup[] areaSetups = [
// Core areas
new SubscriptionSetup(),
new GroupSetup(),

// Tool areas
new AcrSetup(),
new AksSetup(),
new AppConfigSetup(),
new AppLensSetup(),
new AuthorizationSetup(),
new AzureBestPracticesSetup(),
new AzureIsvSetup(),
new AzureManagedLustreSetup(),
new AzureTerraformBestPracticesSetup(),
new BicepSchemaSetup(),
new CloudArchitectSetup(),
new CosmosSetup(),
new DeploySetup(),
new EventGridSetup(),
new ExtensionSetup(),
new FoundrySetup(),
new FunctionAppSetup(),
new GrafanaSetup(),
new KeyVaultSetup(),
new KustoSetup(),
new LoadTestingSetup(),
new MarketplaceSetup(),
new MonitorSetup(),
new MySqlSetup(),
new PostgresSetup(),
new QuotaSetup(),
new RedisSetup(),
new ResourceHealthSetup(),
new SearchSetup(),
new ServiceBusSetup(),
new SqlSetup(),
new StorageSetup(),
new DeploySetup(),
new AppConfigSetup()
new VirtualDesktopSetup(),
new WorkbooksSetup(),
];

var services = serviceProvider ?? CreateDefaultServiceProvider();
Expand All @@ -44,11 +111,45 @@ public static IServiceProvider CreateDefaultServiceProvider()
public static IServiceCollection SetupCommonServices()
{
IAreaSetup[] areaSetups = [
// Core areas
new SubscriptionSetup(),
new GroupSetup(),

// Tool areas
new AcrSetup(),
new AksSetup(),
new AppConfigSetup(),
new AppLensSetup(),
new AuthorizationSetup(),
new AzureBestPracticesSetup(),
new AzureIsvSetup(),
new AzureManagedLustreSetup(),
new AzureTerraformBestPracticesSetup(),
new BicepSchemaSetup(),
new CloudArchitectSetup(),
new CosmosSetup(),
new DeploySetup(),
new EventGridSetup(),
new ExtensionSetup(),
new FoundrySetup(),
new FunctionAppSetup(),
new GrafanaSetup(),
new KeyVaultSetup(),
new KustoSetup(),
new LoadTestingSetup(),
new MarketplaceSetup(),
new MonitorSetup(),
new MySqlSetup(),
new PostgresSetup(),
new QuotaSetup(),
new RedisSetup(),
new ResourceHealthSetup(),
new SearchSetup(),
new ServiceBusSetup(),
new SqlSetup(),
new StorageSetup(),
new DeploySetup(),
new AppConfigSetup()
new VirtualDesktopSetup(),
new WorkbooksSetup(),
];

var builder = new ServiceCollection()
Expand Down
Loading
Loading