Skip to content

Commit 8a64990

Browse files
committed
Enhance MCP Inspector with proxy token support
- ✨ 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
1 parent 8a3ccd0 commit 8a64990

File tree

5 files changed

+144
-10
lines changed

5 files changed

+144
-10
lines changed

src/CommunityToolkit.Aspire.Hosting.McpInspector/CommunityToolkit.Aspire.Hosting.McpInspector.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
<ItemGroup>
88
<PackageReference Include="Aspire.Hosting" />
9+
<PackageReference Include="AspNetCore.HealthChecks.Uris" />
910
</ItemGroup>
1011

1112
</Project>

src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResource.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public class McpInspectorResource(string name) : ExecutableResource(name, "npx",
5353
/// </summary>
5454
public McpServerMetadata? DefaultMcpServer => _defaultMcpServer;
5555

56+
/// <summary>
57+
/// Gets or sets the parameter that contains the MCP proxy authentication token.
58+
/// </summary>
59+
public ParameterResource ProxyTokenParameter { get; set; } = default!;
60+
5661
internal void AddMcpServer(IResourceWithEndpoints mcpServer, bool isDefault, McpTransportType transportType)
5762
{
5863
if (_mcpServers.Any(s => s.Name == mcpServer.Name))

src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Aspire.Hosting.ApplicationModel;
2+
using Microsoft.Extensions.DependencyInjection;
23

34
namespace Aspire.Hosting;
45

@@ -15,16 +16,17 @@ public static class McpInspectorResourceBuilderExtensions
1516
/// <param name="clientPort">The port for the client application. Defaults to 6274.</param>
1617
/// <param name="serverPort">The port for the server proxy application. Defaults to 6277.</param>
1718
/// <param name="inspectorVersion">The version of the Inspector app to use</param>
18-
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion)
19+
/// <param name="proxyToken">The parameter used to provide the proxy authentication token for the MCP Inspector resource. If <see langword="null"/> a random token will be generated.</param>
20+
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion, IResourceBuilder<ParameterResource>? proxyToken = null)
1921
{
22+
var proxyTokenParameter = proxyToken?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-proxyToken");
23+
2024
var resource = builder.AddResource(new McpInspectorResource(name))
2125
.WithArgs(["-y", $"@modelcontextprotocol/inspector@{inspectorVersion}"])
2226
.ExcludeFromManifest()
2327
.WithHttpEndpoint(isProxied: false, port: clientPort, env: "CLIENT_PORT", name: McpInspectorResource.ClientEndpointName)
2428
.WithHttpEndpoint(isProxied: false, port: serverPort, env: "SERVER_PORT", name: McpInspectorResource.ServerProxyEndpointName)
25-
.WithEnvironment("DANGEROUSLY_OMIT_AUTH", "true")
2629
.WithHttpHealthCheck("/", endpointName: McpInspectorResource.ClientEndpointName)
27-
.WithHttpHealthCheck("/config", endpointName: McpInspectorResource.ServerProxyEndpointName)
2830
.WithUrlForEndpoint(McpInspectorResource.ClientEndpointName, annotation =>
2931
{
3032
annotation.DisplayText = "Client";
@@ -34,8 +36,46 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
3436
{
3537
annotation.DisplayText = "Server Proxy";
3638
annotation.DisplayOrder = 1;
39+
annotation.DisplayLocation = UrlDisplayLocation.DetailsOnly;
40+
})
41+
.WithUrls(async context =>
42+
{
43+
var token = await proxyTokenParameter.GetValueAsync(CancellationToken.None);
44+
45+
foreach (var url in context.Urls)
46+
{
47+
if (url.Endpoint is not null)
48+
{
49+
var uriBuilder = new UriBuilder(url.Url);
50+
uriBuilder.Query = $"MCP_PROXY_AUTH_TOKEN={Uri.EscapeDataString(token!)}";
51+
url.Url = uriBuilder.ToString();
52+
}
53+
}
3754
});
3855

56+
resource.Resource.ProxyTokenParameter = proxyTokenParameter;
57+
58+
// Add authenticated health check for server proxy /config endpoint
59+
var healthCheckKey = $"{name}_proxy_config_check";
60+
builder.Services.AddHealthChecks().AddUrlGroup(options =>
61+
{
62+
var serverProxyEndpoint = resource.GetEndpoint(McpInspectorResource.ServerProxyEndpointName);
63+
var uri = serverProxyEndpoint.Url;
64+
if (uri is null)
65+
{
66+
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.");
67+
}
68+
69+
var healthCheckUri = new Uri(new Uri(uri), "/config");
70+
options.AddUri(healthCheckUri, async setup =>
71+
{
72+
var token = await proxyTokenParameter.GetValueAsync(CancellationToken.None);
73+
setup.AddCustomHeader("X-MCP-Proxy-Auth", $"Bearer {token}");
74+
});
75+
}, healthCheckKey);
76+
77+
resource.WithHealthCheck(healthCheckKey);
78+
3979
builder.Eventing.Subscribe<BeforeResourceStartedEvent>(resource.Resource, async (@event, ct) =>
4080
{
4181
if (@event.Resource is not McpInspectorResource inspectorResource)
@@ -80,6 +120,7 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
80120
ctx.EnvironmentVariables["MCP_PROXY_FULL_ADDRESS"] = serverProxyEndpoint.Url;
81121
ctx.EnvironmentVariables["CLIENT_PORT"] = clientEndpoint.TargetPort?.ToString() ?? throw new InvalidOperationException("The MCP Inspector 'client' endpoint must have a target port defined.");
82122
ctx.EnvironmentVariables["SERVER_PORT"] = serverProxyEndpoint.TargetPort?.ToString() ?? throw new InvalidOperationException("The MCP Inspector 'server-proxy' endpoint must have a target port defined.");
123+
ctx.EnvironmentVariables["MCP_PROXY_AUTH_TOKEN"] = proxyTokenParameter;
83124
})
84125
.WithArgs(ctx =>
85126
{
@@ -90,7 +131,6 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
90131
throw new InvalidOperationException("No default MCP server has been configured for the MCP Inspector resource, yet servers have been provided.");
91132
}
92133

93-
94134
if (defaultMcpServer is null && inspectorResource.McpServers.Count == 0)
95135
{
96136
return;

tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/AppHostTests.cs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,41 @@
1+
using Aspire.Hosting.ApplicationModel;
12
using CommunityToolkit.Aspire.Testing;
23

34
namespace CommunityToolkit.Aspire.Hosting.McpInspector.Tests;
45

56
public class AppHostTests(AspireIntegrationTestFixture<Projects.CommunityToolkit_Aspire_Hosting_McpInspector_AppHost> fixture) : IClassFixture<AspireIntegrationTestFixture<Projects.CommunityToolkit_Aspire_Hosting_McpInspector_AppHost>>
67
{
7-
[Theory]
8-
[InlineData(McpInspectorResource.ClientEndpointName, "/")]
9-
[InlineData(McpInspectorResource.ServerProxyEndpointName, "/config")]
10-
public async Task ResourceStartsAndRespondsOk(string endpointName, string route)
8+
[Fact]
9+
public async Task ClientEndpointStartsAndRespondsOk()
1110
{
1211
var resourceName = "mcp-inspector";
1312
await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5));
14-
var httpClient = fixture.CreateHttpClient(resourceName, endpointName: endpointName);
13+
var httpClient = fixture.CreateHttpClient(resourceName, endpointName: McpInspectorResource.ClientEndpointName);
1514

16-
var response = await httpClient.GetAsync(route);
15+
var response = await httpClient.GetAsync("/");
16+
17+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
18+
}
19+
20+
[Fact]
21+
public async Task ServerProxyConfigEndpointWithAuthRespondsOk()
22+
{
23+
var resourceName = "mcp-inspector";
24+
await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5));
25+
26+
// Get the MCP Inspector resource to access the proxy token parameter
27+
var appModel = fixture.App.Services.GetRequiredService<DistributedApplicationModel>();
28+
var mcpInspectorResource = appModel.Resources.OfType<McpInspectorResource>().Single(r => r.Name == resourceName);
29+
30+
// Get the token value
31+
var token = await mcpInspectorResource.ProxyTokenParameter.GetValueAsync(CancellationToken.None);
32+
33+
var httpClient = fixture.CreateHttpClient(resourceName, endpointName: McpInspectorResource.ServerProxyEndpointName);
34+
35+
// Add the Bearer token header for authentication
36+
httpClient.DefaultRequestHeaders.Add("X-MCP-Proxy-Auth", $"Bearer {token}");
37+
38+
var response = await httpClient.GetAsync("/config");
1739

1840
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
1941
}

tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Aspire.Hosting;
2+
using Aspire.Hosting.ApplicationModel;
23

34
namespace CommunityToolkit.Aspire.Hosting.McpInspector.Tests;
45

@@ -116,4 +117,69 @@ public void WithMultipleMcpServersAddsAllServersToResource()
116117
Assert.NotNull(inspectorResource.DefaultMcpServer);
117118
Assert.Equal("mcpServer1", inspectorResource.DefaultMcpServer.Name);
118119
}
120+
121+
[Fact]
122+
public void AddMcpInspectorGeneratesProxyTokenParameter()
123+
{
124+
// Arrange
125+
var appBuilder = DistributedApplication.CreateBuilder();
126+
127+
// Act
128+
var inspector = appBuilder.AddMcpInspector("inspector");
129+
130+
using var app = appBuilder.Build();
131+
132+
// Assert
133+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
134+
135+
var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());
136+
Assert.NotNull(inspectorResource.ProxyTokenParameter);
137+
Assert.Equal("inspector-proxyToken", inspectorResource.ProxyTokenParameter.Name);
138+
}
139+
140+
[Fact]
141+
public void AddMcpInspectorWithCustomProxyTokenUsesProvidedToken()
142+
{
143+
// Arrange
144+
var appBuilder = DistributedApplication.CreateBuilder();
145+
var customToken = appBuilder.AddParameter("custom-token", secret: true);
146+
147+
// Act
148+
var inspector = appBuilder.AddMcpInspector("inspector", proxyToken: customToken);
149+
150+
using var app = appBuilder.Build();
151+
152+
// Assert
153+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
154+
155+
var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());
156+
Assert.NotNull(inspectorResource.ProxyTokenParameter);
157+
Assert.Equal("custom-token", inspectorResource.ProxyTokenParameter.Name);
158+
Assert.Same(customToken.Resource, inspectorResource.ProxyTokenParameter);
159+
}
160+
161+
[Fact]
162+
public void AddMcpInspectorSetsCorrectEnvironmentVariables()
163+
{
164+
// Arrange
165+
var appBuilder = DistributedApplication.CreateBuilder();
166+
167+
// Act
168+
var inspector = appBuilder.AddMcpInspector("inspector", clientPort: 1234, serverPort: 5678);
169+
170+
using var app = appBuilder.Build();
171+
172+
// Assert
173+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
174+
var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());
175+
176+
// Verify endpoints are configured correctly
177+
var clientEndpoint = inspectorResource.Annotations.OfType<EndpointAnnotation>()
178+
.Single(e => e.Name == McpInspectorResource.ClientEndpointName);
179+
var serverEndpoint = inspectorResource.Annotations.OfType<EndpointAnnotation>()
180+
.Single(e => e.Name == McpInspectorResource.ServerProxyEndpointName);
181+
182+
Assert.Equal(1234, clientEndpoint.Port);
183+
Assert.Equal(5678, serverEndpoint.Port);
184+
}
119185
}

0 commit comments

Comments
 (0)