Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<ItemGroup>
<PackageReference Include="Aspire.Hosting" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting;

/// <summary>
/// Options for configuring the MCP Inspector resource.
/// </summary>
public class McpInspectorOptions
{
/// <summary>
/// Gets or sets the port for the client application. Defaults to "6274".
/// </summary>
public int ClientPort { get; set; } = 6274;

/// <summary>
/// Gets or sets the port for the server proxy application. Defaults to "6277".
/// </summary>
public int ServerPort { get; set; } = 6277;

/// <summary>
/// Gets or sets the version of the Inspector app to use. Defaults to <see cref="McpInspectorResource.InspectorVersion"/>.
/// </summary>
public string InspectorVersion { get; set; } = McpInspectorResource.InspectorVersion;

/// <summary>
/// Gets or sets the parameter used to provide the proxy authentication token for the MCP Inspector resource.
/// If <see langword="null"/> a random token will be generated.
/// </summary>
public IResourceBuilder<ParameterResource>? ProxyToken { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ public class McpInspectorResource(string name) : ExecutableResource(name, "npx",
/// </summary>
public McpServerMetadata? DefaultMcpServer => _defaultMcpServer;

/// <summary>
/// Gets or sets the parameter that contains the MCP proxy authentication token.
/// </summary>
public ParameterResource ProxyTokenParameter { get; set; } = default!;

internal void AddMcpServer(IResourceWithEndpoints mcpServer, bool isDefault, McpTransportType transportType)
{
if (_mcpServers.Any(s => s.Name == mcpServer.Name))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;

namespace Aspire.Hosting;

Expand All @@ -15,16 +16,39 @@ public static class McpInspectorResourceBuilderExtensions
/// <param name="clientPort">The port for the client application. Defaults to 6274.</param>
/// <param name="serverPort">The port for the server proxy application. Defaults to 6277.</param>
/// <param name="inspectorVersion">The version of the Inspector app to use</param>
[Obsolete("Use the overload with McpInspectorOptions instead. This overload will be removed in the next version.")]
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, int clientPort = 6274, int serverPort = 6277, string inspectorVersion = McpInspectorResource.InspectorVersion)
{
return builder.AddResource(new McpInspectorResource(name))
.WithArgs(["-y", $"@modelcontextprotocol/inspector@{inspectorVersion}"])
ArgumentNullException.ThrowIfNull(builder);

return AddMcpInspector(builder, name, options =>
{
options.ClientPort = clientPort;
options.ServerPort = serverPort;
options.InspectorVersion = inspectorVersion;
});
}

/// <summary>
/// Adds a MCP Inspector container resource to the <see cref="IDistributedApplicationBuilder"/> using an options object.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to which the MCP Inspector resource will be added.</param>
/// <param name="name">The name of the MCP Inspector container resource.</param>
/// <param name="options">The <see cref="McpInspectorOptions"/> to configure the MCP Inspector resource.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{McpInspectorResource}"/> for further configuration.</returns>
public static IResourceBuilder<McpInspectorResource> 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@{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)
.WithEnvironment("DANGEROUSLY_OMIT_AUTH", "true")
.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 =>
{
Expand All @@ -35,6 +59,7 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
{
annotation.DisplayText = "Server Proxy";
annotation.DisplayOrder = 1;
annotation.DisplayLocation = UrlDisplayLocation.DetailsOnly;
})
.OnBeforeResourceStarted(async (inspectorResource, @event, ct) =>
{
Expand Down Expand Up @@ -78,8 +103,76 @@ public static IResourceBuilder<McpInspectorResource> 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;
})
.WithDefaultArgs();
.WithDefaultArgs()
.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)
{
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 ?? 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);

return resource.WithHealthCheck(healthCheckKey);
}

/// <summary>
/// Adds a MCP Inspector container resource to the <see cref="IDistributedApplicationBuilder"/> using a configuration delegate.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to which the MCP Inspector resource will be added.</param>
/// <param name="name">The name of the MCP Inspector container resource.</param>
/// <param name="configureOptions">A delegate to configure the <see cref="McpInspectorOptions"/>.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{McpInspectorResource}"/> for further configuration.</returns>
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name, Action<McpInspectorOptions> configureOptions)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(configureOptions);

var options = new McpInspectorOptions();
configureOptions(options);

return builder.AddMcpInspector(name, options);
}

/// <summary>
/// Adds a MCP Inspector container resource to the <see cref="IDistributedApplicationBuilder"/> using a configuration delegate.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to which the MCP Inspector resource will be added.</param>
/// <param name="name">The name of the MCP Inspector container resource.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{McpInspectorResource}"/> for further configuration.</returns>
public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistributedApplicationBuilder builder, [ResourceName] string name)
{
ArgumentNullException.ThrowIfNull(builder);

var options = new McpInspectorOptions();

return builder.AddMcpInspector(name, options);
}

/// <summary>
Expand Down
44 changes: 42 additions & 2 deletions src/CommunityToolkit.Aspire.Hosting.McpInspector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,52 @@ 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

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

## Feedback & contributing

https://github.com/CommunityToolkit/Aspire
[https://github.com/CommunityToolkit/Aspire](https://github.com/CommunityToolkit/Aspire)
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Testing;

namespace CommunityToolkit.Aspire.Hosting.McpInspector.Tests;

public class AppHostTests(AspireIntegrationTestFixture<Projects.CommunityToolkit_Aspire_Hosting_McpInspector_AppHost> fixture) : IClassFixture<AspireIntegrationTestFixture<Projects.CommunityToolkit_Aspire_Hosting_McpInspector_AppHost>>
{
[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<DistributedApplicationModel>();
var mcpInspectorResource = appModel.Resources.OfType<McpInspectorResource>().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);
}
Expand Down
Loading
Loading