Skip to content

Commit c52c263

Browse files
Support auth token on MCP Inspector (#780)
* Update MCP Inspector version and extension method parameters - 🔧 Bump InspectorVersion from 0.15.0 to 0.16.2 - 📦 Add inspectorVersion parameter to AddMcpInspector method * 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 * Addressing PR feedback via some refactoring - 🎉 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 * Refactoring before merge * Fixing from bad merge --------- Co-authored-by: Aaron Powell <me@aaron-powell.com>
1 parent 17828ee commit c52c263

7 files changed

Lines changed: 387 additions & 15 deletions

File tree

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>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Aspire.Hosting.ApplicationModel;
2+
3+
namespace Aspire.Hosting;
4+
5+
/// <summary>
6+
/// Options for configuring the MCP Inspector resource.
7+
/// </summary>
8+
public class McpInspectorOptions
9+
{
10+
/// <summary>
11+
/// Gets or sets the port for the client application. Defaults to "6274".
12+
/// </summary>
13+
public int ClientPort { get; set; } = 6274;
14+
15+
/// <summary>
16+
/// Gets or sets the port for the server proxy application. Defaults to "6277".
17+
/// </summary>
18+
public int ServerPort { get; set; } = 6277;
19+
20+
/// <summary>
21+
/// Gets or sets the version of the Inspector app to use. Defaults to <see cref="McpInspectorResource.InspectorVersion"/>.
22+
/// </summary>
23+
public string InspectorVersion { get; set; } = McpInspectorResource.InspectorVersion;
24+
25+
/// <summary>
26+
/// Gets or sets the parameter used to provide the proxy authentication token for the MCP Inspector resource.
27+
/// If <see langword="null"/> a random token will be generated.
28+
/// </summary>
29+
public IResourceBuilder<ParameterResource>? ProxyToken { get; set; }
30+
}

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: 100 additions & 7 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,39 @@ 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>
19+
[Obsolete("Use the overload with McpInspectorOptions instead. This overload will be removed in the next version.")]
1820
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion)
1921
{
20-
return builder.AddResource(new McpInspectorResource(name))
21-
.WithArgs(["-y", $"@modelcontextprotocol/inspector@{inspectorVersion}"])
22+
ArgumentNullException.ThrowIfNull(builder);
23+
24+
return AddMcpInspector(builder, name, options =>
25+
{
26+
options.ClientPort = clientPort;
27+
options.ServerPort = serverPort;
28+
options.InspectorVersion = inspectorVersion;
29+
});
30+
}
31+
32+
/// <summary>
33+
/// Adds a MCP Inspector container resource to the <see cref="IDistributedApplicationBuilder"/> using an options object.
34+
/// </summary>
35+
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to which the MCP Inspector resource will be added.</param>
36+
/// <param name="name">The name of the MCP Inspector container resource.</param>
37+
/// <param name="options">The <see cref="McpInspectorOptions"/> to configure the MCP Inspector resource.</param>
38+
/// <returns>A reference to the <see cref="IResourceBuilder{McpInspectorResource}"/> for further configuration.</returns>
39+
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, McpInspectorOptions options)
40+
{
41+
ArgumentNullException.ThrowIfNull(builder);
42+
ArgumentNullException.ThrowIfNull(options);
43+
44+
var proxyTokenParameter = options.ProxyToken?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-proxyToken");
45+
46+
var resource = builder.AddResource(new McpInspectorResource(name))
47+
.WithArgs(["-y", $"@modelcontextprotocol/inspector@{options.InspectorVersion}"])
2248
.ExcludeFromManifest()
23-
.WithHttpEndpoint(isProxied: false, port: clientPort, env: "CLIENT_PORT", name: McpInspectorResource.ClientEndpointName)
24-
.WithHttpEndpoint(isProxied: false, port: serverPort, env: "SERVER_PORT", name: McpInspectorResource.ServerProxyEndpointName)
25-
.WithEnvironment("DANGEROUSLY_OMIT_AUTH", "true")
49+
.WithHttpEndpoint(isProxied: false, port: options.ClientPort, env: "CLIENT_PORT", name: McpInspectorResource.ClientEndpointName)
50+
.WithHttpEndpoint(isProxied: false, port: options.ServerPort, env: "SERVER_PORT", name: McpInspectorResource.ServerProxyEndpointName)
2651
.WithHttpHealthCheck("/", endpointName: McpInspectorResource.ClientEndpointName)
27-
.WithHttpHealthCheck("/config", endpointName: McpInspectorResource.ServerProxyEndpointName)
2852
.WithEnvironment("MCP_AUTO_OPEN_ENABLED", "false")
2953
.WithUrlForEndpoint(McpInspectorResource.ClientEndpointName, annotation =>
3054
{
@@ -35,6 +59,7 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
3559
{
3660
annotation.DisplayText = "Server Proxy";
3761
annotation.DisplayOrder = 1;
62+
annotation.DisplayLocation = UrlDisplayLocation.DetailsOnly;
3863
})
3964
.OnBeforeResourceStarted(async (inspectorResource, @event, ct) =>
4065
{
@@ -78,8 +103,76 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
78103
ctx.EnvironmentVariables["MCP_PROXY_FULL_ADDRESS"] = serverProxyEndpoint.Url;
79104
ctx.EnvironmentVariables["CLIENT_PORT"] = clientEndpoint.TargetPort?.ToString() ?? throw new InvalidOperationException("The MCP Inspector 'client' endpoint must have a target port defined.");
80105
ctx.EnvironmentVariables["SERVER_PORT"] = serverProxyEndpoint.TargetPort?.ToString() ?? throw new InvalidOperationException("The MCP Inspector 'server-proxy' endpoint must have a target port defined.");
106+
ctx.EnvironmentVariables["MCP_PROXY_AUTH_TOKEN"] = proxyTokenParameter;
81107
})
82-
.WithDefaultArgs();
108+
.WithDefaultArgs()
109+
.WithUrls(async context =>
110+
{
111+
var token = await proxyTokenParameter.GetValueAsync(CancellationToken.None);
112+
113+
foreach (var url in context.Urls)
114+
{
115+
if (url.Endpoint is not null)
116+
{
117+
var uriBuilder = new UriBuilder(url.Url)
118+
{
119+
Query = $"MCP_PROXY_AUTH_TOKEN={Uri.EscapeDataString(token!)}"
120+
};
121+
url.Url = uriBuilder.ToString();
122+
}
123+
}
124+
});
125+
126+
resource.Resource.ProxyTokenParameter = proxyTokenParameter;
127+
128+
// Add authenticated health check for server proxy /config endpoint
129+
var healthCheckKey = $"{name}_proxy_config_check";
130+
builder.Services.AddHealthChecks().AddUrlGroup(options =>
131+
{
132+
var serverProxyEndpoint = resource.GetEndpoint(McpInspectorResource.ServerProxyEndpointName);
133+
var uri = serverProxyEndpoint.Url ?? 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.");
134+
var healthCheckUri = new Uri(new Uri(uri), "/config");
135+
options.AddUri(healthCheckUri, async setup =>
136+
{
137+
var token = await proxyTokenParameter.GetValueAsync(CancellationToken.None);
138+
setup.AddCustomHeader("X-MCP-Proxy-Auth", $"Bearer {token}");
139+
});
140+
}, healthCheckKey);
141+
142+
return resource.WithHealthCheck(healthCheckKey);
143+
}
144+
145+
/// <summary>
146+
/// Adds a MCP Inspector container resource to the <see cref="IDistributedApplicationBuilder"/> using a configuration delegate.
147+
/// </summary>
148+
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to which the MCP Inspector resource will be added.</param>
149+
/// <param name="name">The name of the MCP Inspector container resource.</param>
150+
/// <param name="configureOptions">A delegate to configure the <see cref="McpInspectorOptions"/>.</param>
151+
/// <returns>A reference to the <see cref="IResourceBuilder{McpInspectorResource}"/> for further configuration.</returns>
152+
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, Action<McpInspectorOptions> configureOptions)
153+
{
154+
ArgumentNullException.ThrowIfNull(builder);
155+
ArgumentNullException.ThrowIfNull(configureOptions);
156+
157+
var options = new McpInspectorOptions();
158+
configureOptions(options);
159+
160+
return builder.AddMcpInspector(name, options);
161+
}
162+
163+
/// <summary>
164+
/// Adds a MCP Inspector container resource to the <see cref="IDistributedApplicationBuilder"/> using a configuration delegate.
165+
/// </summary>
166+
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to which the MCP Inspector resource will be added.</param>
167+
/// <param name="name">The name of the MCP Inspector container resource.</param>
168+
/// <returns>A reference to the <see cref="IResourceBuilder{McpInspectorResource}"/> for further configuration.</returns>
169+
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name)
170+
{
171+
ArgumentNullException.ThrowIfNull(builder);
172+
173+
var options = new McpInspectorOptions();
174+
175+
return builder.AddMcpInspector(name, options);
83176
}
84177

