Skip to content

Commit 246019f

Browse files
Merge branch 'progress-tokens' of https://github.com/Tyler-R-Kendrick/mcp-csharp-sdk into progress-tokens
2 parents bb54fb7 + e9faef7 commit 246019f

File tree

10 files changed

+117
-171
lines changed

10 files changed

+117
-171
lines changed
Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
using ModelContextProtocol.Protocol.Messages;
2-
using ModelContextProtocol.Protocol.Types;
1+
using ModelContextProtocol.Protocol.Types;
32

43
namespace ModelContextProtocol.Client;
54

65
/// <summary>
76
/// Represents an instance of an MCP client connecting to a specific server.
87
/// </summary>
9-
public interface IMcpClient : IAsyncDisposable
8+
public interface IMcpClient : IMcpEndpoint
109
{
1110
/// <summary>
1211
/// Gets the capabilities supported by the server.
@@ -24,40 +23,4 @@ public interface IMcpClient : IAsyncDisposable
2423
/// It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt.
2524
/// </summary>
2625
string? ServerInstructions { get; }
27-
28-
/// <summary>
29-
/// Adds a handler for server notifications of a specific method.
30-
/// </summary>
31-
/// <param name="method">The notification method to handle.</param>
32-
/// <param name="handler">The async handler function to process notifications.</param>
33-
/// <remarks>
34-
/// <para>
35-
/// Each method may have multiple handlers. Adding a handler for a method that already has one
36-
/// will not replace the existing handler.
37-
/// </para>
38-
/// <para>
39-
/// <see cref="NotificationMethods"> provides constants for common notification methods.</see>
40-
/// </para>
41-
/// </remarks>
42-
void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler);
43-
44-
/// <summary>
45-
/// Sends a generic JSON-RPC request to the server.
46-
/// </summary>
47-
/// <typeparam name="TResult">The expected response type.</typeparam>
48-
/// <param name="request">The JSON-RPC request to send.</param>
49-
/// <param name="cancellationToken">A token to cancel the operation.</param>
50-
/// <returns>A task containing the server's response.</returns>
51-
/// <remarks>
52-
/// It is recommended to use the capability-specific methods that use this one in their implementation.
53-
/// Use this method for custom requests or those not yet covered explicitly.
54-
/// </remarks>
55-
Task<TResult> SendRequestAsync<TResult>(JsonRpcRequest request, CancellationToken cancellationToken = default) where TResult : class;
56-
57-
/// <summary>
58-
/// Sends a message to the server.
59-
/// </summary>
60-
/// <param name="message">The message.</param>
61-
/// <param name="cancellationToken">A token to cancel the operation.</param>
62-
Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
6326
}

src/ModelContextProtocol/Client/McpClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public McpClient(IClientTransport clientTransport, McpClientOptions options, Mcp
4646
{
4747
var progressToken = request?.Meta?.ProgressToken;
4848
return samplingHandler(
49-
request, progressToken is not null
49+
request, progressToken is { } token
5050
? new ClientTokenProgress(this, progressToken.Value)
5151
: NullProgress.Instance, ct);
5252
});
@@ -61,7 +61,7 @@ public McpClient(IClientTransport clientTransport, McpClientOptions options, Mcp
6161

6262
SetRequestHandler<ListRootsRequestParams, ListRootsResult>(
6363
RequestMethods.RootsList,
64-
(request, ct) => rootsHandler(request, ct));
64+
(request, cancellationToken) => rootsHandler(request, cancellationToken));
6565
}
6666
}
6767

src/ModelContextProtocol/Client/McpClientExtensions.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88

99
namespace ModelContextProtocol.Client;
1010

11-
/// <summary>
12-
/// Provides extensions for operating on MCP clients.
13-
/// </summary>
11+
/// <summary>Provides extension methods for interacting with an <see cref="IMcpClient"/>.</summary>
1412
public static class McpClientExtensions
1513
{
1614
/// <summary>
@@ -542,18 +540,17 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat
542540

543541
var (messages, options) = requestParams.ToChatClientArguments();
544542
var progressToken = requestParams.Meta?.ProgressToken;
545-
int progressValue = 0;
546-
var streamingResponses = chatClient.GetStreamingResponseAsync(
547-
messages, options, cancellationToken);
543+
548544
List<ChatResponseUpdate> updates = [];
549-
await foreach (var streamingResponse in streamingResponses)
545+
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken))
550546
{
551-
updates.Add(streamingResponse);
547+
updates.Add(update);
548+
552549
if (progressToken is not null)
553550
{
554551
progress.Report(new()
555552
{
556-
Progress = ++progressValue,
553+
Progress = updates.Count,
557554
});
558555
}
559556
}

