Add McpServer/ClientResource{Template} and friends (#391)
* Add McpServer/ClientResource{Template} and friends
This PR copies the same design used by tools and prompts to add McpServerResource, McpServerResourceAttribute, McpServerResourceTypeAttribute, McpClientResource, and then all of those for ResourceTemplate as well.
Unlike the others, [McpServerResource] is permitted not only on methods but also on properties. This convenience is afforded because such resources aren't parameterized, and thus can be easily modeled as properties, likely auto-props, e.g.
```C#
[McpServerResource]
public static BlobResourceContents MyAmazingData { get; } = Create();
```
Details about resources can be encoded into the attributes or provided via McpServerResource{Template}.Create methods. If not URI is explicitly defined, one is derived from the member name and arguments. For templates, this means manufacturing a URI template that includes variables for all non-DI parameters.
This also fixes a few issues discovered along the way:
- McpClientExtensions are updated to use `ValueTask<T>` instead of `Task<T>` where relevant.
- McpServerPrompt was missing handling for progress notifications; that's been rectified. It was also missing configuring the description on the ProtocolPrompt.
- Renamed IMcpServerPrimitive.Name to Id. For resources, names aren't unique.
- After going back and forth, updated McpServer to not fail for missing handlers and to instead register default handlers. It simplifies the implementation and makes it more consistent.
- Added a bunch of primitive types into the McpJsonUtilities JSON source generator context so that such primitives are usable with tools/prompts/resources without folks being required to add them into their own context for Native AOT. I think we should consider pushing this down to AIJsonUtilities, instead, and that will then flow up to McpJsonUtilities automatically.
* Address feedback
This commit is contained in:
parent
47de86ab91
commit
d86b3277d4
34 changed files with 4146 additions and 308 deletions
|
|
@ -5,7 +5,6 @@ using ModelContextProtocol.Protocol.Types;
|
|||
using ModelContextProtocol.Shared;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ public static class McpClientExtensions
|
|||
parameters: null,
|
||||
McpJsonUtilities.JsonContext.Default.Object!,
|
||||
McpJsonUtilities.JsonContext.Default.Object,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -92,7 +92,7 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async Task<IList<McpClientTool>> ListToolsAsync(
|
||||
public static async ValueTask<IList<McpClientTool>> ListToolsAsync(
|
||||
this IMcpClient client,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
|
|
@ -205,7 +205,7 @@ public static class McpClientExtensions
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async Task<IList<McpClientPrompt>> ListPromptsAsync(
|
||||
public static async ValueTask<IList<McpClientPrompt>> ListPromptsAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
|
@ -311,7 +311,7 @@ public static class McpClientExtensions
|
|||
/// </remarks>
|
||||
/// <exception cref="McpException">Thrown when the prompt does not exist, when required arguments are missing, or when the server encounters an error processing the prompt.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static Task<GetPromptResult> GetPromptAsync(
|
||||
public static ValueTask<GetPromptResult> GetPromptAsync(
|
||||
this IMcpClient client,
|
||||
string name,
|
||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||
|
|
@ -349,12 +349,12 @@ public static class McpClientExtensions
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async Task<IList<ResourceTemplate>> ListResourceTemplatesAsync(
|
||||
public static async ValueTask<IList<McpClientResourceTemplate>> ListResourceTemplatesAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
List<ResourceTemplate>? templates = null;
|
||||
List<McpClientResourceTemplate>? resourceTemplates = null;
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
|
|
@ -366,20 +366,17 @@ public static class McpClientExtensions
|
|||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (templates is null)
|
||||
resourceTemplates ??= new List<McpClientResourceTemplate>(templateResults.ResourceTemplates.Count);
|
||||
foreach (var template in templateResults.ResourceTemplates)
|
||||
{
|
||||
templates = templateResults.ResourceTemplates;
|
||||
}
|
||||
else
|
||||
{
|
||||
templates.AddRange(templateResults.ResourceTemplates);
|
||||
resourceTemplates.Add(new McpClientResourceTemplate(client, template));
|
||||
}
|
||||
|
||||
cursor = templateResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
|
||||
return templates;
|
||||
return resourceTemplates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -395,7 +392,7 @@ public static class McpClientExtensions
|
|||
/// with cursors if the server responds with templates split across multiple responses.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Every iteration through the returned <see cref="IAsyncEnumerable{ResourceTemplate}"/>
|
||||
/// Every iteration through the returned <see cref="IAsyncEnumerable{McpClientResourceTemplate}"/>
|
||||
/// will result in re-querying the server and yielding the sequence of available resource templates.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
|
|
@ -409,7 +406,7 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async IAsyncEnumerable<ResourceTemplate> EnumerateResourceTemplatesAsync(
|
||||
public static async IAsyncEnumerable<McpClientResourceTemplate> EnumerateResourceTemplatesAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
|
@ -424,9 +421,9 @@ public static class McpClientExtensions
|
|||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var template in templateResults.ResourceTemplates)
|
||||
foreach (var templateResult in templateResults.ResourceTemplates)
|
||||
{
|
||||
yield return template;
|
||||
yield return new McpClientResourceTemplate(client, templateResult);
|
||||
}
|
||||
|
||||
cursor = templateResults.NextCursor;
|
||||
|
|
@ -463,12 +460,12 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async Task<IList<Resource>> ListResourcesAsync(
|
||||
public static async ValueTask<IList<McpClientResource>> ListResourcesAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
List<Resource>? resources = null;
|
||||
List<McpClientResource>? resources = null;
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
|
|
@ -480,13 +477,10 @@ public static class McpClientExtensions
|
|||
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (resources is null)
|
||||
resources ??= new List<McpClientResource>(resourceResults.Resources.Count);
|
||||
foreach (var resource in resourceResults.Resources)
|
||||
{
|
||||
resources = resourceResults.Resources;
|
||||
}
|
||||
else
|
||||
{
|
||||
resources.AddRange(resourceResults.Resources);
|
||||
resources.Add(new McpClientResource(client, resource));
|
||||
}
|
||||
|
||||
cursor = resourceResults.NextCursor;
|
||||
|
|
@ -509,7 +503,7 @@ public static class McpClientExtensions
|
|||
/// with cursors if the server responds with resources split across multiple responses.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Every iteration through the returned <see cref="IAsyncEnumerable{Resource}"/>
|
||||
/// Every iteration through the returned <see cref="IAsyncEnumerable{McpClientResource}"/>
|
||||
/// will result in re-querying the server and yielding the sequence of available resources.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
|
|
@ -523,7 +517,7 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async IAsyncEnumerable<Resource> EnumerateResourcesAsync(
|
||||
public static async IAsyncEnumerable<McpClientResource> EnumerateResourcesAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
|
@ -540,7 +534,7 @@ public static class McpClientExtensions
|
|||
|
||||
foreach (var resource in resourceResults.Resources)
|
||||
{
|
||||
yield return resource;
|
||||
yield return new McpClientResource(client, resource);
|
||||
}
|
||||
|
||||
cursor = resourceResults.NextCursor;
|
||||
|
|
@ -557,7 +551,7 @@ public static class McpClientExtensions
|
|||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="uri"/> is empty or composed entirely of whitespace.</exception>
|
||||
public static Task<ReadResourceResult> ReadResourceAsync(
|
||||
public static ValueTask<ReadResourceResult> ReadResourceAsync(
|
||||
this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
|
@ -579,7 +573,7 @@ public static class McpClientExtensions
|
|||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
|
||||
public static Task<ReadResourceResult> ReadResourceAsync(
|
||||
public static ValueTask<ReadResourceResult> ReadResourceAsync(
|
||||
this IMcpClient client, Uri uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
|
@ -588,6 +582,31 @@ public static class McpClientExtensions
|
|||
return ReadResourceAsync(client, uri.ToString(), cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a resource from the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client instance used to communicate with the MCP server.</param>
|
||||
/// <param name="uriTemplate">The uri template of the resource.</param>
|
||||
/// <param name="arguments">Arguments to use to format <paramref name="uriTemplate"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uriTemplate"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="uriTemplate"/> is empty or composed entirely of whitespace.</exception>
|
||||
public static ValueTask<ReadResourceResult> ReadResourceAsync(
|
||||
this IMcpClient client, string uriTemplate, IReadOnlyDictionary<string, object?> arguments, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(uriTemplate);
|
||||
Throw.IfNull(arguments);
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.ResourcesRead,
|
||||
new() { Uri = UriTemplate.FormatUri(uriTemplate, arguments) },
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests completion suggestions for a prompt argument or resource reference.
|
||||
/// </summary>
|
||||
|
|
@ -617,7 +636,7 @@ public static class McpClientExtensions
|
|||
/// <exception cref="ArgumentNullException"><paramref name="argumentName"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="argumentName"/> is empty or composed entirely of whitespace.</exception>
|
||||
/// <exception cref="McpException">The server returned an error response.</exception>
|
||||
public static Task<CompleteResult> CompleteAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default)
|
||||
public static ValueTask<CompleteResult> CompleteAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNull(reference);
|
||||
|
|
@ -675,7 +694,7 @@ public static class McpClientExtensions
|
|||
new() { Uri = uri },
|
||||
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -744,7 +763,7 @@ public static class McpClientExtensions
|
|||
new() { Uri = uri },
|
||||
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -813,7 +832,7 @@ public static class McpClientExtensions
|
|||
/// });
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static Task<CallToolResponse> CallToolAsync(
|
||||
public static ValueTask<CallToolResponse> CallToolAsync(
|
||||
this IMcpClient client,
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||
|
|
@ -842,7 +861,7 @@ public static class McpClientExtensions
|
|||
McpJsonUtilities.JsonContext.Default.CallToolResponse,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
static async Task<CallToolResponse> SendRequestWithProgressAsync(
|
||||
static async ValueTask<CallToolResponse> SendRequestWithProgressAsync(
|
||||
IMcpClient client,
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, object?>? arguments,
|
||||
|
|
@ -1061,7 +1080,7 @@ public static class McpClientExtensions
|
|||
new() { Level = level },
|
||||
McpJsonUtilities.JsonContext.Default.SetLevelRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
64
src/ModelContextProtocol/Client/McpClientResource.cs
Normal file
64
src/ModelContextProtocol/Client/McpClientResource.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using ModelContextProtocol.Protocol.Types;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a named resource that can be retrieved from an MCP server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This class provides a client-side wrapper around a resource defined on an MCP server. It allows
|
||||
/// retrieving the resource's content by sending a request to the server with the resource's URI.
|
||||
/// Instances of this class are typically obtained by calling <see cref="McpClientExtensions.ListResourcesAsync"/>
|
||||
/// or <see cref="McpClientExtensions.EnumerateResourcesAsync"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class McpClientResource
|
||||
{
|
||||
private readonly IMcpClient _client;
|
||||
|
||||
internal McpClientResource(IMcpClient client, Resource resource)
|
||||
{
|
||||
_client = client;
|
||||
ProtocolResource = resource;
|
||||
}
|
||||
|
||||
/// <summary>Gets the underlying protocol <see cref="Resource"/> type for this instance.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property provides direct access to the underlying protocol representation of the resource,
|
||||
/// which can be useful for advanced scenarios or when implementing custom MCP client extensions.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For most common use cases, you can use the more convenient <see cref="Name"/> and
|
||||
/// <see cref="Description"/> properties instead of accessing the <see cref="ProtocolResource"/> directly.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public Resource ProtocolResource { get; }
|
||||
|
||||
/// <summary>Gets the URI of the resource.</summary>
|
||||
public string Uri => ProtocolResource.Uri;
|
||||
|
||||
/// <summary>Gets the name of the resource.</summary>
|
||||
public string Name => ProtocolResource.Name;
|
||||
|
||||
/// <summary>Gets a description of the resource.</summary>
|
||||
public string? Description => ProtocolResource.Description;
|
||||
|
||||
/// <summary>Gets a media (MIME) type of the resource.</summary>
|
||||
public string? MimeType => ProtocolResource.MimeType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets this resource's content by sending a request to the server.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{ReadResourceResult}"/> containing the resource's result with content and messages.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is a convenience method that internally calls <see cref="McpClientExtensions.ReadResourceAsync(IMcpClient, string, CancellationToken)"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask<ReadResourceResult> ReadAsync(
|
||||
CancellationToken cancellationToken = default) =>
|
||||
_client.ReadResourceAsync(Uri, cancellationToken);
|
||||
}
|
||||
64
src/ModelContextProtocol/Client/McpClientResourceTemplate.cs
Normal file
64
src/ModelContextProtocol/Client/McpClientResourceTemplate.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using ModelContextProtocol.Protocol.Types;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a named resource template that can be retrieved from an MCP server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This class provides a client-side wrapper around a resource template defined on an MCP server. It allows
|
||||
/// retrieving the resource template's content by sending a request to the server with the resource's URI.
|
||||
/// Instances of this class are typically obtained by calling <see cref="McpClientExtensions.ListResourceTemplatesAsync"/>
|
||||
/// or <see cref="McpClientExtensions.EnumerateResourceTemplatesAsync"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class McpClientResourceTemplate
|
||||
{
|
||||
private readonly IMcpClient _client;
|
||||
|
||||
internal McpClientResourceTemplate(IMcpClient client, ResourceTemplate resourceTemplate)
|
||||
{
|
||||
_client = client;
|
||||
ProtocolResourceTemplate = resourceTemplate;
|
||||
}
|
||||
|
||||
/// <summary>Gets the underlying protocol <see cref="ResourceTemplate"/> type for this instance.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property provides direct access to the underlying protocol representation of the resource template,
|
||||
/// which can be useful for advanced scenarios or when implementing custom MCP client extensions.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For most common use cases, you can use the more convenient <see cref="UriTemplate"/> and
|
||||
/// <see cref="Description"/> properties instead of accessing the <see cref="ProtocolResourceTemplate"/> directly.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ResourceTemplate ProtocolResourceTemplate { get; }
|
||||
|
||||
/// <summary>Gets the URI template of the resource template.</summary>
|
||||
public string UriTemplate => ProtocolResourceTemplate.UriTemplate;
|
||||
|
||||
/// <summary>Gets the name of the resource template.</summary>
|
||||
public string Name => ProtocolResourceTemplate.Name;
|
||||
|
||||
/// <summary>Gets a description of the resource template.</summary>
|
||||
public string? Description => ProtocolResourceTemplate.Description;
|
||||
|
||||
/// <summary>Gets a media (MIME) type of the resource template.</summary>
|
||||
public string? MimeType => ProtocolResourceTemplate.MimeType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets this resource template's content by formatting a URI from the template and supplied arguments
|
||||
/// and sending a request to the server.
|
||||
/// </summary>
|
||||
/// <param name="arguments">A dictionary of arguments to pass to the tool. Each key represents a parameter name,
|
||||
/// and its associated value represents the argument value.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{ReadResourceResult}"/> containing the resource template's result with content and messages.</returns>
|
||||
public ValueTask<ReadResourceResult> ReadAsync(
|
||||
IReadOnlyDictionary<string, object?> arguments,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
_client.ReadResourceAsync(UriTemplate, arguments, cancellationToken);
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ public sealed class McpClientTool : AIFunction
|
|||
/// });
|
||||
/// </code>
|
||||
/// </example>
|
||||
public Task<CallToolResponse> CallAsync(
|
||||
public ValueTask<CallToolResponse> CallAsync(
|
||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||
IProgress<ProgressNotificationValue>? progress = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
|
|
|
|||
|
|
@ -297,6 +297,140 @@ public static partial class McpServerBuilderExtensions
|
|||
}
|
||||
#endregion
|
||||
|
||||
#region WithResources
|
||||
private const string WithResourcesRequiresUnreferencedCodeMessage =
|
||||
$"The non-generic {nameof(WithResources)} and {nameof(WithResourcesFromAssembly)} methods require dynamic lookup of member metadata" +
|
||||
$"and may not work in Native AOT. Use the generic {nameof(WithResources)} method instead.";
|
||||
|
||||
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
|
||||
/// <typeparam name="TResourceType">The resource type.</typeparam>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// This method discovers all instance and static methods (public and non-public) on the specified <typeparamref name="TResourceType"/>
|
||||
/// type, where the members are attributed as <see cref="McpServerResourceAttribute"/>, and adds an <see cref="McpServerResource"/>
|
||||
/// instance for each. For instance members, an instance will be constructed for each invocation of the resource.
|
||||
/// </remarks>
|
||||
public static IMcpServerBuilder WithResources<[DynamicallyAccessedMembers(
|
||||
DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors)] TResourceType>(
|
||||
this IMcpServerBuilder builder)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
foreach (var resourceTemplateMethod in typeof(TResourceType).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
|
||||
{
|
||||
if (resourceTemplateMethod.GetCustomAttribute<McpServerResourceAttribute>() is not null)
|
||||
{
|
||||
builder.Services.AddSingleton((Func<IServiceProvider, McpServerResource>)(resourceTemplateMethod.IsStatic ?
|
||||
services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services }) :
|
||||
services => McpServerResource.Create(resourceTemplateMethod, typeof(TResourceType), new() { Services = services })));
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="resourcetemplates">The <see cref="McpServerResource"/> instances to add to the server.</param>
|
||||
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="resourcetemplates"/> is <see langword="null"/>.</exception>
|
||||
public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IEnumerable<McpServerResource> resourcetemplates)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(resourcetemplates);
|
||||
|
||||
foreach (var resourceTemplate in resourcetemplates)
|
||||
{
|
||||
if (resourceTemplate is not null)
|
||||
{
|
||||
builder.Services.AddSingleton(resourceTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="resourceTemplateTypes">Types with marked methods to add as resources to the server.</param>
|
||||
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="resourceTemplateTypes"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// This method discovers all instance and static methods (public and non-public) on the specified <paramref name="resourceTemplateTypes"/>
|
||||
/// types, where the methods are attributed as <see cref="McpServerResourceAttribute"/>, and adds an <see cref="McpServerResource"/>
|
||||
/// instance for each. For instance methods, an instance will be constructed for each invocation of the resource.
|
||||
/// </remarks>
|
||||
[RequiresUnreferencedCode(WithResourcesRequiresUnreferencedCodeMessage)]
|
||||
public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IEnumerable<Type> resourceTemplateTypes)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(resourceTemplateTypes);
|
||||
|
||||
foreach (var resourceTemplateType in resourceTemplateTypes)
|
||||
{
|
||||
if (resourceTemplateType is not null)
|
||||
{
|
||||
foreach (var resourceTemplateMethod in resourceTemplateType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
|
||||
{
|
||||
if (resourceTemplateMethod.GetCustomAttribute<McpServerResourceAttribute>() is not null)
|
||||
{
|
||||
builder.Services.AddSingleton((Func<IServiceProvider, McpServerResource>)(resourceTemplateMethod.IsStatic ?
|
||||
services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services }) :
|
||||
services => McpServerResource.Create(resourceTemplateMethod, resourceTemplateType, new() { Services = services })));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds types marked with the <see cref="McpServerResourceTypeAttribute"/> attribute from the given assembly as resources to the server.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <param name="resourceAssembly">The assembly to load the types from. If <see langword="null"/>, the calling assembly will be used.</param>
|
||||
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method scans the specified assembly (or the calling assembly if none is provided) for classes
|
||||
/// marked with the <see cref="McpServerResourceTypeAttribute"/>. It then discovers all members within those
|
||||
/// classes that are marked with the <see cref="McpServerResourceAttribute"/> and registers them as <see cref="McpServerResource"/>s
|
||||
/// in the <paramref name="builder"/>'s <see cref="IServiceCollection"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The method automatically handles both static and instance members. For instance members, a new instance
|
||||
/// of the containing class will be constructed for each invocation of the resource.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Resource templates registered through this method can be discovered by clients using the <c>list_resourcetemplates</c> request
|
||||
/// and invoked using the <c>read_resource</c> request.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Note that this method performs reflection at runtime and may not work in Native AOT scenarios. For
|
||||
/// Native AOT compatibility, consider using the generic <see cref="WithResources{TResourceType}"/> method instead.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RequiresUnreferencedCode(WithResourcesRequiresUnreferencedCodeMessage)]
|
||||
public static IMcpServerBuilder WithResourcesFromAssembly(this IMcpServerBuilder builder, Assembly? resourceAssembly = null)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
resourceAssembly ??= Assembly.GetCallingAssembly();
|
||||
|
||||
return builder.WithResources(
|
||||
from t in resourceAssembly.GetTypes()
|
||||
where t.GetCustomAttribute<McpServerResourceTypeAttribute>() is not null
|
||||
select t);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Handlers
|
||||
/// <summary>
|
||||
/// Configures a handler for listing resource templates available from the Model Context Protocol server.
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public static class McpEndpointExtensions
|
|||
/// <param name="serializerOptions">The options governing request serialization.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the deserialized result.</returns>
|
||||
public static Task<TResult> SendRequestAsync<TParameters, TResult>(
|
||||
public static ValueTask<TResult> SendRequestAsync<TParameters, TResult>(
|
||||
this IMcpEndpoint endpoint,
|
||||
string method,
|
||||
TParameters parameters,
|
||||
|
|
@ -66,7 +66,7 @@ public static class McpEndpointExtensions
|
|||
/// <param name="requestId">The request id for the request.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the deserialized result.</returns>
|
||||
internal static async Task<TResult> SendRequestAsync<TParameters, TResult>(
|
||||
internal static async ValueTask<TResult> SendRequestAsync<TParameters, TResult>(
|
||||
this IMcpEndpoint endpoint,
|
||||
string method,
|
||||
TParameters parameters,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Protocol.Messages;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Transport;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.ServerSentEvents;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Types;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Types;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Types;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
|
|
|||
|
|
@ -16,5 +16,5 @@ public class ReadResourceRequestParams : RequestParams
|
|||
/// The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it.
|
||||
/// </summary>
|
||||
[JsonPropertyName("uri")]
|
||||
public string? Uri { get; init; }
|
||||
public required string Uri { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ public abstract class ResourceContents
|
|||
[JsonPropertyName("mimeType")]
|
||||
public string? MimeType { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Provides a <see cref="JsonConverter"/> for <see cref="ResourceContents"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -67,4 +67,27 @@ public record ResourceTemplate
|
|||
/// </remarks>
|
||||
[JsonPropertyName("annotations")]
|
||||
public Annotations? Annotations { get; init; }
|
||||
|
||||
/// <summary>Gets whether <see cref="UriTemplate"/> contains any template expressions.</summary>
|
||||
[JsonIgnore]
|
||||
public bool IsTemplated => UriTemplate.Contains('{');
|
||||
|
||||
/// <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>
|
||||
public Resource? AsResource()
|
||||
{
|
||||
if (IsTemplated)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
Uri = UriTemplate,
|
||||
Name = Name,
|
||||
Description = Description,
|
||||
MimeType = MimeType,
|
||||
Annotations = Annotations,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -87,4 +87,22 @@ public class ResourcesCapability
|
|||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<UnsubscribeRequestParams>, CancellationToken, ValueTask<EmptyResult>>? UnsubscribeFromResourcesHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of resources served by the server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Resources specified via <see cref="ResourceCollection"/> augment the <see cref="ListResourcesHandler"/>, <see cref="ListResourceTemplatesHandler"/>
|
||||
/// and <see cref="ReadResourceHandler"/> handlers, if provided. Resources with template expressions in their URI templates are considered resource templates
|
||||
/// and are listed via ListResourceTemplate, whereas resources without template parameters are considered static resources and are listed with ListResources.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// ReadResource requests will first check the <see cref="ResourceCollection"/> for the exact resource being requested. If no match is found, they'll proceed to
|
||||
/// try to match the resource against each resource template in <see cref="ResourceCollection"/>. If no match is still found, the request will fall back to
|
||||
/// any handler registered for <see cref="ReadResourceHandler"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public McpServerPrimitiveCollection<McpServerResource>? ResourceCollection { get; set; }
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
|
@ -27,7 +27,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerPrompt Create(
|
||||
MethodInfo method,
|
||||
|
|
@ -44,7 +44,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerPrompt Create(
|
||||
MethodInfo method,
|
||||
|
|
@ -89,6 +89,27 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IProgress<ProgressNotificationValue>))
|
||||
{
|
||||
// Bind IProgress<ProgressNotificationValue> to the progress token in the request,
|
||||
// if there is one. If we can't get one, return a nop progress.
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
{
|
||||
var requestContent = GetRequestContext(args);
|
||||
if (requestContent?.Server is { } server &&
|
||||
requestContent?.Params?.Meta?.ProgressToken is { } progressToken)
|
||||
{
|
||||
return new TokenProgress(server, progressToken);
|
||||
}
|
||||
|
||||
return NullProgress.Instance;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
static RequestContext<GetPromptRequestParams>? GetRequestContext(AIFunctionArguments args)
|
||||
|
|
@ -138,13 +159,18 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
return new AIFunctionMcpServerPrompt(function, prompt);
|
||||
}
|
||||
|
||||
private static McpServerPromptCreateOptions? DeriveOptions(MethodInfo method, McpServerPromptCreateOptions? options)
|
||||
private static McpServerPromptCreateOptions DeriveOptions(MethodInfo method, McpServerPromptCreateOptions? options)
|
||||
{
|
||||
McpServerPromptCreateOptions newOptions = options?.Clone() ?? new();
|
||||
|
||||
if (method.GetCustomAttribute<McpServerPromptAttribute>() is { } attr)
|
||||
if (method.GetCustomAttribute<McpServerPromptAttribute>() is { } promptAttr)
|
||||
{
|
||||
newOptions.Name ??= attr.Name;
|
||||
newOptions.Name ??= promptAttr.Name;
|
||||
}
|
||||
|
||||
if (method.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)
|
||||
{
|
||||
newOptions.Description ??= descAttr.Description;
|
||||
}
|
||||
|
||||
return newOptions;
|
||||
|
|
@ -167,18 +193,6 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
public override Prompt ProtocolPrompt { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// This implementation invokes the underlying <see cref="AIFunction"/> with the request arguments, and processes
|
||||
/// the result to create a standardized <see cref="GetPromptResult"/>. The method supports various return types from
|
||||
/// the underlying function:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Direct <see cref="GetPromptResult"/> instances are returned as-is</description></item>
|
||||
/// <item><description>String values are converted to a single user message</description></item>
|
||||
/// <item><description>Single <see cref="PromptMessage"/> objects are wrapped in a result</description></item>
|
||||
/// <item><description>Collections of <see cref="PromptMessage"/> objects are combined in a result</description></item>
|
||||
/// <item><description><see cref="ChatMessage"/> objects are converted to prompt messages</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public override async ValueTask<GetPromptResult> GetAsync(
|
||||
RequestContext<GetPromptRequestParams> request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
|
|||
430
src/ModelContextProtocol/Server/AIFunctionMcpServerResource.cs
Normal file
430
src/ModelContextProtocol/Server/AIFunctionMcpServerResource.cs
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>Provides an <see cref="AIFunctionMcpServerResource"/> that's implemented via an <see cref="AIFunction"/>.</summary>
|
||||
internal sealed class AIFunctionMcpServerResource : McpServerResource
|
||||
{
|
||||
private readonly Regex? _uriParser;
|
||||
private readonly string[] _templateVariableNames = [];
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AIFunctionMcpServerResource"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerResource Create(
|
||||
Delegate method,
|
||||
McpServerResourceCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
options = DeriveOptions(method.Method, options);
|
||||
|
||||
return Create(method.Method, method.Target, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerResource"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerResource Create(
|
||||
MethodInfo method,
|
||||
object? target,
|
||||
McpServerResourceCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
options = DeriveOptions(method, options);
|
||||
|
||||
return Create(
|
||||
AIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, options)),
|
||||
options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerResource"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerResource Create(
|
||||
MethodInfo method,
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
|
||||
McpServerResourceCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
options = DeriveOptions(method, options);
|
||||
|
||||
return Create(
|
||||
AIFunctionFactory.Create(method, targetType, CreateAIFunctionFactoryOptions(method, options)),
|
||||
options);
|
||||
}
|
||||
|
||||
private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
|
||||
MethodInfo method, McpServerResourceCreateOptions? options) =>
|
||||
new()
|
||||
{
|
||||
Name = options?.Name ?? method.GetCustomAttribute<McpServerResourceAttribute>()?.Name,
|
||||
Description = options?.Description,
|
||||
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
|
||||
SerializerOptions = McpJsonUtilities.DefaultOptions,
|
||||
Services = options?.Services,
|
||||
ConfigureParameterBinding = pi =>
|
||||
{
|
||||
if (pi.ParameterType == typeof(RequestContext<ReadResourceRequestParams>))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) => GetRequestContext(args),
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IMcpServer))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) => GetRequestContext(args)?.Server,
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IProgress<ProgressNotificationValue>))
|
||||
{
|
||||
// Bind IProgress<ProgressNotificationValue> to the progress token in the request,
|
||||
// if there is one. If we can't get one, return a nop progress.
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
{
|
||||
var requestContent = GetRequestContext(args);
|
||||
if (requestContent?.Server is { } server &&
|
||||
requestContent?.Params?.Meta?.ProgressToken is { } progressToken)
|
||||
{
|
||||
return new TokenProgress(server, progressToken);
|
||||
}
|
||||
|
||||
return NullProgress.Instance;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// These parameters are the ones and only ones to include in the schema. The schema
|
||||
// won't be consumed by anyone other than this instance, which will use it to determine
|
||||
// which properties should show up in the URI template.
|
||||
if (pi.Name is not null && GetConverter(pi.ParameterType) is { } converter)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = false,
|
||||
BindParameter = (pi, args) =>
|
||||
{
|
||||
if (args.TryGetValue(pi.Name!, out var value))
|
||||
{
|
||||
return
|
||||
value is null || pi.ParameterType.IsInstanceOfType(value) ? value :
|
||||
value is string stringValue ? converter(stringValue) :
|
||||
throw new ArgumentException($"Parameter '{pi.Name}' is of type '{pi.ParameterType}', but value '{value}' is of type '{value.GetType()}'.");
|
||||
}
|
||||
|
||||
return
|
||||
pi.HasDefaultValue ? pi.DefaultValue :
|
||||
throw new ArgumentException($"Missing a value for the required parameter '{pi.Name}'.");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
static RequestContext<ReadResourceRequestParams>? GetRequestContext(AIFunctionArguments args)
|
||||
{
|
||||
if (args.Context?.TryGetValue(typeof(RequestContext<ReadResourceRequestParams>), out var rc) is true &&
|
||||
rc is RequestContext<ReadResourceRequestParams> requestContext)
|
||||
{
|
||||
return requestContext;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, Func<string, object?>> s_convertersCache = [];
|
||||
|
||||
private static Func<string, object?>? GetConverter(Type type)
|
||||
{
|
||||
Type key = type;
|
||||
|
||||
if (s_convertersCache.TryGetValue(key, out var converter))
|
||||
{
|
||||
return converter;
|
||||
}
|
||||
|
||||
if (Nullable.GetUnderlyingType(type) is { } underlyingType)
|
||||
{
|
||||
// We will have already screened out null values by the time the converter is used,
|
||||
// so we can parse just the underlying type.
|
||||
type = underlyingType;
|
||||
}
|
||||
|
||||
if (type == typeof(string) || type == typeof(object)) converter = static s => s;
|
||||
if (type == typeof(bool)) converter = static s => bool.Parse(s);
|
||||
if (type == typeof(char)) converter = static s => char.Parse(s);
|
||||
if (type == typeof(byte)) converter = static s => byte.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(sbyte)) converter = static s => sbyte.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(ushort)) converter = static s => ushort.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(short)) converter = static s => short.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(uint)) converter = static s => uint.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(int)) converter = static s => int.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(ulong)) converter = static s => ulong.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(long)) converter = static s => long.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(float)) converter = static s => float.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(double)) converter = static s => double.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(decimal)) converter = static s => decimal.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(TimeSpan)) converter = static s => TimeSpan.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(DateTime)) converter = static s => DateTime.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(DateTimeOffset)) converter = static s => DateTimeOffset.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(Uri)) converter = static s => new Uri(s, UriKind.RelativeOrAbsolute);
|
||||
if (type == typeof(Guid)) converter = static s => Guid.Parse(s);
|
||||
if (type == typeof(Version)) converter = static s => Version.Parse(s);
|
||||
#if NET
|
||||
if (type == typeof(Half)) converter = static s => Half.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(Int128)) converter = static s => Int128.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(UInt128)) converter = static s => UInt128.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(IntPtr)) converter = static s => IntPtr.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(UIntPtr)) converter = static s => UIntPtr.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(DateOnly)) converter = static s => DateOnly.Parse(s, CultureInfo.InvariantCulture);
|
||||
if (type == typeof(TimeOnly)) converter = static s => TimeOnly.Parse(s, CultureInfo.InvariantCulture);
|
||||
#endif
|
||||
if (type.IsEnum) converter = s => Enum.Parse(type, s);
|
||||
|
||||
if (type.GetCustomAttribute<TypeConverterAttribute>() is TypeConverterAttribute tca &&
|
||||
Type.GetType(tca.ConverterTypeName, throwOnError: false) is { } converterType &&
|
||||
Activator.CreateInstance(converterType) is TypeConverter typeConverter &&
|
||||
typeConverter.CanConvertFrom(typeof(string)))
|
||||
{
|
||||
converter = s => typeConverter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
|
||||
}
|
||||
|
||||
if (converter is not null)
|
||||
{
|
||||
s_convertersCache.TryAdd(key, converter);
|
||||
}
|
||||
|
||||
return converter;
|
||||
}
|
||||
|
||||
/// <summary>Creates an <see cref="McpServerResource"/> that wraps the specified <see cref="AIFunction"/>.</summary>
|
||||
public static new AIFunctionMcpServerResource Create(AIFunction function, McpServerResourceCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(function);
|
||||
|
||||
string name = options?.Name ?? function.Name;
|
||||
|
||||
ResourceTemplate resource = new()
|
||||
{
|
||||
UriTemplate = options?.UriTemplate ?? DeriveUriTemplate(name, function),
|
||||
Name = name,
|
||||
Description = options?.Description,
|
||||
MimeType = options?.MimeType,
|
||||
};
|
||||
|
||||
return new AIFunctionMcpServerResource(function, resource);
|
||||
}
|
||||
|
||||
private static McpServerResourceCreateOptions DeriveOptions(MemberInfo member, McpServerResourceCreateOptions? options)
|
||||
{
|
||||
McpServerResourceCreateOptions newOptions = options?.Clone() ?? new();
|
||||
|
||||
if (member.GetCustomAttribute<McpServerResourceAttribute>() is { } resourceAttr)
|
||||
{
|
||||
newOptions.UriTemplate ??= resourceAttr.UriTemplate;
|
||||
newOptions.Name ??= resourceAttr.Name;
|
||||
newOptions.MimeType ??= resourceAttr.MimeType;
|
||||
}
|
||||
|
||||
if (member.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)
|
||||
{
|
||||
newOptions.Description ??= descAttr.Description;
|
||||
}
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
/// <summary>Derives a name to be used as a resource name.</summary>
|
||||
private static string DeriveUriTemplate(string name, AIFunction function)
|
||||
{
|
||||
StringBuilder template = new();
|
||||
|
||||
template.Append("resource://").Append(Uri.EscapeDataString(name));
|
||||
|
||||
if (function.JsonSchema.TryGetProperty("properties", out JsonElement properties))
|
||||
{
|
||||
string separator = "{?";
|
||||
foreach (var prop in properties.EnumerateObject())
|
||||
{
|
||||
template.Append(separator).Append(prop.Name);
|
||||
separator = ",";
|
||||
}
|
||||
|
||||
if (separator == ",")
|
||||
{
|
||||
template.Append('}');
|
||||
}
|
||||
}
|
||||
|
||||
return template.ToString();
|
||||
}
|
||||
|
||||
/// <summary>Gets the <see cref="AIFunction"/> wrapped by this resource.</summary>
|
||||
internal AIFunction AIFunction { get; }
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="McpServerResource"/> class.</summary>
|
||||
private AIFunctionMcpServerResource(AIFunction function, ResourceTemplate resourceTemplate)
|
||||
{
|
||||
AIFunction = function;
|
||||
ProtocolResourceTemplate = resourceTemplate;
|
||||
ProtocolResource = resourceTemplate.AsResource();
|
||||
|
||||
if (ProtocolResource is null)
|
||||
{
|
||||
_uriParser = UriTemplate.CreateParser(resourceTemplate.UriTemplate);
|
||||
_templateVariableNames = _uriParser.GetGroupNames().Where(n => n != "0").ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => AIFunction.ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ResourceTemplate ProtocolResourceTemplate { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Resource? ProtocolResource { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<ReadResourceResult?> ReadAsync(
|
||||
RequestContext<ReadResourceRequestParams> request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(request);
|
||||
Throw.IfNull(request.Params);
|
||||
Throw.IfNull(request.Params.Uri);
|
||||
|
||||
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)
|
||||
{
|
||||
match = _uriParser.Match(request.Params.Uri);
|
||||
if (!match.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (request.Params.Uri != ProtocolResource!.Uri)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build up the arguments for the AIFunction call, including all of the name/value pairs from the URI.
|
||||
AIFunctionArguments arguments = new()
|
||||
{
|
||||
Services = request.Services,
|
||||
Context = new Dictionary<object, object?>() { [typeof(RequestContext<ReadResourceRequestParams>)] = request }
|
||||
};
|
||||
|
||||
// For templates, populate the arguments from the URI template.
|
||||
if (match is not null)
|
||||
{
|
||||
foreach (string varName in _templateVariableNames)
|
||||
{
|
||||
if (match.Groups[varName] is { Success: true } value)
|
||||
{
|
||||
arguments[varName] = Uri.UnescapeDataString(value.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke the function.
|
||||
object? result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// And process the result.
|
||||
return result switch
|
||||
{
|
||||
ReadResourceResult readResourceResult => readResourceResult,
|
||||
|
||||
ResourceContents content => new()
|
||||
{
|
||||
Contents = [content],
|
||||
},
|
||||
|
||||
TextContent tc => new()
|
||||
{
|
||||
Contents = [new TextResourceContents() { Uri = request.Params!.Uri, MimeType = ProtocolResourceTemplate.MimeType, Text = tc.Text }],
|
||||
},
|
||||
|
||||
DataContent dc => new()
|
||||
{
|
||||
Contents = [new BlobResourceContents() { Uri = request.Params!.Uri, MimeType = dc.MediaType, Blob = dc.GetBase64Data() }],
|
||||
},
|
||||
|
||||
string text => new()
|
||||
{
|
||||
Contents = [new TextResourceContents() { Uri = request.Params!.Uri, MimeType = ProtocolResourceTemplate.MimeType, Text = text }],
|
||||
},
|
||||
|
||||
IEnumerable<ResourceContents> contents => new()
|
||||
{
|
||||
Contents = contents.ToList(),
|
||||
},
|
||||
|
||||
IEnumerable<AIContent> aiContents => new()
|
||||
{
|
||||
Contents = aiContents.Select<AIContent, ResourceContents>(
|
||||
ac => ac switch
|
||||
{
|
||||
TextContent tc => new TextResourceContents()
|
||||
{
|
||||
Uri = request.Params!.Uri,
|
||||
MimeType = ProtocolResourceTemplate.MimeType,
|
||||
Text = tc.Text
|
||||
},
|
||||
|
||||
DataContent dc => new BlobResourceContents()
|
||||
{
|
||||
Uri = request.Params!.Uri,
|
||||
MimeType = dc.MediaType,
|
||||
Blob = dc.GetBase64Data()
|
||||
},
|
||||
|
||||
_ => throw new InvalidOperationException($"Unsupported AIContent type '{ac.GetType()}' returned from resource function."),
|
||||
}).ToList(),
|
||||
},
|
||||
|
||||
IEnumerable<string> strings => new()
|
||||
{
|
||||
Contents = strings.Select<string, ResourceContents>(text => new TextResourceContents()
|
||||
{
|
||||
Uri = request.Params!.Uri,
|
||||
MimeType = ProtocolResourceTemplate.MimeType,
|
||||
Text = text
|
||||
}).ToList(),
|
||||
},
|
||||
|
||||
null => throw new InvalidOperationException("Null result returned from resource function."),
|
||||
|
||||
_ => throw new InvalidOperationException($"Unsupported result type '{result.GetType()}' returned from resource function."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
|
@ -27,7 +27,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerTool Create(
|
||||
MethodInfo method,
|
||||
|
|
@ -44,7 +44,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="MethodInfo"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerTool Create(
|
||||
MethodInfo method,
|
||||
|
|
@ -160,36 +160,41 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
|
|||
return new AIFunctionMcpServerTool(function, tool);
|
||||
}
|
||||
|
||||
private static McpServerToolCreateOptions? DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options)
|
||||
private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options)
|
||||
{
|
||||
McpServerToolCreateOptions newOptions = options?.Clone() ?? new();
|
||||
|
||||
if (method.GetCustomAttribute<McpServerToolAttribute>() is { } attr)
|
||||
if (method.GetCustomAttribute<McpServerToolAttribute>() is { } toolAttr)
|
||||
{
|
||||
newOptions.Name ??= attr.Name;
|
||||
newOptions.Title ??= attr.Title;
|
||||
newOptions.Name ??= toolAttr.Name;
|
||||
newOptions.Title ??= toolAttr.Title;
|
||||
|
||||
if (attr._destructive is bool destructive)
|
||||
if (toolAttr._destructive is bool destructive)
|
||||
{
|
||||
newOptions.Destructive ??= destructive;
|
||||
}
|
||||
|
||||
if (attr._idempotent is bool idempotent)
|
||||
if (toolAttr._idempotent is bool idempotent)
|
||||
{
|
||||
newOptions.Idempotent ??= idempotent;
|
||||
}
|
||||
|
||||
if (attr._openWorld is bool openWorld)
|
||||
if (toolAttr._openWorld is bool openWorld)
|
||||
{
|
||||
newOptions.OpenWorld ??= openWorld;
|
||||
}
|
||||
|
||||
if (attr._readOnly is bool readOnly)
|
||||
if (toolAttr._readOnly is bool readOnly)
|
||||
{
|
||||
newOptions.ReadOnly ??= readOnly;
|
||||
}
|
||||
}
|
||||
|
||||
if (method.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)
|
||||
{
|
||||
newOptions.Description ??= descAttr.Description;
|
||||
}
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue