Skip to content

Add McpServer/ClientResource{Template} and friends #391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions THIRD-PARTY-NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,20 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

License notice for URI Template Tests
-------------------------------------

Copyright 2011- The Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/ModelContextProtocol/Client/McpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using ModelContextProtocol.Shared;
using ModelContextProtocol.Utils.Json;
using System.Text.Json;
using System.Threading;

namespace ModelContextProtocol.Client;

Expand Down
91 changes: 55 additions & 36 deletions src/ModelContextProtocol/Client/McpClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static Task PingAsync(this IMcpClient client, CancellationToken cancellat
parameters: null,
McpJsonUtilities.JsonContext.Default.Object!,
McpJsonUtilities.JsonContext.Default.Object,
cancellationToken: cancellationToken);
cancellationToken: cancellationToken).AsTask();
}

/// <summary>
Expand Down Expand Up @@ -92,7 +92,7 @@ public static Task PingAsync(this IMcpClient client, CancellationToken cancellat
/// </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)
Expand Down Expand Up @@ -205,7 +205,7 @@ public static async IAsyncEnumerable<McpClientTool> EnumerateToolsAsync(
/// </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);
Expand Down Expand Up @@ -311,7 +311,7 @@ public static async IAsyncEnumerable<McpClientPrompt> EnumeratePromptsAsync(
/// </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,
Expand Down Expand Up @@ -349,12 +349,12 @@ public static Task<GetPromptResult> GetPromptAsync(
/// </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
Expand All @@ -366,20 +366,17 @@ public static async Task<IList<ResourceTemplate>> ListResourceTemplatesAsync(
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);

if (templates is null)
{
templates = templateResults.ResourceTemplates;
}
else
resourceTemplates ??= new List<McpClientResourceTemplate>(templateResults.ResourceTemplates.Count);
foreach (var template in templateResults.ResourceTemplates)
{
templates.AddRange(templateResults.ResourceTemplates);
resourceTemplates.Add(new McpClientResourceTemplate(client, template));
}

cursor = templateResults.NextCursor;
}
while (cursor is not null);

return templates;
return resourceTemplates;
}

/// <summary>
Expand All @@ -395,7 +392,7 @@ public static async Task<IList<ResourceTemplate>> ListResourceTemplatesAsync(
/// 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>
Expand All @@ -409,7 +406,7 @@ public static async Task<IList<ResourceTemplate>> ListResourceTemplatesAsync(
/// </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);
Expand All @@ -424,9 +421,9 @@ public static async IAsyncEnumerable<ResourceTemplate> EnumerateResourceTemplate
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;
Expand Down Expand Up @@ -463,12 +460,12 @@ public static async IAsyncEnumerable<ResourceTemplate> EnumerateResourceTemplate
/// </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
Expand All @@ -480,13 +477,10 @@ public static async Task<IList<Resource>> ListResourcesAsync(
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);

if (resources is null)
{
resources = resourceResults.Resources;
}
else
resources ??= new List<McpClientResource>(resourceResults.Resources.Count);
foreach (var resource in resourceResults.Resources)
{
resources.AddRange(resourceResults.Resources);
resources.Add(new McpClientResource(client, resource));
}

cursor = resourceResults.NextCursor;
Expand All @@ -509,7 +503,7 @@ public static async Task<IList<Resource>> ListResourcesAsync(
/// 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>
Expand All @@ -523,7 +517,7 @@ public static async Task<IList<Resource>> ListResourcesAsync(
/// </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);
Expand All @@ -540,7 +534,7 @@ public static async IAsyncEnumerable<Resource> EnumerateResourcesAsync(

foreach (var resource in resourceResults.Resources)
{
yield return resource;
yield return new McpClientResource(client, resource);
}

cursor = resourceResults.NextCursor;
Expand All @@ -557,7 +551,7 @@ public static async IAsyncEnumerable<Resource> EnumerateResourcesAsync(
/// <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);
Expand All @@ -579,7 +573,7 @@ public static Task<ReadResourceResult> ReadResourceAsync(
/// <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);
Expand All @@ -588,6 +582,31 @@ public static Task<ReadResourceResult> ReadResourceAsync(
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>
Expand Down Expand Up @@ -617,7 +636,7 @@ public static Task<ReadResourceResult> ReadResourceAsync(
/// <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);
Expand Down Expand Up @@ -675,7 +694,7 @@ public static Task SubscribeToResourceAsync(this IMcpClient client, string uri,
new() { Uri = uri },
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
McpJsonUtilities.JsonContext.Default.EmptyResult,
cancellationToken: cancellationToken);
cancellationToken: cancellationToken).AsTask();
}

/// <summary>
Expand Down Expand Up @@ -744,7 +763,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string u
new() { Uri = uri },
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
McpJsonUtilities.JsonContext.Default.EmptyResult,
cancellationToken: cancellationToken);
cancellationToken: cancellationToken).AsTask();
}

/// <summary>
Expand Down Expand Up @@ -813,7 +832,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri,
/// });
/// </code>
/// </example>
public static Task<CallToolResponse> CallToolAsync(
public static ValueTask<CallToolResponse> CallToolAsync(
this IMcpClient client,
string toolName,
IReadOnlyDictionary<string, object?>? arguments = null,
Expand Down Expand Up @@ -842,7 +861,7 @@ public static Task<CallToolResponse> CallToolAsync(
McpJsonUtilities.JsonContext.Default.CallToolResponse,
cancellationToken: cancellationToken);

static async Task<CallToolResponse> SendRequestWithProgressAsync(
static async ValueTask<CallToolResponse> SendRequestWithProgressAsync(
IMcpClient client,
string toolName,
IReadOnlyDictionary<string, object?>? arguments,
Expand Down Expand Up @@ -1061,7 +1080,7 @@ public static Task SetLoggingLevel(this IMcpClient client, LoggingLevel level, C
new() { Level = level },
McpJsonUtilities.JsonContext.Default.SetLevelRequestParams,
McpJsonUtilities.JsonContext.Default.EmptyResult,
cancellationToken: cancellationToken);
cancellationToken: cancellationToken).AsTask();
}

/// <summary>
Expand Down
64 changes: 64 additions & 0 deletions src/ModelContextProtocol/Client/McpClientResource.cs
Original file line number Diff line number Diff line change
@@ -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);
}
Loading