src/ModelContextProtocol/ClientTokenProgress.cs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,15 @@
33

44
namespace ModelContextProtocol;
55

6-
internal sealed class ClientTokenProgress(IMcpClient client, ProgressToken progressToken)
7-
: IProgress<ProgressNotificationValue>
6+
/// <summary>
7+
/// Provides an <see cref="IProgress{ProgressNotificationValue}"/> tied to a specific progress token and that will issue
8+
/// progress notifications on the supplied endpoint.
9+
/// </summary>
10+
internal sealed class TokenProgress(IMcpEndpoint endpoint, ProgressToken progressToken) : IProgress<ProgressNotificationValue>
811
{
912
/// <inheritdoc />
1013
public void Report(ProgressNotificationValue value)
1114
{
12-
_ = client.SendMessageAsync(new JsonRpcNotification()
13-
{
14-
Method = NotificationMethods.ProgressNotification,
15-
Params = new ProgressNotification()
16-
{
17-
ProgressToken = progressToken,
18-
Progress = new()
19-
{
20-
Progress = value.Progress,
21-
Total = value.Total,
22-
Message = value.Message,
23-
},
24-
},
25-
}, CancellationToken.None);
15+
_ = endpoint.NotifyProgressAsync(progressToken, value, CancellationToken.None);
2616
}
2717
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using ModelContextProtocol.Protocol.Messages;
2+
3+
namespace ModelContextProtocol;
4+
5+
/// <summary>Represents a client or server MCP endpoint.</summary>
6+
public interface IMcpEndpoint : IAsyncDisposable
7+
{
8+
/// <summary>Sends a generic JSON-RPC request to the connected endpoint.</summary>
9+
/// <typeparam name="TResult">The expected response type.</typeparam>
10+
/// <param name="request">The JSON-RPC request to send.</param>
11+
/// <param name="cancellationToken">A token to cancel the operation.</param>
12+
/// <returns>A task containing the client's response.</returns>
13+
Task<TResult> SendRequestAsync<TResult>(JsonRpcRequest request, CancellationToken cancellationToken = default) where TResult : class;
14+
15+
/// <summary>Sends a message to the connected endpoint.</summary>
16+
/// <param name="message">The message.</param>
17+
/// <param name="cancellationToken">A token to cancel the operation.</param>
18+
Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
19+
20+
/// <summary>
21+
/// Adds a handler for server notifications of a specific method.
22+
/// </summary>
23+
/// <param name="method">The notification method to handle.</param>
24+
/// <param name="handler">The async handler function to process notifications.</param>
25+
/// <remarks>
26+
/// <para>
27+
/// Each method may have multiple handlers. Adding a handler for a method that already has one
28+
/// will not replace the existing handler.
29+
/// </para>
30+
/// <para>
31+
/// <see cref="NotificationMethods"> provides constants for common notification methods.</see>
32+
/// </para>
33+
/// </remarks>
34+
void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler);
35+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using ModelContextProtocol.Protocol.Messages;
2+
using ModelContextProtocol.Utils;
3+
4+
namespace ModelContextProtocol;
5+
6+
/// <summary>Provides extension methods for interacting with an <see cref="IMcpEndpoint"/>.</summary>
7+
public static class McpEndpointExtensions
8+
{
9+
/// <summary>Notifies the connected endpoint of progress.</summary>
10+
/// <param name="endpoint">The endpoint issueing the notification.</param>
11+
/// <param name="progressToken">The <see cref="ProgressToken"/> identifying the operation.</param>
12+
/// <param name="progress">The progress update to send.</param>
13+
/// <param name="cancellationToken">A token to cancel the operation.</param>
14+
/// <returns>A task representing the completion of the operation.</returns>
15+
/// <exception cref="ArgumentNullException"><paramref name="endpoint"/> is <see langword="null"/>.</exception>
16+
public static Task NotifyProgressAsync(
17+
this IMcpEndpoint endpoint,
18+
ProgressToken progressToken,
19+
ProgressNotificationValue progress,
20+
CancellationToken cancellationToken = default)
21+
{
22+
Throw.IfNull(endpoint);
23+
24+
return endpoint.SendMessageAsync(new JsonRpcNotification()
25+
{
26+
Method = NotificationMethods.ProgressNotification,
27+
Params = new ProgressNotification()
28+
{
29+
ProgressToken = progressToken,
30+
Progress = progress,
31+
},
32+
}, cancellationToken);
33+
}
34+
}

src/ModelContextProtocol/Protocol/Types/Tool.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public JsonElement InputSchema
3838
{
3939
if (!McpJsonUtilities.IsValidMcpToolSchema(value))
4040
{
41-
throw new ArgumentException("The specified document is not a valid MPC tool JSON schema.", nameof(InputSchema));
41+
throw new ArgumentException("The specified document is not a valid MCP tool JSON schema.", nameof(InputSchema));
4242
}
4343

4444
_inputSchema = value;
Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
using ModelContextProtocol.Protocol.Messages;
2-
using ModelContextProtocol.Protocol.Types;
1+
using ModelContextProtocol.Protocol.Types;
32

43
namespace ModelContextProtocol.Server;
54

65
/// <summary>
76
/// Represents a server that can communicate with a client using the MCP protocol.
87
/// </summary>
9-
public interface IMcpServer : IAsyncDisposable
8+
public interface IMcpServer : IMcpEndpoint
109
{
1110
/// <summary>
1211
/// Gets the capabilities supported by the client.
@@ -26,42 +25,8 @@ public interface IMcpServer : IAsyncDisposable
2625
/// </summary>
2726
IServiceProvider? Services { get; }
2827

29-
/// <summary>
30-
/// Adds a handler for client notifications of a specific method.
31-
/// </summary>
32-
/// <param name="method">The notification method to handle.</param>
33-
/// <param name="handler">The async handler function to process notifications.</param>
34-
/// <remarks>
35-
/// <para>
36-
/// Each method may have multiple handlers. Adding a handler for a method that already has one
37-
/// will not replace the existing handler.
38-
/// </para>
39-
/// <para>
40-
/// <see cref="NotificationMethods"> provides constants for common notification methods.</see>
41-
/// </para>
42-
/// </remarks>
43-
void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler);
44-
4528
/// <summary>
4629
/// Runs the server, listening for and handling client requests.
4730
/// </summary>
4831
Task RunAsync(CancellationToken cancellationToken = default);
49-
50-
/// <summary>
51-
/// Sends a generic JSON-RPC request to the client.
52-
/// NB! This is a temporary method that is available to send not yet implemented feature messages.
53-
/// Once all MCP features are implemented this will be made private, as it is purely a convenience for those who wish to implement features ahead of the library.
54-
/// </summary>
55-
/// <typeparam name="TResult">The expected response type.</typeparam>
56-
/// <param name="request">The JSON-RPC request to send.</param>
57-
/// <param name="cancellationToken">A token to cancel the operation.</param>
58-
/// <returns>A task containing the client's response.</returns>
59-
Task<TResult> SendRequestAsync<TResult>(JsonRpcRequest request, CancellationToken cancellationToken = default) where TResult : class;
60-
61-
/// <summary>
62-
/// Sends a message to the client.
63-
/// </summary>
64-
/// <param name="message">The message.</param>
65-
/// <param name="cancellationToken">A token to cancel the operation.</param>
66-
Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
6732
}

src/ModelContextProtocol/Server/McpServerExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
namespace ModelContextProtocol.Server;
99

10-
/// <inheritdoc />
10+
/// <summary>Provides extension methods for interacting with an <see cref="IMcpServer"/>.</summary>
1111
public static class McpServerExtensions
1212
{
1313
/// <summary>

tests/ModelContextProtocol.Tests/Server/McpServerTests.cs

Lines changed: 28 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -639,81 +639,43 @@ public Task RunAsync(CancellationToken cancellationToken = default) =>
639639
[Fact]
640640
public async Task NotifyProgress_Should_Be_Handled()
641641
{
642-
var taskCompletionSource = new TaskCompletionSource();
643-
bool notificationHandled = false;
644-
await Notifications_Are_Handled(
645-
serverCapabilities: null,
646-
method: NotificationMethods.ProgressNotification,
647-
parameters: new ProgressNotification()
642+
await using TestServerTransport transport = new();
643+
var options = CreateOptions();
644+
645+
var notificationReceived = new TaskCompletionSource<JsonRpcNotification>();
646+
647+
var server = McpServerFactory.Create(transport, options, LoggerFactory, _serviceProvider);
648+
server.AddNotificationHandler(NotificationMethods.ProgressNotification, notification =>
649+
{
650+
notificationReceived.SetResult(notification);
651+
return Task.CompletedTask;
652+
});
653+
654+
Task serverTask = server.RunAsync(TestContext.Current.CancellationToken);
655+
656+
await transport.SendMessageAsync(new JsonRpcNotification
657+
{
658+
Method = NotificationMethods.ProgressNotification,
659+
Params = new ProgressNotification()
648660
{
649-
ProgressToken = new(),
661+
ProgressToken = new("abc"),
650662
Progress = new()
651663
{
652664
Progress = 50,
653665
Total = 100,
654666
Message = "Progress message",
655667
},
656668
},
657-
configureOptions: null,
658-
configureServer: server =>
659-
{
660-
server.AddNotificationHandler(NotificationMethods.ProgressNotification,
661-
(notification) =>
662-
{
663-
notificationHandled = true;
664-
var progress = (ProgressNotificationValue?)notification.Params;
665-
Assert.NotNull(progress);
666-
var progressValue = progress.Value;
667-
taskCompletionSource.SetResult();
668-
Assert.Equal(50, progressValue.Progress);
669-
Assert.Equal(100, progressValue.Total);
670-
Assert.Equal("Progress message", progressValue.Message);
671-
return Task.CompletedTask;
672-
});
673-
},
674-
assertResult: async response =>
675-
{
676-
//Note: awaiting here so handlers are guaranteed to be called first.
677-
await taskCompletionSource.Task.WaitAsync(TimeSpan.FromSeconds(1));
678-
Assert.True(notificationHandled);
679-
});
680-
}
681-
682-
private async Task Notifications_Are_Handled(
683-
ServerCapabilities? serverCapabilities,
684-
string method, object? parameters,
685-
Action<McpServerOptions>? configureOptions,
686-
Action<IMcpServer>? configureServer,
687-
Action<JsonRpcNotification> assertResult)
688-
{
689-
await using TestServerTransport transport = new();
690-
var options = CreateOptions(serverCapabilities);
691-
configureOptions?.Invoke(options);
692-
693-
await using var server = McpServerFactory.Create(
694-
transport, options, LoggerFactory, _serviceProvider);
695-
696-
configureServer?.Invoke(server);
697-
await server.RunAsync();
698-
699-
TaskCompletionSource<JsonRpcNotification> receivedMessage = new();
700-
701-
transport.OnMessageSent = (message) =>
702-
{
703-
Assert.NotNull(message);
704-
if (message is JsonRpcNotification notification && notification.Method == method)
705-
{
706-
assertResult(notification);
707-
receivedMessage.SetResult(notification);
708-
}
709-
};
669+
}, TestContext.Current.CancellationToken);
710670

711-
await transport.SendMessageAsync(new JsonRpcNotification
712-
{
713-
Method = method,
714-
Params = parameters,
715-
});
671+
var notification = await notificationReceived.Task;
672+
var progress = (ProgressNotification)notification.Params!;
673+
Assert.Equal("\"abc\"", progress.ProgressToken.ToString());
674+
Assert.Equal(50, progress.Progress.Progress);
675+
Assert.Equal(100, progress.Progress.Total);
676+
Assert.Equal("Progress message", progress.Progress.Message);
716677

717-
var response = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(1));
678+
await server.DisposeAsync();
679+
await serverTask;
718680
}
719681
}

0 commit comments

Comments
 (0)