85178
/// <summary>

src/CommunityToolkit.Aspire.Hosting.McpInspector/README.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,52 @@ var inspector = builder.AddMcpInspector("inspector")
2323
.WithMcpServer(mcpServer);
2424
```
2525

26-
You can specify the transport type (`StreamableHttp` or `Sse`) and set which server is the default for the inspector.
26+
You can specify the transport type (`StreamableHttp`) and set which server is the default for the inspector.
27+
28+
#### Using options for complex configurations
29+
30+
For more complex configurations with multiple parameters, you can use the options-based approach:
31+
32+
```csharp
33+
var customToken = builder.AddParameter("custom-proxy-token", secret: true);
34+
35+
var options = new McpInspectorOptions
36+
{
37+
ClientPort = 6275,
38+
ServerPort = 6278,
39+
InspectorVersion = "0.16.2",
40+
ProxyToken = customToken
41+
};
42+
43+
var inspector = builder.AddMcpInspector("inspector", options)
44+
.WithMcpServer(mcpServer);
45+
```
46+
47+
Alternatively, you can use a configuration delegate for a more fluent approach:
48+
49+
```csharp
50+
var inspector = builder.AddMcpInspector("inspector", options =>
51+
{
52+
options.ClientPort = 6275;
53+
options.ServerPort = 6278;
54+
options.InspectorVersion = "0.16.2";
55+
})
56+
.WithMcpServer(mcpServer);
57+
```
58+
59+
#### Configuration options
60+
61+
The `McpInspectorOptions` class provides the following configuration properties:
62+
63+
- `ClientPort`: Port for the client application (default: 6274
64+
- `ServerPort`: Port for the server proxy application (default: 6277)
65+
- `InspectorVersion`: Version of the Inspector app to use (default: latest supported version)
66+
- `ProxyToken`: Custom authentication token parameter (default: auto-generated)
2767

2868
## Additional Information
2969

3070
See the [official documentation](https://learn.microsoft.com/dotnet/aspire/community-toolkit/mcpinspector) for more details.
3171

3272
## Feedback & contributing
3373

34-
https://github.com/CommunityToolkit/Aspire
74+
[https://github.com/CommunityToolkit/Aspire](https://github.com/CommunityToolkit/Aspire)

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
}

0 commit comments

Comments
 (0)