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
4 changes: 3 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"version": "21"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/node:latest": {},
"ghcr.io/devcontainers/features/node:latest": {
"installYarnUsingApt": false,
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing comma after false should be removed. JSON doesn't allow trailing commas in object properties.

Suggested change
"installYarnUsingApt": false,
"installYarnUsingApt": false

Copilot uses AI. Check for mistakes.
},
"ghcr.io/devcontainers-community/features/deno": {},
"ghcr.io/devcontainers/features/go:latest": {},
"ghcr.io/devcontainers/features/rust:latest": {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ namespace Aspire.Hosting.ApplicationModel;
/// Resource for the MCP Inspector server.
/// </summary>
/// <param name="name">The name of the resource.</param>
public class McpInspectorResource(string name) : JavaScriptAppResource(name, "npx", "")
/// <param name="packageName">The npm package name for the MCP Inspector.</param>
public class McpInspectorResource(string name, string packageName) : JavaScriptAppResource(name, "npx", "")
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a required packageName parameter to the constructor is a breaking change for anyone directly instantiating McpInspectorResource. Consider making the parameter optional with a default value (e.g., string packageName = "@modelcontextprotocol/inspector@" + InspectorVersion) or providing a constructor overload to maintain backward compatibility.

Suggested change
public class McpInspectorResource(string name, string packageName) : JavaScriptAppResource(name, "npx", "")
public class McpInspectorResource(string name, string packageName = "@modelcontextprotocol/inspector@" + InspectorVersion) : JavaScriptAppResource(name, "npx", "")

Copilot uses AI. Check for mistakes.
{
internal readonly string ConfigPath = Path.GetTempFileName();

/// <summary>
/// Gets the npm package name for the MCP Inspector.
/// </summary>
internal string PackageName { get; } = packageName;

/// <summary>
/// The name of the client endpoint.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.JavaScript;
using CommunityToolkit.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;

Expand Down Expand Up @@ -37,18 +38,28 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
/// <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>
/// <remarks>
/// By default, the MCP Inspector uses npm/npx. To use a different package manager, chain the appropriate method:
/// <code>
/// builder.AddMcpInspector("inspector")
/// .WithYarn();
/// </code>
/// </remarks>
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))
.WithNpm(install: true, installArgs: ["-y", $"@modelcontextprotocol/inspector@{options.InspectorVersion}", "--no-save", "--no-package-lock"])
.WithCommand("npx")
.WithArgs(["-y", $"@modelcontextprotocol/inspector@{options.InspectorVersion}"])
var packageName = $"@modelcontextprotocol/inspector@{options.InspectorVersion}";

var resourceBuilder = builder.AddResource(new McpInspectorResource(name, packageName));

resourceBuilder
.ExcludeFromManifest()
.WithInspectorArgs()
.WithDefaultArgs()
.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)
Expand Down Expand Up @@ -108,7 +119,6 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
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);
Expand All @@ -126,13 +136,13 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
}
});

resource.Resource.ProxyTokenParameter = proxyTokenParameter;
resourceBuilder.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 serverProxyEndpoint = resourceBuilder.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 =>
Expand All @@ -143,7 +153,7 @@ public static IResourceBuilder<McpInspectorResource> AddMcpInspector(this IDistr
}, healthCheckKey);
builder.Services.SuppressHealthCheckHttpClientLogging(healthCheckKey);

return resource.WithHealthCheck(healthCheckKey);
return resourceBuilder.WithHealthCheck(healthCheckKey);
}

/// <summary>
Expand Down Expand Up @@ -261,4 +271,54 @@ internal static Uri Combine(string baseUrl, params string[] segments)

return new Uri(baseUri, relative);
}

/// <summary>
/// Sets up the command and arguments for the MCP Inspector based on the configured package manager.
/// </summary>
private static IResourceBuilder<McpInspectorResource> WithInspectorArgs(this IResourceBuilder<McpInspectorResource> builder)
{
return builder.WithArgs(ctx =>
{
var resource = builder.Resource;
var packageName = resource.PackageName;

// Add the appropriate arguments based on the package manager
switch (resource.Command)
{
case "yarn":
case "pnpm":
ctx.Args.Insert(0, packageName);
ctx.Args.Insert(0, "dlx");
break;
default: // npm/npx
ctx.Args.Insert(0, packageName);
ctx.Args.Insert(0, "-y");
break;
}
});
}

/// <summary>
/// Configures the MCP Inspector to use yarn as the package manager.
/// </summary>
/// <param name="builder">The MCP Inspector resource builder.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<McpInspectorResource> WithYarn(this IResourceBuilder<McpInspectorResource> builder)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithCommand("yarn");
}

/// <summary>
/// Configures the MCP Inspector to use pnpm as the package manager.
/// </summary>
/// <param name="builder">The MCP Inspector resource builder.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<McpInspectorResource> WithPnpm(this IResourceBuilder<McpInspectorResource> builder)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithCommand("pnpm");
}
}
20 changes: 19 additions & 1 deletion src/CommunityToolkit.Aspire.Hosting.McpInspector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ var inspector = builder.AddMcpInspector("inspector")

You can specify the transport type (`StreamableHttp`) and set which server is the default for the inspector.

#### Using alternative package managers

By default, the MCP Inspector uses npm/npx. You can configure it to use yarn or pnpm instead by chaining the appropriate method:

```csharp
// Using yarn
var inspector = builder.AddMcpInspector("inspector")
.WithYarn()
.WithMcpServer(mcpServer);

// Using pnpm
var inspector = builder.AddMcpInspector("inspector")
.WithPnpm()
.WithMcpServer(mcpServer);
```

