Allow notification handlers post-creation temporary registration (#223)

- We still allow notification handlers to be configured pre-creation, eliminating any race conditions that may result from missed notifications. But now we also allow for post-creation notification handler registration.
- Handlers may now also be removed. This permits temporary registrations that may be created in a scoped manner.
- Registration handling is thread-safe.
- Handlers are now cancelable.
- Exceptions from notification handlers trigger all normal exception handling in the dispatch pipeline.

This also:
- Adds a WithStreamServerTransport as a counterpart to WithStdioServerTransport. The latter was used in several tests in a hacky way, and that's now simplified via the former's existence.
This commit is contained in:
Stephen Toub 2025-04-06 21:17:00 -04:00 • committed by GitHub
commit 848c9df433
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -213,7 +213,7 @@ internal sealed class McpSession : IDisposable
break;
case JsonRpcNotification notification:
await HandleNotification(notification).ConfigureAwait(false);
await HandleNotification(notification, cancellationToken).ConfigureAwait(false);
break;
case IJsonRpcMessageWithId messageWithId:
@ -236,7 +236,7 @@ internal sealed class McpSession : IDisposable
}
}
private async Task HandleNotification(JsonRpcNotification notification)
private async Task HandleNotification(JsonRpcNotification notification, CancellationToken cancellationToken)
{
// Special-case cancellation to cancel a pending operation. (We'll still subsequently invoke a user-specified handler if one exists.)
if (notification.Method == NotificationMethods.CancelledNotification)
@ -257,21 +257,7 @@ internal sealed class McpSession : IDisposable
}
// Handle user-defined notifications.
if (_notificationHandlers.TryGetValue(notification.Method, out var handlers))
{
foreach (var notificationHandler in handlers)
{
try
{
await notificationHandler(notification).ConfigureAwait(false);
}
catch (Exception ex)
{
// Log handler error but continue processing
_logger.NotificationHandlerError(EndpointName, notification.Method, ex);
}
}
}
await _notificationHandlers.InvokeHandlers(notification.Method, notification, cancellationToken).ConfigureAwait(false);
}
private void HandleMessageWithId(IJsonRpcMessage message, IJsonRpcMessageWithId messageWithId)
@ -310,6 +296,14 @@ internal sealed class McpSession : IDisposable
}, cancellationToken).ConfigureAwait(false);
}
public IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, Task> handler)
{
Throw.IfNullOrWhiteSpace(method);
Throw.IfNull(handler);
return _notificationHandlers.Register(method, handler);
}
/// <summary>
/// Sends a JSON-RPC request to the server.
/// It is strongly recommended use the capability-specific methods instead of this one.
@ -525,6 +519,11 @@ internal sealed class McpSession : IDisposable
private static void AddExceptionTags(ref TagList tags, Exception e)
{
if (e is AggregateException ae && ae.InnerException is not null and not AggregateException)
{
e = ae.InnerException;
}
tags.Add("error.type", e.GetType().FullName);
tags.Add("rpc.jsonrpc.error_code",
(e as McpException)?.ErrorCode is int errorCode ? errorCode :