Fix URI template derivation and lookup (#530)
* Use lowercase host in resource URI template derivation. * Address feedback * Add casing testing for non-templated resource lookups. * Fix uri primitive lookup * Use a hardcoded mcp hostname in derived resource template URIs and ensure built-in resources work with the VS Code client. * Address feedback * Fix equality comparison * Use dedicated type for resource collections.
This commit is contained in:
parent
283a052e3f
commit
0b26b7bf22
6 changed files with 174 additions and 75 deletions
|
|
@ -103,5 +103,5 @@ public sealed class ResourcesCapability
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public McpServerPrimitiveCollection<McpServerResource>? ResourceCollection { get; set; }
|
||||
public McpServerResourceCollection? ResourceCollection { get; set; }
|
||||
}
|
||||
|
|
@ -264,7 +264,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
Name = name,
|
||||
Title = options?.Title,
|
||||
Description = options?.Description,
|
||||
MimeType = options?.MimeType,
|
||||
MimeType = options?.MimeType ?? "application/octet-stream",
|
||||
};
|
||||
|
||||
return new AIFunctionMcpServerResource(function, resource);
|
||||
|
|
@ -295,7 +295,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
{
|
||||
StringBuilder template = new();
|
||||
|
||||
template.Append("resource://").Append(Uri.EscapeDataString(name));
|
||||
template.Append("resource://mcp/").Append(Uri.EscapeDataString(name));
|
||||
|
||||
if (function.JsonSchema.TryGetProperty("properties", out JsonElement properties))
|
||||
{
|
||||
|
|
@ -359,7 +359,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
return null;
|
||||
}
|
||||
}
|
||||
else if (request.Params.Uri != ProtocolResource!.Uri)
|
||||
else if (!UriTemplate.UriTemplateComparer.Instance.Equals(request.Params.Uri, ProtocolResource!.Uri))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
|||
var unsubscribeHandler = resourcesCapability.UnsubscribeFromResourcesHandler ?? (static async (_, __) => new EmptyResult());
|
||||
var resources = resourcesCapability.ResourceCollection;
|
||||
var listChanged = resourcesCapability.ListChanged;
|
||||
var subcribe = resourcesCapability.Subscribe;
|
||||
var subscribe = resourcesCapability.Subscribe;
|
||||
|
||||
// Handle resources provided via DI.
|
||||
if (resources is { IsEmpty: false })
|
||||
|
|
@ -309,7 +309,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
|||
listChanged = true;
|
||||
|
||||
// TODO: Implement subscribe/unsubscribe logic for resource and resource template collections.
|
||||
// subcribe = true;
|
||||
// subscribe = true;
|
||||
}
|
||||
|
||||
ServerCapabilities.Resources.ListResourcesHandler = listResourcesHandler;
|
||||
|
|
@ -319,7 +319,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
|||
ServerCapabilities.Resources.SubscribeToResourcesHandler = subscribeHandler;
|
||||
ServerCapabilities.Resources.UnsubscribeFromResourcesHandler = unsubscribeHandler;
|
||||
ServerCapabilities.Resources.ListChanged = listChanged;
|
||||
ServerCapabilities.Resources.Subscribe = subcribe;
|
||||
ServerCapabilities.Resources.Subscribe = subscribe;
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesList,
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ public class McpServerPrimitiveCollection<T> : ICollection<T>, IReadOnlyCollecti
|
|||
where T : IMcpServerPrimitive
|
||||
{
|
||||
/// <summary>Concurrent dictionary of primitives, indexed by their names.</summary>
|
||||
private readonly ConcurrentDictionary<string, T> _primitives = [];
|
||||
private readonly ConcurrentDictionary<string, T> _primitives;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpServerPrimitiveCollection{T}"/> class.
|
||||
/// </summary>
|
||||
public McpServerPrimitiveCollection()
|
||||
public McpServerPrimitiveCollection(IEqualityComparer<string>? keyComparer = null)
|
||||
{
|
||||
_primitives = new(keyComparer);
|
||||
}
|
||||
|
||||
/// <summary>Occurs when the collection is changed.</summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>Provides a thread-safe collection of <see cref="McpServerResource"/> instances, indexed by their URI templates.</summary>
|
||||
public sealed class McpServerResourceCollection()
|
||||
: McpServerPrimitiveCollection<McpServerResource>(UriTemplate.UriTemplateComparer.Instance);
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
using System.Buffers;
|
||||
#endif
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
|
@ -453,4 +454,53 @@ internal static partial class UriTemplate
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines an equality comparer for Uri templates as follows:
|
||||
/// 1. Non-templated Uris use regular System.Uri equality comparison (host name is case insensitive).
|
||||
/// 2. Templated Uris use regular string equality.
|
||||
///
|
||||
/// We do this because non-templated resources are looked up directly from the resource dictionary
|
||||
/// and we need to make sure equality is implemented correctly. Templated Uris are resolved in a
|
||||
/// fallback step using linear traversal of the resource dictionary, so their equality is only
|
||||
/// there to distinguish between different templates.
|
||||
/// </summary>
|
||||
public sealed class UriTemplateComparer : IEqualityComparer<string>
|
||||
{
|
||||
public static IEqualityComparer<string> Instance { get; } = new UriTemplateComparer();
|
||||
|
||||
public bool Equals(string? uriTemplate1, string? uriTemplate2)
|
||||
{
|
||||
if (TryParseAsNonTemplatedUri(uriTemplate1, out Uri? uri1) &&
|
||||
TryParseAsNonTemplatedUri(uriTemplate2, out Uri? uri2))
|
||||
{
|
||||
return uri1 == uri2;
|
||||
}
|
||||
|
||||
return string.Equals(uriTemplate1, uriTemplate2, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public int GetHashCode([DisallowNull] string uriTemplate)
|
||||
{
|
||||
if (TryParseAsNonTemplatedUri(uriTemplate, out Uri? uri))
|
||||
{
|
||||
return uri.GetHashCode();
|
||||
}
|
||||
else
|
||||
{
|
||||
return StringComparer.Ordinal.GetHashCode(uriTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseAsNonTemplatedUri(string? uriTemplate, [NotNullWhen(true)] out Uri? uri)
|
||||
{
|
||||
if (uriTemplate is null || uriTemplate.Contains('{'))
|
||||
{
|
||||
uri = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return Uri.TryCreate(uriTemplate, UriKind.Absolute, out uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue