Skip to content

Commit 7f5ec50

Browse files
committed
Improve progress reporting
- Enable injecting an `IProgress<>` into a tool call to report progress when the client has supplied a progress token - Add a custom type for ProgressToken and ProgressNotification to map to the schema - Simplify RequestId to match ProgressToken's shape - Serialize StdioClientTransport's SendMessageAsync implementation - Add strongly-typed names for all request methods - Change all request IDs created by McpJsonRpcEndpoint to include a guid for that endpoint
1 parent 2ab9db0 commit 7f5ec50

27 files changed

+631
-228
lines changed

src/ModelContextProtocol/Client/McpClient.cs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public McpClient(IClientTransport clientTransport, McpClientOptions options, Mcp
4242
}
4343

4444
SetRequestHandler<CreateMessageRequestParams, CreateMessageResult>(
45-
"sampling/createMessage",
45+
RequestMethods.SamplingCreateMessage,
4646
(request, ct) => samplingHandler(request, ct));
4747
}
4848

@@ -54,7 +54,7 @@ public McpClient(IClientTransport clientTransport, McpClientOptions options, Mcp
5454
}
5555

5656
SetRequestHandler<ListRootsRequestParams, ListRootsResult>(
57-
"roots/list",
57+
RequestMethods.RootsList,
5858
(request, ct) => rootsHandler(request, ct));
5959
}
6060
}
@@ -89,21 +89,21 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
8989
using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
9090
initializationCts.CancelAfter(_options.InitializationTimeout);
9191

