Skip to content

Commit 862492d

Browse files
Copilotstephentoub
andauthored
Add SubscribeToResourceAsync overload with handler delegate (#1069)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: stephentoub <[email protected]>
1 parent 1b52dda commit 862492d

File tree

2 files changed

+519
-0
lines changed

2 files changed

+519
-0
lines changed

src/ModelContextProtocol.Core/Client/McpClient.Methods.cs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,18 @@ public Task SubscribeToResourceAsync(string uri, RequestOptions? options = null,
561561
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
562562
/// <returns>The result of the request.</returns>
563563
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
564+
/// <remarks>
565+
/// <para>
566+
/// This method subscribes to resource update notifications but does not register a handler.
567+
/// To receive notifications, you must separately call <see cref="McpSession.RegisterNotificationHandler(string, Func{JsonRpcNotification, CancellationToken, ValueTask})"/>
568+
/// with <see cref="NotificationMethods.ResourceUpdatedNotification"/> and filter for the specific resource URI.
569+
/// To unsubscribe, call <see cref="UnsubscribeFromResourceAsync(UnsubscribeRequestParams, CancellationToken)"/> and dispose the handler registration.
570+
/// </para>
571+
/// <para>
572+
/// For a simpler API that handles both subscription and notification registration in a single call,
573+
/// use <see cref="SubscribeToResourceAsync(Uri, Func{ResourceUpdatedNotificationParams, CancellationToken, ValueTask}, RequestOptions?, CancellationToken)"/>.
574+
/// </para>
575+
/// </remarks>
564576
public Task SubscribeToResourceAsync(
565577
SubscribeRequestParams requestParams,
566578
CancellationToken cancellationToken = default)
@@ -575,6 +587,140 @@ public Task SubscribeToResourceAsync(
575587
cancellationToken: cancellationToken).AsTask();
576588
}
577589

590+
/// <summary>
591+
/// Subscribes to a resource on the server and registers a handler for notifications when it changes.
592+
/// </summary>
593+
/// <param name="uri">The URI of the resource to which to subscribe.</param>
594+
/// <param name="handler">The handler to invoke when the resource is updated. It receives <see cref="ResourceUpdatedNotificationParams"/> for the subscribed resource.</param>
595+
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
596+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
597+
/// <returns>
598+
/// A task that completes with an <see cref="IAsyncDisposable"/> that, when disposed, unsubscribes from the resource
599+
/// and removes the notification handler.
600+
/// </returns>
601+
/// <exception cref="ArgumentNullException"><paramref name="uri"/> or <paramref name="handler"/> is <see langword="null"/>.</exception>
602+
/// <remarks>
603+
/// <para>
604+
/// This method provides a convenient way to subscribe to resource updates and handle notifications in a single call.
605+
/// The returned <see cref="IAsyncDisposable"/> manages both the subscription and the notification handler registration.
606+
/// When disposed, it automatically unsubscribes from the resource and removes the handler.
607+
/// </para>
608+
/// <para>
609+
/// The handler will only be invoked for notifications related to the specified resource URI.
610+
/// Notifications for other resources are filtered out automatically.
611+
/// </para>
612+
/// </remarks>
613+
public Task<IAsyncDisposable> SubscribeToResourceAsync(
614+
Uri uri,
615+
Func<ResourceUpdatedNotificationParams, CancellationToken, ValueTask> handler,
616+
RequestOptions? options = null,
617+
CancellationToken cancellationToken = default)
618+
{
619+
Throw.IfNull(uri);
620+
621+
return SubscribeToResourceAsync(uri.AbsoluteUri, handler, options, cancellationToken);
622+
}
623+
624+
/// <summary>
625+
/// Subscribes to a resource on the server and registers a handler for notifications when it changes.
626+
/// </summary>
627+
/// <param name="uri">The URI of the resource to which to subscribe.</param>
628+
/// <param name="handler">The handler to invoke when the resource is updated. It receives <see cref="ResourceUpdatedNotificationParams"/> for the subscribed resource.</param>
629+
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
630+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
631+
/// <returns>
632+
/// A task that completes with an <see cref="IAsyncDisposable"/> that, when disposed, unsubscribes from the resource
633+
/// and removes the notification handler.
634+
/// </returns>
635+
/// <exception cref="ArgumentNullException"><paramref name="uri"/> or <paramref name="handler"/> is <see langword="null"/>.</exception>
636+
/// <exception cref="ArgumentException"><paramref name="uri"/> is empty or composed entirely of whitespace.</exception>
637+
/// <remarks>
638+
/// <para>
639+
/// This method provides a convenient way to subscribe to resource updates and handle notifications in a single call.
640+
/// The returned <see cref="IAsyncDisposable"/> manages both the subscription and the notification handler registration.
641+
/// When disposed, it automatically unsubscribes from the resource and removes the handler.
642+
/// </para>
643+
/// <para>
644+
/// The handler will only be invoked for notifications related to the specified resource URI.
645+
/// Notifications for other resources are filtered out automatically.
646+
/// </para>
647+
/// </remarks>
648+
public async Task<IAsyncDisposable> SubscribeToResourceAsync(
649+
string uri,
650+
Func<ResourceUpdatedNotificationParams, CancellationToken, ValueTask> handler,
651+
RequestOptions? options = null,
652+
CancellationToken cancellationToken = default)
653+
{
654+
Throw.IfNullOrWhiteSpace(uri);
655+
Throw.IfNull(handler);
656+
657+
// Register a notification handler that filters for this specific resource
658+
IAsyncDisposable handlerRegistration = RegisterNotificationHandler(
659+
NotificationMethods.ResourceUpdatedNotification,
660+
async (notification, ct) =>
661+
{
662+
if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ResourceUpdatedNotificationParams) is { } resourceUpdate &&
663+
UriTemplate.UriTemplateComparer.Instance.Equals(resourceUpdate.Uri, uri))
664+
{
665+
await handler(resourceUpdate, ct).ConfigureAwait(false);
666+
}
667+
});
668+
669+
try
670+
{
671+
// Subscribe to the resource
672+
await SubscribeToResourceAsync(uri, options, cancellationToken).ConfigureAwait(false);
673+
}
674+
catch
675+
{
676+
// If subscription fails, unregister the handler before propagating the exception
677+
await handlerRegistration.DisposeAsync().ConfigureAwait(false);
678+
throw;
679+
}
680+
681+
// Return a disposable that unsubscribes and removes the handler
682+
return new ResourceSubscription(this, uri, handlerRegistration, options);
683+
}
684+
685+
/// <summary>
686+
/// Manages a resource subscription, handling both unsubscription and handler disposal.
687+
/// </summary>
688+
private sealed class ResourceSubscription : IAsyncDisposable
689+
{
690+
private readonly McpClient _client;
691+
private readonly string _uri;
692+
private readonly IAsyncDisposable _handlerRegistration;
693+
private readonly RequestOptions? _options;
694+
private int _disposed;
695+
696+
public ResourceSubscription(McpClient client, string uri, IAsyncDisposable handlerRegistration, RequestOptions? options)
697+
{
698+
_client = client;
699+
_uri = uri;
700+
_handlerRegistration = handlerRegistration;
701+
_options = options;
702+
}
703+
704+
public async ValueTask DisposeAsync()
705+
{
706+
if (Interlocked.Exchange(ref _disposed, 1) != 0)
707+
{
708+
return;
709+
}
710+
711+
try
712+
{
713+
// Unsubscribe from the resource
714+
await _client.UnsubscribeFromResourceAsync(_uri, _options, CancellationToken.None).ConfigureAwait(false);
715+
}
716+
finally
717+
{
718+
// Dispose the notification handler registration
719+
await _handlerRegistration.DisposeAsync().ConfigureAwait(false);
720+
}
721+
}
722+
}
723+
578724
/// <summary>
579725
/// Unsubscribes from a resource on the server to stop receiving notifications about its changes.
580726
/// </summary>

0 commit comments

Comments
 (0)