diff --git a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs index e8fcad33d..73f5aaa2e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.McpInspector/McpInspectorResourceBuilderExtensions.cs @@ -197,6 +197,9 @@ public static IResourceBuilder WithMcpServer( ArgumentNullException.ThrowIfNull(builder); builder.Resource.AddMcpServer(mcpServer.Resource, isDefault, transportType, path); + + mcpServer.WithRelationship(builder.Resource, "InspectedBy"); + return builder; } @@ -239,11 +242,12 @@ internal static Uri Combine(string baseUrl, params string[] segments) if (Uri.IsWellFormedUriString(segments[0], UriKind.Absolute)) return new Uri(segments[0], UriKind.Absolute); - var escaped = segments + var escapedSegments = segments .Where(s => !string.IsNullOrEmpty(s)) - .Select(s => Uri.EscapeDataString(s.Trim('/'))); + .SelectMany(s => s.Trim('/').Split('/', StringSplitOptions.RemoveEmptyEntries)) + .Select(Uri.EscapeDataString); - var relative = string.Join("/", escaped); + var relative = string.Join("/", escapedSegments); return new Uri(baseUri, relative); } diff --git a/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs index d3af17914..430fadef2 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/McpInspectorResourceBuilderExtensionsTests.cs @@ -297,4 +297,81 @@ public void AddMcpInspectorWithConfigurationDelegateCreatesResourceCorrectly() Assert.Equal(3333, clientEndpoint.Port); Assert.Equal(4444, serverEndpoint.Port); } + + [Fact] + public void WithMcpServerCreatesResourceRelationshipAnnotations() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + + // Create a mock MCP server resource + var mockServer = appBuilder.AddProject("mcpServer"); + + // Act + var inspector = appBuilder.AddMcpInspector("inspector") + .WithMcpServer(mockServer, isDefault: true); + + using var app = appBuilder.Build(); + + // Assert + var appModel = app.Services.GetRequiredService(); + + var inspectorResource = Assert.Single(appModel.Resources.OfType()); + var serverResource = appModel.Resources.Single(r => r.Name == "mcpServer"); + + // The inspector and/or server should have a resource relationship annotation linking them. + var serverHasRelationship = serverResource.TryGetAnnotationsOfType(out var serverRelationships); + var inspectorHasRelationship = inspectorResource.TryGetAnnotationsOfType(out var inspectorRelationships); + + Assert.True(serverHasRelationship || inspectorHasRelationship, "Expected a ResourceRelationshipAnnotation on either the server or inspector resource."); + } + + [Fact] + public void WithMcpServerPreservesCustomPathSegments() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + + // Create a mock MCP server resource + var mockServer = appBuilder.AddProject("mcpServer"); + + // Act + var inspector = appBuilder.AddMcpInspector("inspector") + .WithMcpServer(mockServer, isDefault: true, path: "/route/mcp"); + + using var app = appBuilder.Build(); + + // Assert + var appModel = app.Services.GetRequiredService(); + + var inspectorResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Single(inspectorResource.McpServers); + var serverMeta = inspectorResource.McpServers.Single(); + + // Path should be preserved exactly as provided (not url-encoded) + Assert.Equal("/route/mcp", serverMeta.Path); + } + + [Fact] + public void CombineHandlesMultipleSegmentsAndDoesNotEncodeSlashes() + { + // Arrange + var baseUrl = "http://localhost:1234"; + var segments = new[] { "/route/mcp", "nested/path" }; + + // Use reflection to call internal Combine + var type = typeof(McpInspectorResourceBuilderExtensions); + var method = type.GetMethod("Combine", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + Assert.NotNull(method); + + // Act + var result = method!.Invoke(null, new object[] { baseUrl, segments }) as Uri; + + // Assert + Assert.NotNull(result); + // Ensure that slashes from segments are preserved and not percent-encoded + var expected = new Uri("http://localhost:1234/route/mcp/nested/path"); + Assert.Equal(expected, result); + } }