92-
try
93-
{
94-
// Send initialize request
95-
var initializeResponse = await SendRequestAsync<InitializeResult>(
96-
new JsonRpcRequest
92+
try
93+
{
94+
// Send initialize request
95+
var initializeResponse = await SendRequestAsync<InitializeResult>(
96+
new JsonRpcRequest
97+
{
98+
Method = RequestMethods.Initialize,
99+
Params = new InitializeRequestParams()
97100
{
98-
Method = "initialize",
99-
Params = new InitializeRequestParams()
100-
{
101-
ProtocolVersion = _options.ProtocolVersion,
102-
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
103-
ClientInfo = _options.ClientInfo,
104-
}
105-
},
106-
initializationCts.Token).ConfigureAwait(false);
101+
ProtocolVersion = _options.ProtocolVersion,
102+
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
103+
ClientInfo = _options.ClientInfo
104+
}
105+
},
106+
initializationCts.Token).ConfigureAwait(false);
107107

108108
// Store server information
109109
_logger.ServerCapabilitiesReceived(EndpointName,
@@ -123,7 +123,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
123123

124124
// Send initialized notification
125125
await SendMessageAsync(
126-
new JsonRpcNotification { Method = "notifications/initialized" },
126+
new JsonRpcNotification { Method = NotificationMethods.InitializedNotification },
127127
initializationCts.Token).ConfigureAwait(false);
128128
}
129129
catch (OperationCanceledException) when (initializationCts.IsCancellationRequested)

src/ModelContextProtocol/Client/McpClientExtensions.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public static Task PingAsync(this IMcpClient client, CancellationToken cancellat
4141
Throw.IfNull(client);
4242

4343
return client.SendRequestAsync<dynamic>(
44-
CreateRequest("ping", null),
44+
CreateRequest(RequestMethods.Ping, null),
4545
cancellationToken);
4646
}
4747

@@ -61,7 +61,7 @@ public static async Task<IList<McpClientTool>> ListToolsAsync(
6161
do
6262
{
6363
var toolResults = await client.SendRequestAsync<ListToolsResult>(
64-
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
64+
CreateRequest(RequestMethods.ToolsList, CreateCursorDictionary(cursor)),
6565
cancellationToken).ConfigureAwait(false);
6666

6767
tools ??= new List<McpClientTool>(toolResults.Tools.Count);
@@ -96,7 +96,7 @@ public static async IAsyncEnumerable<McpClientTool> EnumerateToolsAsync(
9696
do
9797
{
9898
var toolResults = await client.SendRequestAsync<ListToolsResult>(
99-
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
99+
CreateRequest(RequestMethods.ToolsList, CreateCursorDictionary(cursor)),
100100
cancellationToken).ConfigureAwait(false);
101101

102102
foreach (var tool in toolResults.Tools)
@@ -126,7 +126,7 @@ public static async Task<IList<Prompt>> ListPromptsAsync(
126126
do
127127
{
128128
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
129-
CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
129+
CreateRequest(RequestMethods.PromptsList, CreateCursorDictionary(cursor)),
130130
cancellationToken).ConfigureAwait(false);
131131

132132
if (prompts is null)
@@ -164,7 +164,7 @@ public static async IAsyncEnumerable<Prompt> EnumeratePromptsAsync(
164164
do
165165
{
166166
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
167-
CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
167+
CreateRequest(RequestMethods.PromptsList, CreateCursorDictionary(cursor)),
168168
cancellationToken).ConfigureAwait(false);
169169

170170
foreach (var prompt in promptResults.Prompts)
@@ -192,7 +192,7 @@ public static Task<GetPromptResult> GetPromptAsync(
192192
Throw.IfNullOrWhiteSpace(name);
193193

194194
return client.SendRequestAsync<GetPromptResult>(
195-
CreateRequest("prompts/get", CreateParametersDictionary(name, arguments)),
195+
CreateRequest(RequestMethods.PromptsGet, CreateParametersDictionary(name, arguments)),
196196
cancellationToken);
197197
}
198198

@@ -213,7 +213,7 @@ public static async Task<IList<ResourceTemplate>> ListResourceTemplatesAsync(
213213
do
214214
{
215215
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>(
216-
CreateRequest("resources/templates/list", CreateCursorDictionary(cursor)),
216+
CreateRequest(RequestMethods.ResourcesTemplatesList, CreateCursorDictionary(cursor)),
217217
cancellationToken).ConfigureAwait(false);
218218

219219
if (templates is null)
@@ -251,7 +251,7 @@ public static async IAsyncEnumerable<ResourceTemplate> EnumerateResourceTemplate
251251
do
252252
{
253253
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>(
254-
CreateRequest("resources/templates/list", CreateCursorDictionary(cursor)),
254+
CreateRequest(RequestMethods.ResourcesTemplatesList, CreateCursorDictionary(cursor)),
255255
cancellationToken).ConfigureAwait(false);
256256

257257
foreach (var template in templateResults.ResourceTemplates)
@@ -281,7 +281,7 @@ public static async Task<IList<Resource>> ListResourcesAsync(
281281
do
282282
{
283283
var resourceResults = await client.SendRequestAsync<ListResourcesResult>(
284-
CreateRequest("resources/list", CreateCursorDictionary(cursor)),
284+
CreateRequest(RequestMethods.ResourcesList, CreateCursorDictionary(cursor)),
285285
cancellationToken).ConfigureAwait(false);
286286

287287
if (resources is null)
@@ -319,7 +319,7 @@ public static async IAsyncEnumerable<Resource> EnumerateResourcesAsync(
319319
do
320320
{
321321
var resourceResults = await client.SendRequestAsync<ListResourcesResult>(
322-
CreateRequest("resources/list", CreateCursorDictionary(cursor)),
322+
CreateRequest(RequestMethods.ResourcesList, CreateCursorDictionary(cursor)),
323323
cancellationToken).ConfigureAwait(false);
324324

325325
foreach (var resource in resourceResults.Resources)
@@ -345,7 +345,7 @@ public static Task<ReadResourceResult> ReadResourceAsync(
345345
Throw.IfNullOrWhiteSpace(uri);
346346

347347
return client.SendRequestAsync<ReadResourceResult>(
348-
CreateRequest("resources/read", new() { ["uri"] = uri }),
348+
CreateRequest(RequestMethods.ResourcesRead, new() { ["uri"] = uri }),
349349
cancellationToken);
350350
}
351351

@@ -369,7 +369,7 @@ public static Task<CompleteResult> GetCompletionAsync(this IMcpClient client, Re
369369
}
370370

371371
return client.SendRequestAsync<CompleteResult>(
372-
CreateRequest("completion/complete", new()
372+
CreateRequest(RequestMethods.CompletionComplete, new()
373373
{
374374
["ref"] = reference,
375375
["argument"] = new Argument { Name = argumentName, Value = argumentValue }
@@ -389,7 +389,7 @@ public static Task SubscribeToResourceAsync(this IMcpClient client, string uri,
389389
Throw.IfNullOrWhiteSpace(uri);
390390

391391
return client.SendRequestAsync<EmptyResult>(
392-
CreateRequest("resources/subscribe", new() { ["uri"] = uri }),
392+
CreateRequest(RequestMethods.ResourcesSubscribe, new() { ["uri"] = uri }),
393393
cancellationToken);
394394
}
395395

@@ -405,7 +405,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string u
405405
Throw.IfNullOrWhiteSpace(uri);
406406

407407
return client.SendRequestAsync<EmptyResult>(
408-
CreateRequest("resources/unsubscribe", new() { ["uri"] = uri }),
408+
CreateRequest(RequestMethods.ResourcesUnsubscribe, new() { ["uri"] = uri }),
409409
cancellationToken);
410410
}
411411

@@ -424,7 +424,7 @@ public static Task<CallToolResponse> CallToolAsync(
424424
Throw.IfNull(toolName);
425425

426426
return client.SendRequestAsync<CallToolResponse>(
427-
CreateRequest("tools/call", CreateParametersDictionary(toolName, arguments)),
427+
CreateRequest(RequestMethods.ToolsCall, CreateParametersDictionary(toolName, arguments)),
428428
cancellationToken);
429429
}
430430

@@ -570,7 +570,7 @@ public static Task SetLoggingLevel(this IMcpClient client, LoggingLevel level, C
570570
Throw.IfNull(client);
571571

572572
return client.SendRequestAsync<EmptyResult>(
573-
CreateRequest("logging/setLevel", new() { ["level"] = level }),
573+
CreateRequest(RequestMethods.LoggingSetLevel, new() { ["level"] = level }),
574574
cancellationToken);
575575
}
576576

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace ModelContextProtocol;
2+
3+
/// <summary>Provides an <see cref="IProgress{ProgressNotificationValue}"/> that's a nop.</summary>
4+
internal sealed class NullProgress : IProgress<ProgressNotificationValue>
5+
{
6+
public static NullProgress Instance { get; } = new();
7+
8+
/// <inheritdoc />
9+
public void Report(ProgressNotificationValue value)
10+
{
11+
}
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace ModelContextProtocol;
2+
3+
/// <summary>Provides a progress value that can be sent using <see cref="IProgress{ProgressNotificationValue}"/>.</summary>
4+
public record struct ProgressNotificationValue
5+
{
6+
/// <summary>Gets or sets the progress thus far.</summary>
7+
public required float Progress { get; init; }
8+
9+
/// <summary>Gets or sets the total number of items to process (or total progress required), if known.</summary>
10+
public float? Total { get; init; }
11+
12+
/// <summary>Gets or sets an optional message describing the current progress.</summary>
13+
public string? Message { get; init; }
14+
}

src/ModelContextProtocol/Protocol/Messages/NotificationMethods.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,27 @@ public static class NotificationMethods
3434
/// Sent by the server when a log message is generated.
3535
/// </summary>
3636
public const string LoggingMessageNotification = "notifications/message";
37+
38+
/// <summary>
39+
/// Sent from the client to the server after initialization has finished.
40+
/// </summary>
41+
public const string InitializedNotification = "notifications/initialized";
42+
43+
/// <summary>
44+
/// Sent to inform the receiver of a progress update for a long-running request.
45+
/// </summary>
46+
public const string ProgressNotification = "notifications/progress";
47+
48+
/// <summary>
49+
/// Sent by either side to indicate that it is cancelling a previously-issued request.
50+
/// </summary>
51+
/// <remarks>
52+
/// The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification
53+
/// MAY arrive after the request has already finished.
54+
///
55+
/// This notification indicates that the result will be unused, so any associated processing SHOULD cease.
56+
///
57+
/// A client MUST NOT attempt to cancel its `initialize` request.".
58+
/// </remarks>
59+
public const string CancelledNotification = "notifications/cancelled";
3760
}

src/ModelContextProtocol/Protocol/Messages/OperationNames.cs

Lines changed: 0 additions & 43 deletions
This file was deleted.

0 commit comments

Comments
 (0)