Skip to content
This repository was archived by the owner on Sep 4, 2025. It is now read-only.

Commit f33eced

Browse files
authored
Support for launching smaller service level MCP servers (#324)
Enables launching per service MCP servers using new optional --service flag. If flag is omitted, it continues to start a server with all tools enabled.
1 parent d63600a commit f33eced

File tree

9 files changed

+151
-12
lines changed

9 files changed

+151
-12
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Release History
22

3+
## 0.2.0 (Unreleased)
4+
5+
### Features Added
6+
7+
- Support for launching smaller service level MCP servers. https://github.com/Azure/azure-mcp/pull/324
8+
39
## 0.1.3 (2025-06-05)
410

511
### Bugs Fixed

src/AzureMcp.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<Version>0.1.3</Version>
4+
<Version>0.2.0</Version>
55
<OutputType>Exe</OutputType>
66
<CliName>azmcp</CliName>
77
<AssemblyName>$(CliName)</AssemblyName>

src/Commands/CommandFactory.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ public CommandFactory(IServiceProvider serviceProvider, ILogger<CommandFactory>
6464

6565
public IReadOnlyDictionary<string, IBaseCommand> AllCommands => _commandMap;
6666

67+
public IReadOnlyDictionary<string, IBaseCommand> GroupCommands(string groupName)
68+
{
69+
foreach (CommandGroup group in _rootGroup.SubGroup)
70+
{
71+
if (string.Equals(group.Name, groupName, StringComparison.OrdinalIgnoreCase))
72+
{
73+
return CreateCommmandDictionary(group, string.Empty);
74+
}
75+
}
76+
77+
throw new KeyNotFoundException($"Group '{groupName}' not found in command groups.");
78+
}
6779

6880
private void RegisterCommandGroup()
6981
{

src/Commands/Server/ServiceStartCommand.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public sealed class ServiceStartCommand : BaseCommand
2323
private const string _commandTitle = "Start MCP Server";
2424
private readonly Option<string> _transportOption = OptionDefinitions.Service.Transport;
2525
private readonly Option<int> _portOption = OptionDefinitions.Service.Port;
26+
private readonly Option<string?> _serviceTypeOption = OptionDefinitions.Service.ServiceType;
2627

2728
public override string Name => "start";
2829
public override string Description => "Starts Azure MCP Server.";
@@ -33,6 +34,7 @@ protected override void RegisterOptions(Command command)
3334
base.RegisterOptions(command);
3435
command.AddOption(_transportOption);
3536
command.AddOption(_portOption);
37+
command.AddOption(_serviceTypeOption);
3638
}
3739

3840
public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult)
@@ -41,10 +43,15 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
4143
? OptionDefinitions.Service.Port.GetDefaultValue()
4244
: parseResult.GetValueForOption(_portOption);
4345

46+
var service = parseResult.GetValueForOption(_serviceTypeOption) == default
47+
? OptionDefinitions.Service.ServiceType.GetDefaultValue()
48+
: parseResult.GetValueForOption(_serviceTypeOption);
49+
4450
var serverOptions = new ServiceStartOptions
4551
{
4652
Transport = parseResult.GetValueForOption(_transportOption) ?? TransportTypes.StdIo,
47-
Port = port
53+
Port = port,
54+
Service = service,
4855
};
4956

5057
using var host = CreateHost(serverOptions);
@@ -60,7 +67,7 @@ private IHost CreateHost(ServiceStartOptions serverOptions)
6067
{
6168
var builder = WebApplication.CreateBuilder([]);
6269
Program.ConfigureServices(builder.Services);
63-
ConfigureMcpServer(builder.Services, serverOptions.Transport);
70+
ConfigureMcpServer(builder.Services, serverOptions);
6471

6572
builder.WebHost
6673
.ConfigureKestrel(server => server.ListenAnyIP(serverOptions.Port))
@@ -86,13 +93,13 @@ private IHost CreateHost(ServiceStartOptions serverOptions)
8693
.ConfigureServices(services =>
8794
{
8895
Program.ConfigureServices(services);
89-
ConfigureMcpServer(services, serverOptions.Transport);
96+
ConfigureMcpServer(services, serverOptions);
9097
})
9198
.Build();
9299
}
93100
}
94101

