Fix regression matching templated McpServerResources (#897)
This commit is contained in:
parent
8e607e6093
commit
1343dd3ebd
5 changed files with 107 additions and 46 deletions
|
|
@ -12,8 +12,8 @@ namespace ModelContextProtocol;
|
|||
///
|
||||
/// This exception type can be thrown by MCP tools or tool call filters to propagate detailed error messages
|
||||
/// from <see cref="Exception.Message"/> when a tool execution fails via a <see cref="CallToolResult"/>.
|
||||
/// For non-tool calls, this exception controls the message propogated via a <see cref="JsonRpcError"/>.
|
||||
///
|
||||
/// For non-tool calls, this exception controls the message propagated via a <see cref="JsonRpcError"/>.
|
||||
///
|
||||
/// <see cref="McpProtocolException"/> is a derived type that can be used to also specify the
|
||||
/// <see cref="McpErrorCode"/> that should be used for the resulting <see cref="JsonRpcError"/>.
|
||||
/// </remarks>
|
||||
|
|
|
|||
|
|
@ -314,7 +314,28 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
public override IReadOnlyList<object> Metadata => _metadata;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<ReadResourceResult?> ReadAsync(
|
||||
public override bool IsMatch(string uri)
|
||||
{
|
||||
Throw.IfNull(uri);
|
||||
return TryMatch(uri, out _);
|
||||
}
|
||||
|
||||
private bool TryMatch(string uri, out Match? match)
|
||||
{
|
||||
// For templates, use the Regex to parse. For static resources, we can just compare the URIs.
|
||||
if (_uriParser is null)
|
||||
{
|
||||
// This resource is not templated.
|
||||
match = null;
|
||||
return UriTemplate.UriTemplateComparer.Instance.Equals(uri, ProtocolResourceTemplate.UriTemplate);
|
||||
}
|
||||
|
||||
match = _uriParser.Match(uri);
|
||||
return match.Success;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<ReadResourceResult> ReadAsync(
|
||||
RequestContext<ReadResourceRequestParams> request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(request);
|
||||
|
|
@ -323,20 +344,9 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Check to see if this URI template matches the request URI. If it doesn't, return null.
|
||||
// For templates, use the Regex to parse. For static resources, we can just compare the URIs.
|
||||
Match? match = null;
|
||||
if (_uriParser is not null)
|
||||
if (!TryMatch(request.Params.Uri, out Match? match))
|
||||
{
|
||||
match = _uriParser.Match(request.Params.Uri);
|
||||
if (!match.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (!UriTemplate.UriTemplateComparer.Instance.Equals(request.Params.Uri, ProtocolResource!.Uri))
|
||||
{
|
||||
return null;
|
||||
throw new InvalidOperationException($"Resource '{ProtocolResourceTemplate.UriTemplate}' does not match the provided URI '{request.Params.Uri}'.");
|
||||
}
|
||||
|
||||
// Build up the arguments for the AIFunction call, including all of the name/value pairs from the URI.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ public abstract class DelegatingMcpServerResource : McpServerResource
|
|||
public override ResourceTemplate ProtocolResourceTemplate => _innerResource.ProtocolResourceTemplate;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueTask<ReadResourceResult?> ReadAsync(RequestContext<ReadResourceRequestParams> request, CancellationToken cancellationToken = default) =>
|
||||
public override bool IsMatch(string uri) => _innerResource.IsMatch(uri);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueTask<ReadResourceResult> ReadAsync(RequestContext<ReadResourceRequestParams> request, CancellationToken cancellationToken = default) =>
|
||||
_innerResource.ReadAsync(request, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -342,10 +342,7 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
{
|
||||
if (request.MatchedPrimitive is McpServerResource matchedResource)
|
||||
{
|
||||
if (await matchedResource.ReadAsync(request, cancellationToken).ConfigureAwait(false) is { } result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
return await matchedResource.ReadAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await originalReadResourceHandler(request, cancellationToken).ConfigureAwait(false);
|
||||
|
|
@ -366,22 +363,17 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
if (request.Params?.Uri is { } uri && resources is not null)
|
||||
{
|
||||
// First try an O(1) lookup by exact match.
|
||||
if (resources.TryGetPrimitive(uri, out var resource))
|
||||
if (resources.TryGetPrimitive(uri, out var resource) && !resource.IsTemplated)
|
||||
{
|
||||
request.MatchedPrimitive = resource;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fall back to an O(N) lookup, trying to match against each URI template.
|
||||
// The number of templates is controlled by the server developer, and the number is expected to be
|
||||
// not terribly large. If that changes, this can be tweaked to enable a more efficient lookup.
|
||||
foreach (var resourceTemplate in resources)
|
||||
{
|
||||
// Check if this template would handle the request by testing if ReadAsync would succeed
|
||||
if (resourceTemplate.IsTemplated)
|
||||
if (resourceTemplate.IsMatch(uri))
|
||||
{
|
||||
// This is a simplified check - a more robust implementation would match the URI pattern
|
||||
// For now, we'll let the actual handler attempt the match
|
||||
request.MatchedPrimitive = resourceTemplate;
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,6 +162,16 @@ public abstract class McpServerResource : IMcpServerPrimitive
|
|||
/// </remarks>
|
||||
public abstract IReadOnlyList<object> Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates whether the <paramref name="uri"/> matches the <see cref="ProtocolResourceTemplate"/>
|
||||
/// and can be used as the <see cref="ReadResourceRequestParams.Uri"/> passed to <see cref="ReadAsync"/>.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI being evaluated for this resource.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the <paramref name="uri"/> matches the <see cref="ProtocolResourceTemplate"/>; otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
public abstract bool IsMatch(string uri);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resource, rendering it with the provided request parameters and returning the resource result.
|
||||
/// </summary>
|
||||
|
|
@ -174,12 +184,14 @@ public abstract class McpServerResource : IMcpServerPrimitive
|
|||
/// </param>
|
||||
/// <returns>
|
||||
/// A <see cref="ValueTask{ReadResourceResult}"/> representing the asynchronous operation, containing a <see cref="ReadResourceResult"/> with
|
||||
/// the resource content and messages. If and only if this <see cref="McpServerResource"/> doesn't match the <see cref="ReadResourceRequestParams.Uri"/>,
|
||||
/// the method returns <see langword="null"/>.
|
||||
/// the resource content and messages.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="request"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The resource implementation returned <see langword="null"/> or an unsupported result type.</exception>
|
||||
public abstract ValueTask<ReadResourceResult?> ReadAsync(
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The <see cref="ReadResourceRequestParams.Uri"/> did not match the <see cref="ProtocolResourceTemplate"/> for this resource,
|
||||
/// the resource implementation returned <see langword="null"/>, or the resource implementation returned an unsupported result type.
|
||||
/// </exception>
|
||||
public abstract ValueTask<ReadResourceResult> ReadAsync(
|
||||
RequestContext<ReadResourceRequestParams> request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue