Remove server back-references from protocol DTO types (#1345)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
parent
2c9bb5d368
commit
74622d0c9c
9 changed files with 195 additions and 96 deletions
|
|
@ -43,31 +43,39 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
|
||||||
|
|
||||||
private void ConfigureListToolsFilter(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 toolCollection = options.ToolCollection;
|
||||||
|
return async (context, cancellationToken) =>
|
||||||
|
{
|
||||||
|
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||||
|
|
||||||
var result = await next(context, cancellationToken);
|
var result = await next(context, cancellationToken);
|
||||||
await FilterAuthorizedItemsAsync(
|
await FilterAuthorizedItemsAsync(
|
||||||
result.Tools, static tool => tool.McpServerTool,
|
result.Tools, tool => toolCollection is not null && toolCollection.TryGetPrimitive(tool.Name, out var serverTool) ? serverTool : null,
|
||||||
context.User, context.Services, context);
|
context.User, context.Services, context);
|
||||||
return result;
|
return result;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CheckListToolsFilter(McpServerOptions options)
|
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);
|
var toolCollection = options.ToolCollection;
|
||||||
|
return async (context, cancellationToken) =>
|
||||||
if (HasAuthorizationMetadata(result.Tools.Select(static tool => tool.McpServerTool))
|
|
||||||
&& !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.");
|
var result = await next(context, cancellationToken);
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
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;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,61 +111,77 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
|
||||||
|
|
||||||
private void ConfigureListResourcesFilter(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 resourceCollection = options.ResourceCollection;
|
||||||
|
return async (context, cancellationToken) =>
|
||||||
|
{
|
||||||
|
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||||
|
|
||||||
var result = await next(context, cancellationToken);
|
var result = await next(context, cancellationToken);
|
||||||
await FilterAuthorizedItemsAsync(
|
await FilterAuthorizedItemsAsync(
|
||||||
result.Resources, static resource => resource.McpServerResource,
|
result.Resources, resource => resourceCollection is not null && resourceCollection.TryGetPrimitive(resource.Uri, out var serverResource) ? serverResource : null,
|
||||||
context.User, context.Services, context);
|
context.User, context.Services, context);
|
||||||
return result;
|
return result;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CheckListResourcesFilter(McpServerOptions options)
|
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);
|
var resourceCollection = options.ResourceCollection;
|
||||||
|
return async (context, cancellationToken) =>
|
||||||
if (HasAuthorizationMetadata(result.Resources.Select(static resource => resource.McpServerResource))
|
|
||||||
&& !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.");
|
var result = await next(context, cancellationToken);
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
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;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigureListResourceTemplatesFilter(McpServerOptions options)
|
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 resourceCollection = options.ResourceCollection;
|
||||||
|
return async (context, cancellationToken) =>
|
||||||
|
{
|
||||||
|
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||||
|
|
||||||
var result = await next(context, cancellationToken);
|
var result = await next(context, cancellationToken);
|
||||||
await FilterAuthorizedItemsAsync(
|
await FilterAuthorizedItemsAsync(
|
||||||
result.ResourceTemplates, static resourceTemplate => resourceTemplate.McpServerResource,
|
result.ResourceTemplates, resourceTemplate => resourceCollection is not null && resourceCollection.TryGetPrimitive(resourceTemplate.UriTemplate, out var serverResource) ? serverResource : null,
|
||||||
context.User, context.Services, context);
|
context.User, context.Services, context);
|
||||||
return result;
|
return result;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CheckListResourceTemplatesFilter(McpServerOptions options)
|
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);
|
var resourceCollection = options.ResourceCollection;
|
||||||
|
return async (context, cancellationToken) =>
|
||||||
if (HasAuthorizationMetadata(result.ResourceTemplates.Select(static resourceTemplate => resourceTemplate.McpServerResource))
|
|
||||||
&& !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.");
|
var result = await next(context, cancellationToken);
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
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;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,31 +217,39 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
|
||||||
|
|
||||||
private void ConfigureListPromptsFilter(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 promptCollection = options.PromptCollection;
|
||||||
|
return async (context, cancellationToken) =>
|
||||||
|
{
|
||||||
|
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||||
|
|
||||||
var result = await next(context, cancellationToken);
|
var result = await next(context, cancellationToken);
|
||||||
await FilterAuthorizedItemsAsync(
|
await FilterAuthorizedItemsAsync(
|
||||||
result.Prompts, static prompt => prompt.McpServerPrompt,
|
result.Prompts, prompt => promptCollection is not null && promptCollection.TryGetPrimitive(prompt.Name, out var serverPrompt) ? serverPrompt : null,
|
||||||
context.User, context.Services, context);
|
context.User, context.Services, context);
|
||||||
return result;
|
return result;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CheckListPromptsFilter(McpServerOptions options)
|
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);
|
var promptCollection = options.PromptCollection;
|
||||||
|
return async (context, cancellationToken) =>
|
||||||
if (HasAuthorizationMetadata(result.Prompts.Select(static prompt => prompt.McpServerPrompt))
|
|
||||||
&& !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.");
|
var result = await next(context, cancellationToken);
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
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;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol;
|
namespace ModelContextProtocol.Protocol;
|
||||||
|
|
||||||
|
|
@ -72,12 +71,6 @@ public sealed class Prompt : IBaseMetadata
|
||||||
[JsonPropertyName("_meta")]
|
[JsonPropertyName("_meta")]
|
||||||
public JsonObject? Meta { get; set; }
|
public JsonObject? Meta { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the callable server prompt corresponding to this metadata if any.
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public McpServerPrompt? McpServerPrompt { get; set; }
|
|
||||||
|
|
||||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||||
private string DebuggerDisplay
|
private string DebuggerDisplay
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol;
|
namespace ModelContextProtocol.Protocol;
|
||||||
|
|
||||||
|
|
@ -101,10 +100,4 @@ public sealed class Resource : IBaseMetadata
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonPropertyName("_meta")]
|
[JsonPropertyName("_meta")]
|
||||||
public JsonObject? Meta { get; set; }
|
public JsonObject? Meta { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the callable server resource corresponding to this metadata if any.
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public McpServerResource? McpServerResource { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol;
|
namespace ModelContextProtocol.Protocol;
|
||||||
|
|
||||||
|
|
@ -94,12 +93,6 @@ public sealed class ResourceTemplate : IBaseMetadata
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsTemplated => UriTemplate.Contains('{');
|
public bool IsTemplated => UriTemplate.Contains('{');
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the callable server resource corresponding to this metadata, if any.
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public McpServerResource? McpServerResource { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Converts the <see cref="ResourceTemplate"/> into a <see cref="Resource"/>.</summary>
|
/// <summary>Converts the <see cref="ResourceTemplate"/> into a <see cref="Resource"/>.</summary>
|
||||||
/// <returns>A <see cref="Resource"/> if <see cref="IsTemplated"/> is <see langword="false"/>; otherwise, <see langword="null"/>.</returns>
|
/// <returns>A <see cref="Resource"/> if <see cref="IsTemplated"/> is <see langword="false"/>; otherwise, <see langword="null"/>.</returns>
|
||||||
public Resource? AsResource()
|
public Resource? AsResource()
|
||||||
|
|
@ -119,7 +112,6 @@ public sealed class ResourceTemplate : IBaseMetadata
|
||||||
Annotations = Annotations,
|
Annotations = Annotations,
|
||||||
Icons = Icons,
|
Icons = Icons,
|
||||||
Meta = Meta,
|
Meta = Meta,
|
||||||
McpServerResource = McpServerResource,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
@ -158,12 +157,6 @@ public sealed class Tool : IBaseMetadata
|
||||||
[JsonPropertyName("_meta")]
|
[JsonPropertyName("_meta")]
|
||||||
public JsonObject? Meta { get; set; }
|
public JsonObject? Meta { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the callable server tool corresponding to this metadata if any.
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public McpServerTool? McpServerTool { get; set; }
|
|
||||||
|
|
||||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||||
private string DebuggerDisplay
|
private string DebuggerDisplay
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,6 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
||||||
{
|
{
|
||||||
AIFunction = function;
|
AIFunction = function;
|
||||||
ProtocolPrompt = prompt;
|
ProtocolPrompt = prompt;
|
||||||
ProtocolPrompt.McpServerPrompt = this;
|
|
||||||
_metadata = metadata;
|
_metadata = metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -294,7 +294,6 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
||||||
{
|
{
|
||||||
AIFunction = function;
|
AIFunction = function;
|
||||||
ProtocolResourceTemplate = resourceTemplate;
|
ProtocolResourceTemplate = resourceTemplate;
|
||||||
ProtocolResourceTemplate.McpServerResource = this;
|
|
||||||
ProtocolResource = resourceTemplate.AsResource();
|
ProtocolResource = resourceTemplate.AsResource();
|
||||||
_metadata = metadata;
|
_metadata = metadata;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,6 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
||||||
|
|
||||||
AIFunction = function;
|
AIFunction = function;
|
||||||
ProtocolTool = tool;
|
ProtocolTool = tool;
|
||||||
ProtocolTool.McpServerTool = this;
|
|
||||||
|
|
||||||
_structuredOutputRequiresWrapping = structuredOutputRequiresWrapping;
|
_structuredOutputRequiresWrapping = structuredOutputRequiresWrapping;
|
||||||
_metadata = metadata;
|
_metadata = metadata;
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,105 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
|
||||||
log.Exception.Message.Contains("Ensure that AddAuthorizationFilters() is called"));
|
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<AuthorizationTestTools>();
|
||||||
|
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<WebApplication> StartServerWithAuth(Action<IMcpServerBuilder> configure, string? userName = null, params string[] roles)
|
private async Task<WebApplication> StartServerWithAuth(Action<IMcpServerBuilder> configure, string? userName = null, params string[] roles)
|
||||||
{
|
{
|
||||||
var mcpServerBuilder = Builder.Services.AddMcpServer().WithHttpTransport().AddAuthorizationFilters();
|
var mcpServerBuilder = Builder.Services.AddMcpServer().WithHttpTransport().AddAuthorizationFilters();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue