From 249f9ae8488f1f54df55b6af55d007b01c05a3c4 Mon Sep 17 00:00:00 2001 From: Tim Heuer Date: Thu, 31 Jul 2025 17:11:39 -0700 Subject: [PATCH 1/5] Update MCP Inspector version and extension method parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ๐Ÿ”ง Bump InspectorVersion from 0.15.0 to 0.16.2 - ๐Ÿ“ฆ Add inspectorVersion parameter to AddMcpInspector method --- .../McpInspectorResource.cs | 2 +- .../McpInspectorResourceBuilderExtensions.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs index 901fa4b3..c014a7bf 100644 --- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs @@ -37,7 +37,7 @@ public class McpInspectorResource(string name) : ExecutableResource(name, "npx", /// /// Gets the version of the MCP Inspector. /// - public const string InspectorVersion = "0.15.0"; + public const string InspectorVersion = "0.16.2"; private readonly List _mcpServers = []; diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs index c23b89e4..2b894c11 100644 --- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs @@ -14,10 +14,11 @@ public static class McpInspectorResourceBuilderExtensions /// The name of the MCP Inspector container resource. /// The port for the client application. Defaults to 6274. /// The port for the server proxy application. Defaults to 6277. - public static IResourceBuilder AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277) + /// The version of the Inspector app to use + public static IResourceBuilder AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion) { var resource = builder.AddResource(new McpInspectorResource(name)) - .WithArgs(["-y", $"@modelcontextprotocol/inspector@{McpInspectorResource.InspectorVersion}"]) + .WithArgs(["-y", $"@modelcontextprotocol/inspector@{inspectorVersion}"]) .ExcludeFromManifest() .WithHttpEndpoint(isProxied: false, port: clientPort, env: "CLIENT_PORT", name: McpInspectorResource.ClientEndpointName) .WithHttpEndpoint(isProxied: false, port: serverPort, env: "SERVER_PORT", name: McpInspectorResource.ServerProxyEndpointName) From 8a64990560572254cf27812c09f6ede222aee417 Mon Sep 17 00:00:00 2001 From: Tim Heuer Date: Fri, 1 Aug 2025 17:46:08 -0700 Subject: [PATCH 2/5] Enhance MCP Inspector with proxy token support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - โœจ Add ProxyTokenParameter to McpInspectorResource - ๐Ÿ”ง Update AddMcpInspector method to accept proxy token - โœ… Implement authenticated health checks for server proxy endpoint - ๐Ÿงช Add tests for proxy token generation and usage --- ...Toolkit.Aspire.Hosting.McpInspector.csproj | 1 + .../McpInspectorResource.cs | 5 ++ .../McpInspectorResourceBuilderExtensions.cs | 48 ++++++++++++-- .../AppHostTests.cs | 34 ++++++++-- ...InspectorResourceBuilderExtensionsTests.cs | 66 +++++++++++++++++++ 5 files changed, 144 insertions(+), 10 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/CommunityToolkit.Aspire.Hosting.McpInspector.csproj b/src/CommunityToolkit.Aspire.Hosting.McpInspector/CommunityToolkit.Aspire.Hosting.McpInspector.csproj index de12cbf0..3aa5b4a8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/CommunityToolkit.Aspire.Hosting.McpInspector.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/CommunityToolkit.Aspire.Hosting.McpInspector.csproj @@ -6,6 +6,7 @@ + diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs index c014a7bf..16d99a33 100644 --- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs @@ -53,6 +53,11 @@ public class McpInspectorResource(string name) : ExecutableResource(name, "npx", /// public McpServerMetadata? DefaultMcpServer => _defaultMcpServer; + /// + /// Gets or sets the parameter that contains the MCP proxy authentication token. + /// + public ParameterResource ProxyTokenParameter { get; set; } = default!; + internal void AddMcpServer(IResourceWithEndpoints mcpServer, bool isDefault, McpTransportType transportType) { if (_mcpServers.Any(s => s.Name == mcpServer.Name)) diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs index 62d6cd73..2927e7c8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs @@ -1,4 +1,5 @@ using Aspire.Hosting.ApplicationModel; +using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting; @@ -15,16 +16,17 @@ public static class McpInspectorResourceBuilderExtensions /// The port for the client application. Defaults to 6274. /// The port for the server proxy application. Defaults to 6277. /// The version of the Inspector app to use - public static IResourceBuilder AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion) + /// The parameter used to provide the proxy authentication token for the MCP Inspector resource. If a random token will be generated. + public static IResourceBuilder AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion, IResourceBuilder? proxyToken = null) { + var proxyTokenParameter = proxyToken?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-proxyToken"); + var resource = builder.AddResource(new McpInspectorResource(name)) .WithArgs(["-y", $"@modelcontextprotocol/inspector@{inspectorVersion}"]) .ExcludeFromManifest() .WithHttpEndpoint(isProxied: false, port: clientPort, env: "CLIENT_PORT", name: McpInspectorResource.ClientEndpointName) .WithHttpEndpoint(isProxied: false, port: serverPort, env: "SERVER_PORT", name: McpInspectorResource.ServerProxyEndpointName) - .WithEnvironment("DANGEROUSLY_OMIT_AUTH", "true") .WithHttpHealthCheck("/", endpointName: McpInspectorResource.ClientEndpointName) - .WithHttpHealthCheck("/config", endpointName: McpInspectorResource.ServerProxyEndpointName) .WithUrlForEndpoint(McpInspectorResource.ClientEndpointName, annotation => { annotation.DisplayText = "Client"; @@ -34,8 +36,46 @@ public static IResourceBuilder AddMcpInspector(this IDistr { annotation.DisplayText = "Server Proxy"; annotation.DisplayOrder = 1; + annotation.DisplayLocation = UrlDisplayLocation.DetailsOnly; + }) + .WithUrls(async context => + { + var token = await proxyTokenParameter.GetValueAsync(CancellationToken.None); + + foreach (var url in context.Urls) + { + if (url.Endpoint is not null) + { + var uriBuilder = new UriBuilder(url.Url); + uriBuilder.Query = $"MCP_PROXY_AUTH_TOKEN={Uri.EscapeDataString(token!)}"; + url.Url = uriBuilder.ToString(); + } + } }); + resource.Resource.ProxyTokenParameter = proxyTokenParameter; + + // Add authenticated health check for server proxy /config endpoint + var healthCheckKey = $"{name}_proxy_config_check"; + builder.Services.AddHealthChecks().AddUrlGroup(options => + { + var serverProxyEndpoint = resource.GetEndpoint(McpInspectorResource.ServerProxyEndpointName); + var uri = serverProxyEndpoint.Url; + if (uri is null) + { + throw new DistributedApplicationException("The MCP Inspector 'server-proxy' endpoint URL is not set. Ensure that the resource has been allocated before the health check is executed."); + } + + var healthCheckUri = new Uri(new Uri(uri), "/config"); + options.AddUri(healthCheckUri, async setup => + { + var token = await proxyTokenParameter.GetValueAsync(CancellationToken.None); + setup.AddCustomHeader("X-MCP-Proxy-Auth", $"Bearer {token}"); + }); + }, healthCheckKey); + + resource.WithHealthCheck(healthCheckKey); + builder.Eventing.Subscribe(resource.Resource, async (@event, ct) => { if (@event.Resource is not McpInspectorResource inspectorResource) @@ -80,6 +120,7 @@ public static IResourceBuilder AddMcpInspector(this IDistr ctx.EnvironmentVariables["MCP_PROXY_FULL_ADDRESS"] = serverProxyEndpoint.Url; ctx.EnvironmentVariables["CLIENT_PORT"] = clientEndpoint.TargetPort?.ToString() ?? throw new InvalidOperationException("The MCP Inspector 'client' endpoint must have a target port defined."); ctx.EnvironmentVariables["SERVER_PORT"] = serverProxyEndpoint.TargetPort?.ToString() ?? throw new InvalidOperationException("The MCP Inspector 'server-proxy' endpoint must have a target port defined."); + ctx.EnvironmentVariables["MCP_PROXY_AUTH_TOKEN"] = proxyTokenParameter; }) .WithArgs(ctx => { @@ -90,7 +131,6 @@ public static IResourceBuilder AddMcpInspector(this IDistr throw new InvalidOperationException("No default MCP server has been configured for the MCP Inspector resource, yet servers have been provided."); } - if (defaultMcpServer is null && inspectorResource.McpServers.Count == 0) { return; diff --git a/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/AppHostTests.cs index 6ce1a0b3..054cf433 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/AppHostTests.cs @@ -1,19 +1,41 @@ +using Aspire.Hosting.ApplicationModel; using CommunityToolkit.Aspire.Testing; namespace CommunityToolkit.Aspire.Hosting.McpInspector.Tests; public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> { - [Theory] - [InlineData(McpInspectorResource.ClientEndpointName, "/")] - [InlineData(McpInspectorResource.ServerProxyEndpointName, "/config")] - public async Task ResourceStartsAndRespondsOk(string endpointName, string route) + [Fact] + public async Task ClientEndpointStartsAndRespondsOk() { var resourceName = "mcp-inspector"; await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5)); - var httpClient = fixture.CreateHttpClient(resourceName, endpointName: endpointName); + var httpClient = fixture.CreateHttpClient(resourceName, endpointName: McpInspectorResource.ClientEndpointName); - var response = await httpClient.GetAsync(route); + var response = await httpClient.GetAsync("/"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task ServerProxyConfigEndpointWithAuthRespondsOk() + { + var resourceName = "mcp-inspector"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5)); + + // Get the MCP Inspector resource to access the proxy token parameter + var appModel = fixture.App.Services.GetRequiredService(); + var mcpInspectorResource = appModel.Resources.OfType().Single(r => r.Name == resourceName); + + // Get the token value + var token = await mcpInspectorResource.ProxyTokenParameter.GetValueAsync(CancellationToken.None); + + var httpClient = fixture.CreateHttpClient(resourceName, endpointName: McpInspectorResource.ServerProxyEndpointName); + + // Add the Bearer token header for authentication + httpClient.DefaultRequestHeaders.Add("X-MCP-Proxy-Auth", $"Bearer {token}"); + + var response = await httpClient.GetAsync("/config"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } diff --git a/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs index 1400f649..13dac9fd 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs @@ -1,4 +1,5 @@ using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; namespace CommunityToolkit.Aspire.Hosting.McpInspector.Tests; @@ -116,4 +117,69 @@ public void WithMultipleMcpServersAddsAllServersToResource() Assert.NotNull(inspectorResource.DefaultMcpServer); Assert.Equal("mcpServer1", inspectorResource.DefaultMcpServer.Name); } + + [Fact] + public void AddMcpInspectorGeneratesProxyTokenParameter() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + + // Act + var inspector = appBuilder.AddMcpInspector("inspector"); + + using var app = appBuilder.Build(); + + // Assert + var appModel = app.Services.GetRequiredService(); + + var inspectorResource = Assert.Single(appModel.Resources.OfType()); + Assert.NotNull(inspectorResource.ProxyTokenParameter); + Assert.Equal("inspector-proxyToken", inspectorResource.ProxyTokenParameter.Name); + } + + [Fact] + public void AddMcpInspectorWithCustomProxyTokenUsesProvidedToken() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + var customToken = appBuilder.AddParameter("custom-token", secret: true); + + // Act + var inspector = appBuilder.AddMcpInspector("inspector", proxyToken: customToken); + + using var app = appBuilder.Build(); + + // Assert + var appModel = app.Services.GetRequiredService(); + + var inspectorResource = Assert.Single(appModel.Resources.OfType()); + Assert.NotNull(inspectorResource.ProxyTokenParameter); + Assert.Equal("custom-token", inspectorResource.ProxyTokenParameter.Name); + Assert.Same(customToken.Resource, inspectorResource.ProxyTokenParameter); + } + + [Fact] + public void AddMcpInspectorSetsCorrectEnvironmentVariables() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + + // Act + var inspector = appBuilder.AddMcpInspector("inspector", clientPort: 1234, serverPort: 5678); + + using var app = appBuilder.Build(); + + // Assert + var appModel = app.Services.GetRequiredService(); + var inspectorResource = Assert.Single(appModel.Resources.OfType()); + + // Verify endpoints are configured correctly + var clientEndpoint = inspectorResource.Annotations.OfType() + .Single(e => e.Name == McpInspectorResource.ClientEndpointName); + var serverEndpoint = inspectorResource.Annotations.OfType() + .Single(e => e.Name == McpInspectorResource.ServerProxyEndpointName); + + Assert.Equal(1234, clientEndpoint.Port); + Assert.Equal(5678, serverEndpoint.Port); + } } From 49b22719327bd7bd4bb46f8a7fa6cafcb0914929 Mon Sep 17 00:00:00 2001 From: Tim Heuer Date: Mon, 4 Aug 2025 14:53:21 -0700 Subject: [PATCH 3/5] Addressing PR feedback via some refactoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ๐ŸŽ‰ Introduced McpInspectorOptions for better configuration management - ๐Ÿ”„ Updated AddMcpInspector methods to accept options - ๐Ÿ› ๏ธ Deprecated old overloads for cleaner API - ๐Ÿ“š Enhanced README with usage examples for new options --- .../McpInspectorOptions.cs | 30 +++++ .../McpInspectorResourceBuilderExtensions.cs | 69 ++++++++-- .../README.md | 44 ++++++- ...InspectorResourceBuilderExtensionsTests.cs | 119 +++++++++++++++++- 4 files changed, 251 insertions(+), 11 deletions(-) create mode 100644 src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorOptions.cs diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorOptions.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorOptions.cs new file mode 100644 index 00000000..7932c679 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorOptions.cs @@ -0,0 +1,30 @@ +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting; + +/// +/// Options for configuring the MCP Inspector resource. +/// +public class McpInspectorOptions +{ + /// + /// Gets or sets the port for the client application. Defaults to "6274". + /// + public int ClientPort { get; set; } = 6274; + + /// + /// Gets or sets the port for the server proxy application. Defaults to "6277". + /// + public int ServerPort { get; set; } = 6277; + + /// + /// Gets or sets the version of the Inspector app to use. Defaults to . + /// + public string InspectorVersion { get; set; } = McpInspectorResource.InspectorVersion; + + /// + /// Gets or sets the parameter used to provide the proxy authentication token for the MCP Inspector resource. + /// If a random token will be generated. + /// + public IResourceBuilder? ProxyToken { get; set; } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs index 2927e7c8..c8eeaf8f 100644 --- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs @@ -16,16 +16,38 @@ public static class McpInspectorResourceBuilderExtensions /// The port for the client application. Defaults to 6274. /// The port for the server proxy application. Defaults to 6277. /// The version of the Inspector app to use - /// The parameter used to provide the proxy authentication token for the MCP Inspector resource. If a random token will be generated. - public static IResourceBuilder AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion, IResourceBuilder? proxyToken = null) + [Obsolete("Use the overload with McpInspectorOptions instead. This overload will be removed in the next version.")] + public static IResourceBuilder AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion) { - var proxyTokenParameter = proxyToken?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-proxyToken"); + ArgumentNullException.ThrowIfNull(builder); + + return AddMcpInspector(builder, name, options => + { + options.ClientPort = clientPort; + options.ServerPort = serverPort; + options.InspectorVersion = inspectorVersion; + }); + } + + /// + /// Adds a MCP Inspector container resource to the using an options object. + /// + /// The to which the MCP Inspector resource will be added. + /// The name of the MCP Inspector container resource. + /// The to configure the MCP Inspector resource. + /// A reference to the for further configuration. + public static IResourceBuilder AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, McpInspectorOptions options) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(options); + + var proxyTokenParameter = options.ProxyToken?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-proxyToken"); var resource = builder.AddResource(new McpInspectorResource(name)) - .WithArgs(["-y", $"@modelcontextprotocol/inspector@{inspectorVersion}"]) + .WithArgs(["-y", $"@modelcontextprotocol/inspector@{options.InspectorVersion}"]) .ExcludeFromManifest() - .WithHttpEndpoint(isProxied: false, port: clientPort, env: "CLIENT_PORT", name: McpInspectorResource.ClientEndpointName) - .WithHttpEndpoint(isProxied: false, port: serverPort, env: "SERVER_PORT", name: McpInspectorResource.ServerProxyEndpointName) + .WithHttpEndpoint(isProxied: false, port: options.ClientPort, env: "CLIENT_PORT", name: McpInspectorResource.ClientEndpointName) + .WithHttpEndpoint(isProxied: false, port: options.ServerPort, env: "SERVER_PORT", name: McpInspectorResource.ServerProxyEndpointName) .WithHttpHealthCheck("/", endpointName: McpInspectorResource.ClientEndpointName) .WithUrlForEndpoint(McpInspectorResource.ClientEndpointName, annotation => { @@ -41,7 +63,7 @@ public static IResourceBuilder AddMcpInspector(this IDistr .WithUrls(async context => { var token = await proxyTokenParameter.GetValueAsync(CancellationToken.None); - + foreach (var url in context.Urls) { if (url.Endpoint is not null) @@ -143,6 +165,39 @@ public static IResourceBuilder AddMcpInspector(this IDistr }); } + /// + /// Adds a MCP Inspector container resource to the using a configuration delegate. + /// + /// The to which the MCP Inspector resource will be added. + /// The name of the MCP Inspector container resource. + /// A delegate to configure the . + /// A reference to the for further configuration. + public static IResourceBuilder AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, Action configureOptions) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configureOptions); + + var options = new McpInspectorOptions(); + configureOptions(options); + + return builder.AddMcpInspector(name, options); + } + + /// + /// Adds a MCP Inspector container resource to the using a configuration delegate. + /// + /// The to which the MCP Inspector resource will be added. + /// The name of the MCP Inspector container resource. + /// A reference to the for further configuration. + public static IResourceBuilder AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name) + { + ArgumentNullException.ThrowIfNull(builder); + + var options = new McpInspectorOptions(); + + return builder.AddMcpInspector(name, options); + } + /// /// Configures the MCP Inspector resource to use a specified MCP server resource that uses SSE as the transport type. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/README.md b/src/CommunityToolkit.Aspire.Hosting.McpInspector/README.md index cdc6dc86..975aacb6 100644 --- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/README.md @@ -23,7 +23,47 @@ var inspector = builder.AddMcpInspector("inspector") .WithMcpServer(mcpServer); ``` -You can specify the transport type (`StreamableHttp` or `Sse`) and set which server is the default for the inspector. +You can specify the transport type (`StreamableHttp`) and set which server is the default for the inspector. + +#### Using options for complex configurations + +For more complex configurations with multiple parameters, you can use the options-based approach: + +```csharp +var customToken = builder.AddParameter("custom-proxy-token", secret: true); + +var options = new McpInspectorOptions +{ + ClientPort = 6275, + ServerPort = 6278, + InspectorVersion = "0.16.2", + ProxyToken = customToken +}; + +var inspector = builder.AddMcpInspector("inspector", options) + .WithMcpServer(mcpServer); +``` + +Alternatively, you can use a configuration delegate for a more fluent approach: + +```csharp +var inspector = builder.AddMcpInspector("inspector", options => +{ + options.ClientPort = 6275; + options.ServerPort = 6278; + options.InspectorVersion = "0.16.2"; +}) + .WithMcpServer(mcpServer); +``` + +#### Configuration options + +The `McpInspectorOptions` class provides the following configuration properties: + +- `ClientPort`: Port for the client application (default: 6274 +- `ServerPort`: Port for the server proxy application (default: 6277) +- `InspectorVersion`: Version of the Inspector app to use (default: latest supported version) +- `ProxyToken`: Custom authentication token parameter (default: auto-generated) ## Additional Information @@ -31,4 +71,4 @@ See the [official documentation](https://learn.microsoft.com/dotnet/aspire/commu ## Feedback & contributing -https://github.com/CommunityToolkit/Aspire +[https://github.com/CommunityToolkit/Aspire](https://github.com/CommunityToolkit/Aspire) diff --git a/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs index 13dac9fd..096ef51c 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs @@ -145,7 +145,10 @@ public void AddMcpInspectorWithCustomProxyTokenUsesProvidedToken() var customToken = appBuilder.AddParameter("custom-token", secret: true); // Act - var inspector = appBuilder.AddMcpInspector("inspector", proxyToken: customToken); + var inspector = appBuilder.AddMcpInspector("inspector", options => + { + options.ProxyToken = customToken; + }); using var app = appBuilder.Build(); @@ -165,7 +168,11 @@ public void AddMcpInspectorSetsCorrectEnvironmentVariables() var appBuilder = DistributedApplication.CreateBuilder(); // Act - var inspector = appBuilder.AddMcpInspector("inspector", clientPort: 1234, serverPort: 5678); + var inspector = appBuilder.AddMcpInspector("inspector", options => + { + options.ClientPort = 1234; + options.ServerPort = 5678; + }); using var app = appBuilder.Build(); @@ -182,4 +189,112 @@ public void AddMcpInspectorSetsCorrectEnvironmentVariables() Assert.Equal(1234, clientEndpoint.Port); Assert.Equal(5678, serverEndpoint.Port); } + + [Fact] + public void AddMcpInspectorWithOptionsCreatesResourceCorrectly() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + var customToken = appBuilder.AddParameter("custom-token", secret: true); + + var options = new McpInspectorOptions + { + ClientPort = 1111, + ServerPort = 2222, + InspectorVersion = "0.15.0", + ProxyToken = customToken + }; + + // Act + var inspector = appBuilder.AddMcpInspector("inspector", options); + + using var app = appBuilder.Build(); + + // Assert + var appModel = app.Services.GetRequiredService(); + var inspectorResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("inspector", inspectorResource.Name); + Assert.Equal("custom-token", inspectorResource.ProxyTokenParameter.Name); + Assert.Same(customToken.Resource, inspectorResource.ProxyTokenParameter); + + // Verify endpoints are configured correctly + var clientEndpoint = inspectorResource.Annotations.OfType() + .Single(e => e.Name == McpInspectorResource.ClientEndpointName); + var serverEndpoint = inspectorResource.Annotations.OfType() + .Single(e => e.Name == McpInspectorResource.ServerProxyEndpointName); + + Assert.Equal(1111, clientEndpoint.Port); + Assert.Equal(2222, serverEndpoint.Port); + + // Verify version argument is set correctly + var argsAnnotation = inspectorResource.Annotations.OfType().First(); + // We can't easily test the args directly, but we can verify the structure is correct + Assert.NotNull(argsAnnotation); + } + + [Fact] + public void AddMcpInspectorWithOptionsUsesDefaultsWhenNotSpecified() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + var options = new McpInspectorOptions(); // Use all defaults + + // Act + var inspector = appBuilder.AddMcpInspector("inspector", options); + + using var app = appBuilder.Build(); + + // Assert + var appModel = app.Services.GetRequiredService(); + var inspectorResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("inspector", inspectorResource.Name); + Assert.Equal("inspector-proxyToken", inspectorResource.ProxyTokenParameter.Name); + + // Verify endpoints use default ports + var clientEndpoint = inspectorResource.Annotations.OfType() + .Single(e => e.Name == McpInspectorResource.ClientEndpointName); + var serverEndpoint = inspectorResource.Annotations.OfType() + .Single(e => e.Name == McpInspectorResource.ServerProxyEndpointName); + + Assert.Equal(6274, clientEndpoint.Port); + Assert.Equal(6277, serverEndpoint.Port); + } + + [Fact] + public void AddMcpInspectorWithConfigurationDelegateCreatesResourceCorrectly() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + var customToken = appBuilder.AddParameter("custom-token", secret: true); + + // Act + var inspector = appBuilder.AddMcpInspector("inspector", options => + { + options.ClientPort = 3333; + options.ServerPort = 4444; + options.InspectorVersion = "0.15.0"; + options.ProxyToken = customToken; + }); + + using var app = appBuilder.Build(); + + // Assert + var appModel = app.Services.GetRequiredService(); + var inspectorResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("inspector", inspectorResource.Name); + Assert.Equal("custom-token", inspectorResource.ProxyTokenParameter.Name); + Assert.Same(customToken.Resource, inspectorResource.ProxyTokenParameter); + + // Verify endpoints are configured correctly + var clientEndpoint = inspectorResource.Annotations.OfType() + .Single(e => e.Name == McpInspectorResource.ClientEndpointName); + var serverEndpoint = inspectorResource.Annotations.OfType() + .Single(e => e.Name == McpInspectorResource.ServerProxyEndpointName); + + Assert.Equal(3333, clientEndpoint.Port); + Assert.Equal(4444, serverEndpoint.Port); + } } From b723015542b62b2c88f90f3c48d33a015b5af0f4 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 8 Aug 2025 00:23:21 +0000 Subject: [PATCH 4/5] Refactoring before merge --- .../McpInspectorResourceBuilderExtensions.cs | 137 +++++++++--------- 1 file changed, 70 insertions(+), 67 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs index c8eeaf8f..177c5ba4 100644 --- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs @@ -59,7 +59,51 @@ public static IResourceBuilder AddMcpInspector(this IDistr annotation.DisplayText = "Server Proxy"; annotation.DisplayOrder = 1; annotation.DisplayLocation = UrlDisplayLocation.DetailsOnly; + }).OnBeforeResourceStarted(async (inspectorResource, @event, ct) => + { + if (inspectorResource.DefaultMcpServer is null && inspectorResource.McpServers.Count > 0) + { + throw new InvalidOperationException("No default MCP server has been configured for the MCP Inspector resource, yet servers have been provided."); + } + + var servers = inspectorResource.McpServers.ToDictionary(s => s.Name, s => new + { + transport = s.TransportType switch + { + McpTransportType.StreamableHttp => "streamable-http", +#pragma warning disable CS0618 + McpTransportType.Sse => "sse", +#pragma warning restore CS0618 + _ => throw new NotSupportedException($"The transport type {s.TransportType} is not supported.") + }, + endpoint = s.Endpoint.Url + }); + + var config = new { mcpServers = servers }; + + await File.WriteAllTextAsync(inspectorResource.ConfigPath, System.Text.Json.JsonSerializer.Serialize(config), ct); }) + .WithEnvironment(ctx => + { + if (ctx.Resource is not McpInspectorResource resource) + { + return; + } + + var clientEndpoint = resource.GetEndpoint(McpInspectorResource.ClientEndpointName); + var serverProxyEndpoint = resource.GetEndpoint(McpInspectorResource.ServerProxyEndpointName); + + if (clientEndpoint is null || serverProxyEndpoint is null) + { + throw new InvalidOperationException("The MCP Inspector resource must have both 'client' and 'server-proxy' endpoints defined."); + } + + ctx.EnvironmentVariables["MCP_PROXY_FULL_ADDRESS"] = serverProxyEndpoint.Url; + ctx.EnvironmentVariables["CLIENT_PORT"] = clientEndpoint.TargetPort?.ToString() ?? throw new InvalidOperationException("The MCP Inspector 'client' endpoint must have a target port defined."); + ctx.EnvironmentVariables["SERVER_PORT"] = serverProxyEndpoint.TargetPort?.ToString() ?? throw new InvalidOperationException("The MCP Inspector 'server-proxy' endpoint must have a target port defined."); + ctx.EnvironmentVariables["MCP_PROXY_AUTH_TOKEN"] = proxyTokenParameter; + }) + .WithDefaultArgs() .WithUrls(async context => { var token = await proxyTokenParameter.GetValueAsync(CancellationToken.None); @@ -96,73 +140,7 @@ public static IResourceBuilder AddMcpInspector(this IDistr }); }, healthCheckKey); - resource.WithHealthCheck(healthCheckKey); - - builder.Eventing.Subscribe(resource.Resource, async (@event, ct) => - { - if (@event.Resource is not McpInspectorResource inspectorResource) - { - return; - } - - if (inspectorResource.DefaultMcpServer is null && inspectorResource.McpServers.Count > 0) - { - throw new InvalidOperationException("No default MCP server has been configured for the MCP Inspector resource, yet servers have been provided."); - } - - var servers = inspectorResource.McpServers.ToDictionary(s => s.Name, s => new - { - transport = s.TransportType switch - { - McpTransportType.StreamableHttp => "streamable-http", -#pragma warning disable CS0618 - McpTransportType.Sse => "sse", -#pragma warning restore CS0618 - _ => throw new NotSupportedException($"The transport type {s.TransportType} is not supported.") - }, - endpoint = s.Endpoint.Url - }); - - var config = new { mcpServers = servers }; - - await File.WriteAllTextAsync(inspectorResource.ConfigPath, System.Text.Json.JsonSerializer.Serialize(config), ct); - }); - - return resource - .WithEnvironment(ctx => - { - var clientEndpoint = resource.GetEndpoint(McpInspectorResource.ClientEndpointName); - var serverProxyEndpoint = resource.GetEndpoint(McpInspectorResource.ServerProxyEndpointName); - - if (clientEndpoint is null || serverProxyEndpoint is null) - { - throw new InvalidOperationException("The MCP Inspector resource must have both 'client' and 'server-proxy' endpoints defined."); - } - - ctx.EnvironmentVariables["MCP_PROXY_FULL_ADDRESS"] = serverProxyEndpoint.Url; - ctx.EnvironmentVariables["CLIENT_PORT"] = clientEndpoint.TargetPort?.ToString() ?? throw new InvalidOperationException("The MCP Inspector 'client' endpoint must have a target port defined."); - ctx.EnvironmentVariables["SERVER_PORT"] = serverProxyEndpoint.TargetPort?.ToString() ?? throw new InvalidOperationException("The MCP Inspector 'server-proxy' endpoint must have a target port defined."); - ctx.EnvironmentVariables["MCP_PROXY_AUTH_TOKEN"] = proxyTokenParameter; - }) - .WithArgs(ctx => - { - McpInspectorResource inspectorResource = resource.Resource; - McpServerMetadata? defaultMcpServer = inspectorResource.DefaultMcpServer; - if ((defaultMcpServer is null && inspectorResource.McpServers.Count > 0) || (defaultMcpServer is not null && inspectorResource.McpServers.Count == 0)) - { - throw new InvalidOperationException("No default MCP server has been configured for the MCP Inspector resource, yet servers have been provided."); - } - - if (defaultMcpServer is null && inspectorResource.McpServers.Count == 0) - { - return; - } - - ctx.Args.Add("--config"); - ctx.Args.Add(inspectorResource.ConfigPath); - ctx.Args.Add("--server"); - ctx.Args.Add(defaultMcpServer?.Name ?? throw new InvalidOperationException("The MCP Inspector resource must have a default MCP server defined.")); - }); + return resource.WithHealthCheck(healthCheckKey); } /// @@ -220,4 +198,29 @@ public static IResourceBuilder WithMcpServer( builder.Resource.AddMcpServer(mcpServer.Resource, isDefault, transportType); return builder; } + + private static IResourceBuilder WithDefaultArgs(this IResourceBuilder builder) + { + return builder + .WithArgs(ctx => + { + McpInspectorResource inspectorResource = builder.Resource; + McpServerMetadata? defaultMcpServer = inspectorResource.DefaultMcpServer; + if ((defaultMcpServer is null && inspectorResource.McpServers.Count > 0) || (defaultMcpServer is not null && inspectorResource.McpServers.Count == 0)) + { + throw new InvalidOperationException("No default MCP server has been configured for the MCP Inspector resource, yet servers have been provided."); + } + + + if (defaultMcpServer is null && inspectorResource.McpServers.Count == 0) + { + return; + } + + ctx.Args.Add("--config"); + ctx.Args.Add(inspectorResource.ConfigPath); + ctx.Args.Add("--server"); + ctx.Args.Add(defaultMcpServer?.Name ?? throw new InvalidOperationException("The MCP Inspector resource must have a default MCP server defined.")); + }); + } } From 982b02ff7c72698d8e64425e83b661b324794136 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 8 Aug 2025 01:09:08 +0000 Subject: [PATCH 5/5] Fixing from bad merge --- .../McpInspectorResourceBuilderExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs index 799c3f3e..1ab5405c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs @@ -49,7 +49,6 @@ public static IResourceBuilder AddMcpInspector(this IDistr .WithHttpEndpoint(isProxied: false, port: options.ClientPort, env: "CLIENT_PORT", name: McpInspectorResource.ClientEndpointName) .WithHttpEndpoint(isProxied: false, port: options.ServerPort, env: "SERVER_PORT", name: McpInspectorResource.ServerProxyEndpointName) .WithHttpHealthCheck("/", endpointName: McpInspectorResource.ClientEndpointName) - .WithHttpHealthCheck("/config", endpointName: McpInspectorResource.ServerProxyEndpointName) .WithEnvironment("MCP_AUTO_OPEN_ENABLED", "false") .WithUrlForEndpoint(McpInspectorResource.ClientEndpointName, annotation => {