@@ -15,7 +15,7 @@ namespace ModelContextProtocol.Shared;
1515/// This is especially true as a client represents a connection to one and only one server, and vice versa.
1616/// Any multi-client or multi-server functionality should be implemented at a higher level of abstraction.
1717/// </summary>
18- internal abstract class McpJsonRpcEndpoint : IMcpEndpoint , IAsyncDisposable
18+ public abstract class McpJsonRpcEndpoint : IMcpEndpoint , IAsyncDisposable
1919{
2020 private readonly RequestHandlers _requestHandlers = [ ] ;
2121 private readonly NotificationHandlers _notificationHandlers = [ ] ;
@@ -27,6 +27,9 @@ internal abstract class McpJsonRpcEndpoint : IMcpEndpoint, IAsyncDisposable
2727 private readonly SemaphoreSlim _disposeLock = new ( 1 , 1 ) ;
2828 private bool _disposed ;
2929
30+ /// <summary>
31+ /// The logger for this endpoint.
32+ /// </summary>
3033 protected readonly ILogger _logger ;
3134
3235 /// <summary>
@@ -38,18 +41,53 @@ protected McpJsonRpcEndpoint(ILoggerFactory? loggerFactory = null)
3841 _logger = loggerFactory ? . CreateLogger ( GetType ( ) ) ?? NullLogger . Instance ;
3942 }
4043
44+ /// <summary>
45+ /// Sets the request handler for a specific method.
46+ /// </summary>
47+ /// <typeparam name="TRequest">The MCP Request type</typeparam>
48+ /// <typeparam name="TResponse">The MCP Response type</typeparam>
49+ /// <param name="method">The method name.</param>
50+ /// <param name="handler">The handler function.</param>
4151 protected void SetRequestHandler < TRequest , TResponse > ( string method , Func < TRequest ? , CancellationToken , Task < TResponse > > handler )
4252 => _requestHandlers . Set ( method , handler ) ;
4353
54+ /// <summary>
55+ /// Sets the request handler for a specific method.
56+ /// </summary>
57+ /// <param name="method">The method name.</param>
58+ /// <param name="handler">The handler function.</param>
4459 public void AddNotificationHandler ( string method , Func < JsonRpcNotification , Task > handler )
4560 => _notificationHandlers . Add ( method , handler ) ;
4661
62+ /// <summary>
63+ /// Sends a request over the protocol
64+ /// </summary>
65+ /// <typeparam name="TResult">The MCP Response type.</typeparam>
66+ /// <param name="request">The request instance</param>
67+ /// <param name="cancellationToken">The token for cancellation.</param>
68+ /// <returns>The MCP response.</returns>
4769 public async Task < TResult > SendRequestAsync < TResult > ( JsonRpcRequest request , CancellationToken cancellationToken = default ) where TResult : class
4870 {
49- using var registration = cancellationToken . Register ( ( ) => _ = this . NotifyCancelAsync ( request . Id ) ) ;
71+ using var registration = cancellationToken . Register ( async ( ) =>
72+ {
73+ try
74+ {
75+ await this . NotifyCancelAsync ( request . Id ) . ConfigureAwait ( false ) ;
76+ }
77+ catch ( Exception ex )
78+ {
79+ _logger . LogError ( ex , "An error occurred while notifying cancellation for request {RequestId}." , request . Id ) ;
80+ }
81+ } ) ;
5082 return await GetSessionOrThrow ( ) . SendRequestAsync < TResult > ( request , cancellationToken ) ;
5183 }
5284
85+ /// <summary>
86+ /// Sends a notification over the protocol.
87+ /// </summary>
88+ /// <param name="message">The message to send.</param>
89+ /// <param name="cancellationToken">The token for cancellation.</param>
90+ /// <returns>A task representing the completion of the operation.</returns>
5391 public Task SendMessageAsync ( IJsonRpcMessage message , CancellationToken cancellationToken = default )
5492 => GetSessionOrThrow ( ) . SendMessageAsync ( message , cancellationToken ) ;
5593
@@ -63,6 +101,12 @@ public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancella
63101 /// </summary>
64102 protected Task ? MessageProcessingTask { get ; set ; }
65103
104+ /// <summary>
105+ /// Starts the session with the given transport.
106+ /// </summary>
107+ /// <param name="sessionTransport">The transport to use for the session.</param>
108+ /// <param name="fullSessionCancellationToken">A cancellation token for the full session.</param>
109+ /// <exception cref="InvalidOperationException">Thrown if the session has already started.</exception>
66110 [ MemberNotNull ( nameof ( MessageProcessingTask ) ) ]
67111 protected void StartSession ( ITransport sessionTransport , CancellationToken fullSessionCancellationToken = default )
68112 {
@@ -76,6 +120,10 @@ protected void StartSession(ITransport sessionTransport, CancellationToken fullS
76120 MessageProcessingTask = _session . ProcessMessagesAsync ( _sessionCts . Token ) ;
77121 }
78122
123+ /// <summary>
124+ /// Disposes the endpoint and releases resources.
125+ /// </summary>
126+ /// <returns>A task representing the completion of the operation.</returns>
79127 public async ValueTask DisposeAsync ( )
80128 {
81129 using var _ = await _disposeLock . LockAsync ( ) . ConfigureAwait ( false ) ;
@@ -125,6 +173,11 @@ public virtual async ValueTask DisposeUnsynchronizedAsync()
125173 _logger . EndpointCleanedUp ( EndpointName ) ;
126174 }
127175
128- protected McpSession GetSessionOrThrow ( )
176+ /// <summary>
177+ /// Gets the current session.
178+ /// </summary>
179+ /// <returns>The current session.</returns>
180+ /// <exception cref="InvalidOperationException">Thrown if the session is not started.</exception>
181+ protected IMcpSession GetSessionOrThrow ( )
129182 => _session ?? throw new InvalidOperationException ( $ "This should be unreachable from public API! Call { nameof ( StartSession ) } before sending messages.") ;
130183}
0 commit comments