diff --git a/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs b/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs index a549ec384..3f5870700 100644 --- a/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs +++ b/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs @@ -43,31 +43,39 @@ public void PostConfigure(string? name, McpServerOptions options) private void ConfigureListToolsFilter(McpServerOptions options) { - options.Filters.Request.ListToolsFilters.Add(next => async (context, cancellationToken) => + options.Filters.Request.ListToolsFilters.Add(next => { - context.Items[AuthorizationFilterInvokedKey] = true; - - var result = await next(context, cancellationToken); - await FilterAuthorizedItemsAsync( - result.Tools, static tool => tool.McpServerTool, - context.User, context.Services, context); - return result; + var toolCollection = options.ToolCollection; + return async (context, cancellationToken) => + { + context.Items[AuthorizationFilterInvokedKey] = true; + + var result = await next(context, cancellationToken); + await FilterAuthorizedItemsAsync( + result.Tools, tool => toolCollection is not null && toolCollection.TryGetPrimitive(tool.Name, out var serverTool) ? serverTool : null, + context.User, context.Services, context); + return result; + }; }); } private static void CheckListToolsFilter(McpServerOptions options) { - options.Filters.Request.ListToolsFilters.Add(next => async (context, cancellationToken) => + options.Filters.Request.ListToolsFilters.Add(next => { - var result = await next(context, cancellationToken); - - if (HasAuthorizationMetadata(result.Tools.Select(static tool => tool.McpServerTool)) - && !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) + var toolCollection = options.ToolCollection; + return async (context, cancellationToken) => { - throw new InvalidOperationException("Authorization filter was not invoked for tools/list operation, but authorization metadata was found on the tools. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters."); - } + var result = await next(context, cancellationToken); + + if (HasAuthorizationMetadata(result.Tools.Select(tool => toolCollection is not null && toolCollection.TryGetPrimitive(tool.Name, out var serverTool) ? serverTool : null)) + && !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) + { + throw new InvalidOperationException("Authorization filter was not invoked for tools/list operation, but authorization metadata was found on the tools. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters."); + } - return result; + return result; + }; }); } @@ -103,61 +111,77 @@ private static void CheckCallToolFilter(McpServerOptions options) private void ConfigureListResourcesFilter(McpServerOptions options) { - options.Filters.Request.ListResourcesFilters.Add(next => async (context, cancellationToken) => + options.Filters.Request.ListResourcesFilters.Add(next => { - context.Items[AuthorizationFilterInvokedKey] = true; - - var result = await next(context, cancellationToken); - await FilterAuthorizedItemsAsync( - result.Resources, static resource => resource.McpServerResource, - context.User, context.Services, context); - return result; + var resourceCollection = options.ResourceCollection; + return async (context, cancellationToken) => + { + context.Items[AuthorizationFilterInvokedKey] = true; + + var result = await next(context, cancellationToken); + await FilterAuthorizedItemsAsync( + result.Resources, resource => resourceCollection is not null && resourceCollection.TryGetPrimitive(resource.Uri, out var serverResource) ? serverResource : null, + context.User, context.Services, context); + return result; + }; }); } private static void CheckListResourcesFilter(McpServerOptions options) { - options.Filters.Request.ListResourcesFilters.Add(next => async (context, cancellationToken) => + options.Filters.Request.ListResourcesFilters.Add(next => { - var result = await next(context, cancellationToken); - - if (HasAuthorizationMetadata(result.Resources.Select(static resource => resource.McpServerResource)) - && !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) + var resourceCollection = options.ResourceCollection; + return async (context, cancellationToken) => { - throw new InvalidOperationException("Authorization filter was not invoked for resources/list operation, but authorization metadata was found on the resources. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters."); - } + var result = await next(context, cancellationToken); + + if (HasAuthorizationMetadata(result.Resources.Select(resource => resourceCollection is not null && resourceCollection.TryGetPrimitive(resource.Uri, out var serverResource) ? serverResource : null)) + && !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) + { + throw new InvalidOperationException("Authorization filter was not invoked for resources/list operation, but authorization metadata was found on the resources. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters."); + } - return result; + return result; + }; }); } private void ConfigureListResourceTemplatesFilter(McpServerOptions options) { - options.Filters.Request.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) => + options.Filters.Request.ListResourceTemplatesFilters.Add(next => { - context.Items[AuthorizationFilterInvokedKey] = true; - - var result = await next(context, cancellationToken); - await FilterAuthorizedItemsAsync( - result.ResourceTemplates, static resourceTemplate => resourceTemplate.McpServerResource, - context.User, context.Services, context); - return result; + var resourceCollection = options.ResourceCollection; + return async (context, cancellationToken) => + { + context.Items[AuthorizationFilterInvokedKey] = true; + + var result = await next(context, cancellationToken); + await FilterAuthorizedItemsAsync( + result.ResourceTemplates, resourceTemplate => resourceCollection is not null && resourceCollection.TryGetPrimitive(resourceTemplate.UriTemplate, out var serverResource) ? serverResource : null, + context.User, context.Services, context); + return result; + }; }); } private static void CheckListResourceTemplatesFilter(McpServerOptions options) { - options.Filters.Request.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) => + options.Filters.Request.ListResourceTemplatesFilters.Add(next => { - var result = await next(context, cancellationToken); - - if (HasAuthorizationMetadata(result.ResourceTemplates.Select(static resourceTemplate => resourceTemplate.McpServerResource)) - && !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) + var resourceCollection = options.ResourceCollection; + return async (context, cancellationToken) => { - throw new InvalidOperationException("Authorization filter was not invoked for resources/templates/list operation, but authorization metadata was found on the resource templates. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters."); - } + var result = await next(context, cancellationToken); + + if (HasAuthorizationMetadata(result.ResourceTemplates.Select(resourceTemplate => resourceCollection is not null && resourceCollection.TryGetPrimitive(resourceTemplate.UriTemplate, out var serverResource) ? serverResource : null)) + && !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) + { + throw new InvalidOperationException("Authorization filter was not invoked for resources/templates/list operation, but authorization metadata was found on the resource templates. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters."); + } - return result; + return result; + }; }); } @@ -193,31 +217,39 @@ private static void CheckReadResourceFilter(McpServerOptions options) private void ConfigureListPromptsFilter(McpServerOptions options) { - options.Filters.Request.ListPromptsFilters.Add(next => async (context, cancellationToken) => + options.Filters.Request.ListPromptsFilters.Add(next => { - context.Items[AuthorizationFilterInvokedKey] = true; - - var result = await next(context, cancellationToken); - await FilterAuthorizedItemsAsync( - result.Prompts, static prompt => prompt.McpServerPrompt, - context.User, context.Services, context); - return result; + var promptCollection = options.PromptCollection; + return async (context, cancellationToken) => + { + context.Items[AuthorizationFilterInvokedKey] = true; + + var result = await next(context, cancellationToken); + await FilterAuthorizedItemsAsync( + result.Prompts, prompt => promptCollection is not null && promptCollection.TryGetPrimitive(prompt.Name, out var serverPrompt) ? serverPrompt : null, + context.User, context.Services, context); + return result; + }; }); } private static void CheckListPromptsFilter(McpServerOptions options) { - options.Filters.Request.ListPromptsFilters.Add(next => async (context, cancellationToken) => + options.Filters.Request.ListPromptsFilters.Add(next => { - var result = await next(context, cancellationToken); - - if (HasAuthorizationMetadata(result.Prompts.Select(static prompt => prompt.McpServerPrompt)) - && !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) + var promptCollection = options.PromptCollection; + return async (context, cancellationToken) => { - throw new InvalidOperationException("Authorization filter was not invoked for prompts/list operation, but authorization metadata was found on the prompts. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters."); - } + var result = await next(context, cancellationToken); + + if (HasAuthorizationMetadata(result.Prompts.Select(prompt => promptCollection is not null && promptCollection.TryGetPrimitive(prompt.Name, out var serverPrompt) ? serverPrompt : null)) + && !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) + { + throw new InvalidOperationException("Authorization filter was not invoked for prompts/list operation, but authorization metadata was found on the prompts. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters."); + } - return result; + return result; + }; }); } diff --git a/src/ModelContextProtocol.Core/Protocol/Prompt.cs b/src/ModelContextProtocol.Core/Protocol/Prompt.cs index 9f5534a31..bc4624f1d 100644 --- a/src/ModelContextProtocol.Core/Protocol/Prompt.cs +++ b/src/ModelContextProtocol.Core/Protocol/Prompt.cs @@ -1,7 +1,6 @@ using System.Diagnostics; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using ModelContextProtocol.Server; namespace ModelContextProtocol.Protocol; @@ -72,12 +71,6 @@ public sealed class Prompt : IBaseMetadata [JsonPropertyName("_meta")] public JsonObject? Meta { get; set; } - /// - /// Gets or sets the callable server prompt corresponding to this metadata if any. - /// - [JsonIgnore] - public McpServerPrompt? McpServerPrompt { get; set; } - [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay { diff --git a/src/ModelContextProtocol.Core/Protocol/Resource.cs b/src/ModelContextProtocol.Core/Protocol/Resource.cs index be2a4aa60..42bf22c57 100644 --- a/src/ModelContextProtocol.Core/Protocol/Resource.cs +++ b/src/ModelContextProtocol.Core/Protocol/Resource.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using ModelContextProtocol.Server; namespace ModelContextProtocol.Protocol; @@ -101,10 +100,4 @@ public sealed class Resource : IBaseMetadata /// [JsonPropertyName("_meta")] public JsonObject? Meta { get; set; } - - /// - /// Gets or sets the callable server resource corresponding to this metadata if any. - /// - [JsonIgnore] - public McpServerResource? McpServerResource { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs b/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs index af37a3164..09edb09fc 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs @@ -1,6 +1,5 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using ModelContextProtocol.Server; namespace ModelContextProtocol.Protocol; @@ -94,12 +93,6 @@ public sealed class ResourceTemplate : IBaseMetadata [JsonIgnore] public bool IsTemplated => UriTemplate.Contains('{'); - /// - /// Gets or sets the callable server resource corresponding to this metadata, if any. - /// - [JsonIgnore] - public McpServerResource? McpServerResource { get; set; } - /// Converts the into a . /// A if is ; otherwise, . public Resource? AsResource() @@ -119,7 +112,6 @@ public sealed class ResourceTemplate : IBaseMetadata Annotations = Annotations, Icons = Icons, Meta = Meta, - McpServerResource = McpServerResource, }; } } diff --git a/src/ModelContextProtocol.Core/Protocol/Tool.cs b/src/ModelContextProtocol.Core/Protocol/Tool.cs index dfce0eaa1..f70a1d260 100644 --- a/src/ModelContextProtocol.Core/Protocol/Tool.cs +++ b/src/ModelContextProtocol.Core/Protocol/Tool.cs @@ -1,4 +1,3 @@ -using ModelContextProtocol.Server; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json; @@ -158,12 +157,6 @@ public ToolExecution? Execution [JsonPropertyName("_meta")] public JsonObject? Meta { get; set; } - /// - /// Gets or sets the callable server tool corresponding to this metadata if any. - /// - [JsonIgnore] - public McpServerTool? McpServerTool { get; set; } - [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay { diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs index c55c76465..c755cd9e5 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs @@ -182,7 +182,6 @@ private AIFunctionMcpServerPrompt(AIFunction function, Prompt prompt, IReadOnlyL { AIFunction = function; ProtocolPrompt = prompt; - ProtocolPrompt.McpServerPrompt = this; _metadata = metadata; } diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs index 469ba5d37..3413d7038 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs @@ -294,7 +294,6 @@ private AIFunctionMcpServerResource(AIFunction function, ResourceTemplate resour { AIFunction = function; ProtocolResourceTemplate = resourceTemplate; - ProtocolResourceTemplate.McpServerResource = this; ProtocolResource = resourceTemplate.AsResource(); _metadata = metadata; diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index f72e5a483..f29d8fef1 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -234,7 +234,6 @@ private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider AIFunction = function; ProtocolTool = tool; - ProtocolTool.McpServerTool = this; _structuredOutputRequiresWrapping = structuredOutputRequiresWrapping; _metadata = metadata; diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/AuthorizeAttributeTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/AuthorizeAttributeTests.cs index 33535cc09..76a7201d8 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/AuthorizeAttributeTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/AuthorizeAttributeTests.cs @@ -398,6 +398,105 @@ log.Exception is InvalidOperationException && log.Exception.Message.Contains("Ensure that AddAuthorizationFilters() is called")); } + [Fact] + public async Task ListTools_WithHandlerAndNullCollection_AllToolsVisible() + { + // When ToolCollection is null (custom handler only), the auth filter can't look up + // primitives in the collection and should not filter any tools. + await using var app = await StartServerWithAuth(builder => + builder.WithListToolsHandler(static (_, _) => ValueTask.FromResult(new ListToolsResult + { + Tools = [new Tool { Name = "custom_tool" }] + }))); + + var client = await ConnectAsync(); + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + + // Tool from custom handler (not in ToolCollection) should be visible even to anonymous users + Assert.Single(tools); + Assert.Equal("custom_tool", tools[0].Name); + } + + [Fact] + public async Task ListTools_WithMixedCollectionAndHandler_HandlerToolsNotFiltered() + { + // Tools in the ToolCollection are filtered based on auth metadata. + // Tools returned only from a custom handler (not in ToolCollection) are not filtered. + await using var app = await StartServerWithAuth(builder => + { + builder.WithTools(); + builder.WithListToolsHandler(static (_, _) => ValueTask.FromResult(new ListToolsResult + { + Tools = [new Tool { Name = "handler_tool" }] + })); + }); + + var client = await ConnectAsync(); + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + + // Anonymous user: anonymous_tool from collection + handler_tool (not in collection, so not filtered) + Assert.Equal(2, tools.Count); + var toolNames = tools.Select(t => t.Name).OrderBy(n => n).ToList(); + Assert.Equal(["anonymous_tool", "handler_tool"], toolNames); + } + + [Fact] + public async Task ListPrompts_WithHandlerAndNullCollection_AllPromptsVisible() + { + // When PromptCollection is null (custom handler only), the auth filter can't look up + // primitives in the collection and should not filter any prompts. + await using var app = await StartServerWithAuth(builder => + builder.WithListPromptsHandler(static (_, _) => ValueTask.FromResult(new ListPromptsResult + { + Prompts = [new Prompt { Name = "custom_prompt" }] + }))); + + var client = await ConnectAsync(); + var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); + + // Prompt from custom handler (not in PromptCollection) should be visible even to anonymous users + Assert.Single(prompts); + Assert.Equal("custom_prompt", prompts[0].Name); + } + + [Fact] + public async Task ListResources_WithHandlerAndNullCollection_AllResourcesVisible() + { + // When ResourceCollection is null (custom handler only), the auth filter can't look up + // primitives in the collection and should not filter any resources. + await using var app = await StartServerWithAuth(builder => + builder.WithListResourcesHandler(static (_, _) => ValueTask.FromResult(new ListResourcesResult + { + Resources = [new Resource { Name = "custom_resource", Uri = "resource://custom" }] + }))); + + var client = await ConnectAsync(); + var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); + + // Resource from custom handler (not in ResourceCollection) should be visible even to anonymous users + Assert.Single(resources); + Assert.Equal("resource://custom", resources[0].Uri); + } + + [Fact] + public async Task ListResourceTemplates_WithHandlerAndNullCollection_AllResourceTemplatesVisible() + { + // When ResourceCollection is null (custom handler only), the auth filter can't look up + // primitives in the collection and should not filter any resource templates. + await using var app = await StartServerWithAuth(builder => + builder.WithListResourceTemplatesHandler(static (_, _) => ValueTask.FromResult(new ListResourceTemplatesResult + { + ResourceTemplates = [new ResourceTemplate { Name = "custom_template", UriTemplate = "resource://custom/{id}" }] + }))); + + var client = await ConnectAsync(); + var templates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken); + + // Template from custom handler (not in ResourceCollection) should be visible even to anonymous users + Assert.Single(templates); + Assert.Equal("resource://custom/{id}", templates[0].UriTemplate); + } + private async Task StartServerWithAuth(Action configure, string? userName = null, params string[] roles) { var mcpServerBuilder = Builder.Services.AddMcpServer().WithHttpTransport().AddAuthorizationFilters();