When using yarn or pnpm, the inspector will use `yarn dlx` or `pnpm dlx` respectively to run the MCP Inspector package.

#### Using options for complex configurations

For more complex configurations with multiple parameters, you can use the options-based approach:
Expand Down Expand Up @@ -60,7 +78,7 @@ var inspector = builder.AddMcpInspector("inspector", options =>

The `McpInspectorOptions` class provides the following configuration properties:

- `ClientPort`: Port for the client application (default: 6274
- `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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.JavaScript;

namespace CommunityToolkit.Aspire.Hosting.McpInspector.Tests;

Expand Down Expand Up @@ -491,4 +492,167 @@ public void WithMcpServerWithBothEndpointsUsesHttps()
// Verify the endpoint is the https one (preferred when both exist)
Assert.Equal("https", serverMetadata.Endpoint.EndpointName);
}

[Fact]
public void AddMcpInspectorDefaultsToNpx()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();

// Act
var inspector = appBuilder.AddMcpInspector("inspector");
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to obsolete method AddMcpInspector.

Suggested change
var inspector = appBuilder.AddMcpInspector("inspector");
var inspector = appBuilder.AddMcpInspectorResource("inspector");

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to inspector is useless, since its value is never read.

Suggested change
var inspector = appBuilder.AddMcpInspector("inspector");
appBuilder.AddMcpInspector("inspector");

Copilot uses AI. Check for mistakes.

using var app = appBuilder.Build();

// Assert
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());

// Default command is npx (set in constructor)
Assert.Equal("npx", inspectorResource.Command);
}

[Fact]
public void WithYarnSetsCommand()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();

// Act
var inspector = appBuilder.AddMcpInspector("inspector")
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to obsolete method AddMcpInspector.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to inspector is useless, since its value is never read.

Suggested change
var inspector = appBuilder.AddMcpInspector("inspector")
appBuilder.AddMcpInspector("inspector")

Copilot uses AI. Check for mistakes.
.WithYarn();

using var app = appBuilder.Build();

// Assert
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());

Assert.Equal("yarn", inspectorResource.Command);
}

[Fact]
public void WithPnpmSetsCommand()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();

// Act
var inspector = appBuilder.AddMcpInspector("inspector")
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to obsolete method AddMcpInspector.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to inspector is useless, since its value is never read.

Suggested change
var inspector = appBuilder.AddMcpInspector("inspector")
appBuilder.AddMcpInspector("inspector")

Copilot uses AI. Check for mistakes.
.WithPnpm();

using var app = appBuilder.Build();

// Assert
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());

Assert.Equal("pnpm", inspectorResource.Command);
}

[Fact]
public async Task WithYarnSetsCorrectArguments()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();

// Act
var inspector = appBuilder.AddMcpInspector("inspector", options =>
{
options.InspectorVersion = "0.15.0";
})
Comment on lines +560 to +563
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to obsolete method AddMcpInspector.

Copilot uses AI. Check for mistakes.
.WithYarn();
Comment on lines +560 to +564
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to inspector is useless, since its value is never read.

Copilot uses AI. Check for mistakes.

using var app = appBuilder.Build();

// Assert
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());

var args = await inspectorResource.GetArgumentValuesAsync();
var argsList = args.ToList();

// For yarn, the first arg should be "dlx"
Assert.Equal("dlx", argsList[0]);
Assert.Equal("@modelcontextprotocol/inspector@0.15.0", argsList[1]);
}

[Fact]
public async Task WithPnpmSetsCorrectArguments()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();

// Act
var inspector = appBuilder.AddMcpInspector("inspector", options =>
{
options.InspectorVersion = "0.15.0";
})
Comment on lines +587 to +590
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to obsolete method AddMcpInspector.

Copilot uses AI. Check for mistakes.
.WithPnpm();
Comment on lines +587 to +591
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to inspector is useless, since its value is never read.

Copilot uses AI. Check for mistakes.

using var app = appBuilder.Build();

// Assert
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());

var args = await inspectorResource.GetArgumentValuesAsync();
var argsList = args.ToList();

// For pnpm, the first arg should be "dlx"
Assert.Equal("dlx", argsList[0]);
Assert.Equal("@modelcontextprotocol/inspector@0.15.0", argsList[1]);
}

[Fact]
public async Task DefaultNpxUsesCorrectArguments()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();

// Act
var inspector = appBuilder.AddMcpInspector("inspector", options =>
{
options.InspectorVersion = "0.15.0";
});
Comment on lines +614 to +617
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to obsolete method AddMcpInspector.

Copilot uses AI. Check for mistakes.
Comment on lines +614 to +617
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to inspector is useless, since its value is never read.

Copilot uses AI. Check for mistakes.

using var app = appBuilder.Build();

// Assert
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());

var args = await inspectorResource.GetArgumentValuesAsync();
var argsList = args.ToList();

// For npm/npx, the first arg should be "-y"
Assert.Equal("-y", argsList[0]);
Assert.Equal("@modelcontextprotocol/inspector@0.15.0", argsList[1]);
}

[Fact]
public async Task AddMcpInspectorWithOptionsRespectsInspectorVersion()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();

var options = new McpInspectorOptions
{
InspectorVersion = "1.2.3"
};

// Act
var inspector = appBuilder.AddMcpInspector("inspector", options);

using var app = appBuilder.Build();

// Assert
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var inspectorResource = Assert.Single(appModel.Resources.OfType<McpInspectorResource>());

var args = await inspectorResource.GetArgumentValuesAsync();
var argsList = args.ToList();

Assert.Equal("@modelcontextprotocol/inspector@1.2.3", argsList[1]);
}
}
Loading