95-
private static void ConfigureMcpServer(IServiceCollection services, string transport)
102+
private static void ConfigureMcpServer(IServiceCollection services, ServiceStartOptions options)
96103
{
97104
services.AddSingleton<ToolOperations>();
98105
services.AddSingleton<AzureEventSourceLogForwarder>();
@@ -111,18 +118,18 @@ private static void ConfigureMcpServer(IServiceCollection services, string trans
111118
Version = assemblyName?.Version?.ToString() ?? "1.0.0-beta"
112119
};
113120

121+
toolOperations.CommandGroup = options.Service;
114122
mcpServerOptions.Capabilities = new ServerCapabilities
115123
{
116124
Tools = toolOperations.ToolsCapability
117125
};
118126

119127
mcpServerOptions.ProtocolVersion = "2024-11-05";
120-
121128
});
122129

123130
var mcpServerBuilder = services.AddMcpServer();
124131

125-
if (transport != TransportTypes.Sse)
132+
if (options.Transport != TransportTypes.Sse)
126133
{
127134
mcpServerBuilder.WithStdioServerTransport();
128135
}

src/Commands/Server/ToolOperations.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class ToolOperations
1313
{
1414
private readonly IServiceProvider _serviceProvider;
1515
private readonly CommandFactory _commandFactory;
16+
private IReadOnlyDictionary<string, IBaseCommand> _toolCommands;
1617
private readonly ILogger<ToolOperations> _logger;
1718

1819
public ToolOperations(IServiceProvider serviceProvider, CommandFactory commandFactory, ILogger<ToolOperations> logger)
@@ -30,16 +31,21 @@ public ToolOperations(IServiceProvider serviceProvider, CommandFactory commandFa
3031

3132
public ToolsCapability ToolsCapability { get; }
3233

34+
public string? CommandGroup { get; set; }
35+
3336
private ValueTask<ListToolsResult> OnListTools(RequestContext<ListToolsRequestParams> requestContext,
3437
CancellationToken cancellationToken)
3538
{
36-
var allCommands = _commandFactory.AllCommands;
37-
if (allCommands.Count == 0)
39+
if (string.IsNullOrWhiteSpace(CommandGroup))
40+
{
41+
_toolCommands = _commandFactory.AllCommands;
42+
}
43+
else
3844
{
39-
return ValueTask.FromResult(new ListToolsResult { Tools = [] });
45+
_toolCommands = _commandFactory.GroupCommands(CommandGroup);
4046
}
4147

42-
var tools = CommandFactory.GetVisibleCommands(allCommands)
48+
var tools = CommandFactory.GetVisibleCommands(_toolCommands)
4349
.Select(kvp => GetTool(kvp.Key, kvp.Value))
4450
.ToList();
4551

@@ -69,7 +75,7 @@ private async ValueTask<CallToolResponse> OnCallTools(RequestContext<CallToolReq
6975
};
7076
}
7177

72-
var command = _commandFactory.FindCommandByName(parameters.Params.Name);
78+
var command = _toolCommands.GetValueOrDefault(parameters.Params.Name);
7379
if (command == null)
7480
{
7581
var content = new Content

src/Models/Option/OptionDefinitions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ public static class Service
360360
{
361361
public const string TransportName = "transport";
362362
public const string PortName = "port";
363+
public const string ServiceName = "service";
363364

364365
public static readonly Option<string> Transport = new(
365366
$"--{TransportName}",
@@ -378,6 +379,15 @@ public static class Service
378379
{
379380
IsRequired = false
380381
};
382+
383+
public static readonly Option<string?> ServiceType = new(
384+
$"--{ServiceName}",
385+
() => null,
386+
"The service to expose on the MCP server."
387+
)
388+
{
389+
IsRequired = false,
390+
};
381391
}
382392

383393
public static class AppConfig

src/Options/Server/ServiceStartOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@ public class ServiceStartOptions
1212

1313
[JsonPropertyName("port")]
1414
public int Port { get; set; }
15+
16+
[JsonPropertyName("service")]
17+
public string? Service { get; set; } = null;
1518
}

tests/Commands/Server/ServiceStartCommandTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.CommandLine;
5+
using System.CommandLine.Parsing;
6+
using AzureMcp.Models.Option;
47
using Xunit;
58

69
namespace AzureMcp.Commands.Server.Tests;
@@ -23,4 +26,45 @@ public void Constructor_InitializesCommandCorrectly()
2326
Assert.Equal("start", _command.GetCommand().Name);
2427
Assert.Equal("Starts Azure MCP Server.", _command.GetCommand().Description!);
2528
}
29+
30+
[Theory]
31+
[InlineData(null, "", 1234, "stdio")]
32+
[InlineData("storage", "storage", 1234, "stdio")]
33+
public void ServiceOption_ParsesCorrectly(string? inputService, string expectedService, int expectedPort, string expectedTransport)
34+
{
35+
// Arrange
36+
var parseResult = CreateParseResult(inputService);
37+
38+
// Act
39+
var actualService = parseResult.GetValueForOption(OptionDefinitions.Service.ServiceType);
40+
var actualPort = parseResult.GetValueForOption(OptionDefinitions.Service.Port);
41+
var actualTransport = parseResult.GetValueForOption(OptionDefinitions.Service.Transport);
42+
43+
// Assert
44+
Assert.Equal(expectedService, actualService ?? "");
45+
Assert.Equal(expectedPort, actualPort);
46+
Assert.Equal(expectedTransport, actualTransport);
47+
}
48+
49+
private static ParseResult CreateParseResult(string? serviceValue)
50+
{
51+
var root = new RootCommand
52+
{
53+
OptionDefinitions.Service.ServiceType,
54+
OptionDefinitions.Service.Port,
55+
OptionDefinitions.Service.Transport
56+
};
57+
var args = new List<string>();
58+
if (!string.IsNullOrEmpty(serviceValue))
59+
{
60+
args.Add("--service");
61+
args.Add(serviceValue);
62+
}
63+
// Add required port/transport defaults for test
64+
args.Add("--port");
65+
args.Add("1234");
66+
args.Add("--transport");
67+
args.Add("stdio");
68+
return new Parser(root).Parse(args.ToArray());
69+
}
2670
}

tests/Commands/Server/ToolOperationsTest.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,55 @@ public async Task GetsAllTools()
8888
}
8989
}
9090
}
91+
92+
[Theory]
93+
[InlineData(null)] // All tools
94+
[InlineData("storage")]
95+
[InlineData("keyvault")]
96+
[InlineData("group")]
97+
public async Task GetsToolsByCommandGroup(string? commandGroup)
98+
{
99+
var operations = new ToolOperations(_serviceProvider, _commandFactory, _logger)
100+
{
101+
CommandGroup = commandGroup
102+
};
103+
var requestContext = new RequestContext<ListToolsRequestParams>(_server);
104+
var handler = operations.ToolsCapability.ListToolsHandler;
105+
Assert.NotNull(handler);
106+
var result = await handler(requestContext, CancellationToken.None);
107+
Assert.NotNull(result);
108+
Assert.NotEmpty(result.Tools);
109+
110+
// If a group is specified, all tool names should start with that group
111+
if (!string.IsNullOrWhiteSpace(commandGroup))
112+
{
113+
foreach (var tool in result.Tools)
114+
{
115+
Assert.StartsWith($"{commandGroup}-", tool.Name);
116+
}
117+
}
118+
else
119+
{
120+
// If no group, ensure we have a mix of tools from different groups
121+
var toolGroups = result.Tools.Select(t => t.Name.Split('-')[1]).Distinct().ToList();
122+
Assert.True(toolGroups.Count > 1, "Should return tools from multiple groups when no CommandGroup is set.");
123+
}
124+
}
125+
126+
[Fact]
127+
public async Task GetsNoToolsForUnknownCommandGroup()
128+
{
129+
var operations = new ToolOperations(_serviceProvider, _commandFactory, _logger)
130+
{
131+
CommandGroup = "unknown-group"
132+
};
133+
var requestContext = new RequestContext<ListToolsRequestParams>(_server);
134+
var handler = operations.ToolsCapability.ListToolsHandler;
135+
Assert.NotNull(handler);
136+
var ex = await Assert.ThrowsAsync<KeyNotFoundException>(async () =>
137+
{
138+
await handler(requestContext, CancellationToken.None);
139+
});
140+
Assert.Contains("unknown-group", ex.Message);
141+
}
91142
}

0 commit comments

Comments
 (0)