Improve progress reporting (#145)
* 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 * Address PR feedback
This commit is contained in:
parent
0294c574a6
commit
2c75acded8
27 changed files with 627 additions and 228 deletions
|
|
@ -42,7 +42,7 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
|
|||
}
|
||||
|
||||
SetRequestHandler<CreateMessageRequestParams, CreateMessageResult>(
|
||||
"sampling/createMessage",
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
(request, ct) => samplingHandler(request, ct));
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
|
|||
}
|
||||
|
||||
SetRequestHandler<ListRootsRequestParams, ListRootsResult>(
|
||||
"roots/list",
|
||||
RequestMethods.RootsList,
|
||||
(request, ct) => rootsHandler(request, ct));
|
||||
}
|
||||
}
|
||||
|
|
@ -89,21 +89,21 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
|
|||
using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
initializationCts.CancelAfter(_options.InitializationTimeout);
|
||||
|
||||
try
|
||||
{
|
||||
// Send initialize request
|
||||
var initializeResponse = await SendRequestAsync<InitializeResult>(
|
||||
new JsonRpcRequest
|
||||
try
|
||||
{
|
||||
// Send initialize request
|
||||
var initializeResponse = await SendRequestAsync<InitializeResult>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
Method = RequestMethods.Initialize,
|
||||
Params = new InitializeRequestParams()
|
||||
{
|
||||
Method = "initialize",
|
||||
Params = new InitializeRequestParams()
|
||||
{
|
||||
ProtocolVersion = _options.ProtocolVersion,
|
||||
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
|
||||
ClientInfo = _options.ClientInfo,
|
||||
}
|
||||
},
|
||||
initializationCts.Token).ConfigureAwait(false);
|
||||
ProtocolVersion = _options.ProtocolVersion,
|
||||
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
|
||||
ClientInfo = _options.ClientInfo
|
||||
}
|
||||
},
|
||||
initializationCts.Token).ConfigureAwait(false);
|
||||
|
||||
// Store server information
|
||||
_logger.ServerCapabilitiesReceived(EndpointName,
|
||||
|
|
@ -123,7 +123,7 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
|
|||
|
||||
// Send initialized notification
|
||||
await SendMessageAsync(
|
||||
new JsonRpcNotification { Method = "notifications/initialized" },
|
||||
new JsonRpcNotification { Method = NotificationMethods.InitializedNotification },
|
||||
initializationCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (initializationCts.IsCancellationRequested)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public static class McpClientExtensions
|
|||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<dynamic>(
|
||||
CreateRequest("ping", null),
|
||||
CreateRequest(RequestMethods.Ping, null),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ public static class McpClientExtensions
|
|||
do
|
||||
{
|
||||
var toolResults = await client.SendRequestAsync<ListToolsResult>(
|
||||
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
|
||||
CreateRequest(RequestMethods.ToolsList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
tools ??= new List<McpClientTool>(toolResults.Tools.Count);
|
||||
|
|
@ -96,7 +96,7 @@ public static class McpClientExtensions
|
|||
do
|
||||
{
|
||||
var toolResults = await client.SendRequestAsync<ListToolsResult>(
|
||||
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
|
||||
CreateRequest(RequestMethods.ToolsList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var tool in toolResults.Tools)
|
||||
|
|
@ -126,7 +126,7 @@ public static class McpClientExtensions
|
|||
do
|
||||
{
|
||||
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
|
||||
CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
|
||||
CreateRequest(RequestMethods.PromptsList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (prompts is null)
|
||||
|
|
@ -164,7 +164,7 @@ public static class McpClientExtensions
|
|||
do
|
||||
{
|
||||
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
|
||||
CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
|
||||
CreateRequest(RequestMethods.PromptsList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var prompt in promptResults.Prompts)
|
||||
|
|
@ -192,7 +192,7 @@ public static class McpClientExtensions
|
|||
Throw.IfNullOrWhiteSpace(name);
|
||||
|
||||
return client.SendRequestAsync<GetPromptResult>(
|
||||
CreateRequest("prompts/get", CreateParametersDictionary(name, arguments)),
|
||||
CreateRequest(RequestMethods.PromptsGet, CreateParametersDictionary(name, arguments)),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +213,7 @@ public static class McpClientExtensions
|
|||
do
|
||||
{
|
||||
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>(
|
||||
CreateRequest("resources/templates/list", CreateCursorDictionary(cursor)),
|
||||
CreateRequest(RequestMethods.ResourcesTemplatesList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (templates is null)
|
||||
|
|
@ -251,7 +251,7 @@ public static class McpClientExtensions
|
|||
do
|
||||
{
|
||||
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>(
|
||||
CreateRequest("resources/templates/list", CreateCursorDictionary(cursor)),
|
||||
CreateRequest(RequestMethods.ResourcesTemplatesList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var template in templateResults.ResourceTemplates)
|
||||
|
|
@ -281,7 +281,7 @@ public static class McpClientExtensions
|
|||
do
|
||||
{
|
||||
var resourceResults = await client.SendRequestAsync<ListResourcesResult>(
|
||||
CreateRequest("resources/list", CreateCursorDictionary(cursor)),
|
||||
CreateRequest(RequestMethods.ResourcesList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (resources is null)
|
||||
|
|
@ -319,7 +319,7 @@ public static class McpClientExtensions
|
|||
do
|
||||
{
|
||||
var resourceResults = await client.SendRequestAsync<ListResourcesResult>(
|
||||
CreateRequest("resources/list", CreateCursorDictionary(cursor)),
|
||||
CreateRequest(RequestMethods.ResourcesList, CreateCursorDictionary(cursor)),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var resource in resourceResults.Resources)
|
||||
|
|
@ -345,7 +345,7 @@ public static class McpClientExtensions
|
|||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return client.SendRequestAsync<ReadResourceResult>(
|
||||
CreateRequest("resources/read", new() { ["uri"] = uri }),
|
||||
CreateRequest(RequestMethods.ResourcesRead, new() { ["uri"] = uri }),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
|
@ -369,7 +369,7 @@ public static class McpClientExtensions
|
|||
}
|
||||
|
||||
return client.SendRequestAsync<CompleteResult>(
|
||||
CreateRequest("completion/complete", new()
|
||||
CreateRequest(RequestMethods.CompletionComplete, new()
|
||||
{
|
||||
["ref"] = reference,
|
||||
["argument"] = new Argument { Name = argumentName, Value = argumentValue }
|
||||
|
|
@ -389,7 +389,7 @@ public static class McpClientExtensions
|
|||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return client.SendRequestAsync<EmptyResult>(
|
||||
CreateRequest("resources/subscribe", new() { ["uri"] = uri }),
|
||||
CreateRequest(RequestMethods.ResourcesSubscribe, new() { ["uri"] = uri }),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
|
@ -405,7 +405,7 @@ public static class McpClientExtensions
|
|||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return client.SendRequestAsync<EmptyResult>(
|
||||
CreateRequest("resources/unsubscribe", new() { ["uri"] = uri }),
|
||||
CreateRequest(RequestMethods.ResourcesUnsubscribe, new() { ["uri"] = uri }),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
|
@ -424,7 +424,7 @@ public static class McpClientExtensions
|
|||
Throw.IfNull(toolName);
|
||||
|
||||
return client.SendRequestAsync<CallToolResponse>(
|
||||
CreateRequest("tools/call", CreateParametersDictionary(toolName, arguments)),
|
||||
CreateRequest(RequestMethods.ToolsCall, CreateParametersDictionary(toolName, arguments)),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
|
@ -560,7 +560,7 @@ public static class McpClientExtensions
|
|||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<EmptyResult>(
|
||||
CreateRequest("logging/setLevel", new() { ["level"] = level }),
|
||||
CreateRequest(RequestMethods.LoggingSetLevel, new() { ["level"] = level }),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
|
|
|||
12
src/ModelContextProtocol/NopProgress.cs
Normal file
12
src/ModelContextProtocol/NopProgress.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace ModelContextProtocol;
|
||||
|
||||
/// <summary>Provides an <see cref="IProgress{ProgressNotificationValue}"/> that's a nop.</summary>
|
||||
internal sealed class NullProgress : IProgress<ProgressNotificationValue>
|
||||
{
|
||||
public static NullProgress Instance { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Report(ProgressNotificationValue value)
|
||||
{
|
||||
}
|
||||
}
|
||||
14
src/ModelContextProtocol/ProgressNotificationValue.cs
Normal file
14
src/ModelContextProtocol/ProgressNotificationValue.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
namespace ModelContextProtocol;
|
||||
|
||||
/// <summary>Provides a progress value that can be sent using <see cref="IProgress{ProgressNotificationValue}"/>.</summary>
|
||||
public record struct ProgressNotificationValue
|
||||
{
|
||||
/// <summary>Gets or sets the progress thus far.</summary>
|
||||
public required float Progress { get; init; }
|
||||
|
||||
/// <summary>Gets or sets the total number of items to process (or total progress required), if known.</summary>
|
||||
public float? Total { get; init; }
|
||||
|
||||
/// <summary>Gets or sets an optional message describing the current progress.</summary>
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
|
|
@ -34,4 +34,27 @@ public static class NotificationMethods
|
|||
/// Sent by the server when a log message is generated.
|
||||
/// </summary>
|
||||
public const string LoggingMessageNotification = "notifications/message";
|
||||
|
||||
/// <summary>
|
||||
/// Sent from the client to the server after initialization has finished.
|
||||
/// </summary>
|
||||
public const string InitializedNotification = "notifications/initialized";
|
||||
|
||||
/// <summary>
|
||||
/// Sent to inform the receiver of a progress update for a long-running request.
|
||||
/// </summary>
|
||||
public const string ProgressNotification = "notifications/progress";
|
||||
|
||||
/// <summary>
|
||||
/// Sent by either side to indicate that it is cancelling a previously-issued request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification
|
||||
/// MAY arrive after the request has already finished.
|
||||
///
|
||||
/// This notification indicates that the result will be unused, so any associated processing SHOULD cease.
|
||||
///
|
||||
/// A client MUST NOT attempt to cancel its `initialize` request.".
|
||||
/// </remarks>
|
||||
public const string CancelledNotification = "notifications/cancelled";
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>Provides names of standard operations for use with registering handlers.</summary>
|
||||
/// <remarks>
|
||||
/// These values should not be inspected or relied on for their exact values.
|
||||
/// They serve only as opaque keys. They will be stable for the lifetime of a process
|
||||
/// but may change between versions of this library.
|
||||
/// </remarks>
|
||||
public static class OperationNames
|
||||
{
|
||||
/// <summary>Gets the name of the sampling operation.</summary>
|
||||
public static string Sampling { get; } = "operation/sampling";
|
||||
|
||||
/// <summary>Gets the name of the roots operation.</summary>
|
||||
public static string Roots { get; } = "operation/roots";
|
||||
|
||||
/// <summary>Gets the name of the list tools operation.</summary>
|
||||
public static string ListTools { get; } = "operation/listTools";
|
||||
|
||||
/// <summary>Gets the name of the call tool operation.</summary>
|
||||
public static string CallTool { get; } = "operation/callTool";
|
||||
|
||||
/// <summary>Gets the name of the list prompts operation.</summary>
|
||||
public static string ListPrompts { get; } = "operation/listPrompts";
|
||||
|
||||
/// <summary>Gets the name of the get prompt operation.</summary>
|
||||
public static string GetPrompt { get; } = "operation/getPrompt";
|
||||
|
||||
/// <summary>Gets the name of the list resources operation.</summary>
|
||||
public static string ListResources { get; } = "operation/listResources";
|
||||
|
||||
/// <summary>Gets the name of the read resource operation.</summary>
|
||||
public static string ReadResource { get; } = "operation/readResource";
|
||||
|
||||
/// <summary>Gets the name of the get completion operation.</summary>
|
||||
public static string GetCompletion { get; } = "operation/getCompletion";
|
||||
|
||||
/// <summary>Gets the name of the subscribe to resources operation.</summary>
|
||||
public static string SubscribeToResources { get; } = "operation/subscribeToResources";
|
||||
|
||||
/// <summary>Gets the name of the subscribe to resources operation.</summary>
|
||||
public static string UnsubscribeFromResources { get; } = "operation/unsubscribeFromResources";
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// An out-of-band notification used to inform the receiver of a progress update for a long-running request.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(Converter))]
|
||||
public class ProgressNotification
|
||||
{
|
||||
/// <summary>
|
||||
/// The progress token which was given in the initial request, used to associate this notification with the request that is proceeding.
|
||||
/// </summary>
|
||||
public required ProgressToken ProgressToken { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The progress thus far. This should increase every time progress is made, even if the total is unknown.
|
||||
/// </summary>
|
||||
public required ProgressNotificationValue Progress { get; init; }
|
||||
|
||||
/// <summary>Provides a <see cref="JsonConverter"/> for <see cref="ProgressNotification"/>.</summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class Converter : JsonConverter<ProgressNotification>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override ProgressNotification? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
ProgressToken? progressToken = null;
|
||||
float? progress = null;
|
||||
float? total = null;
|
||||
string? message = null;
|
||||
|
||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||
{
|
||||
var propertyName = reader.GetString();
|
||||
reader.Read();
|
||||
switch (propertyName)
|
||||
{
|
||||
case "progressToken":
|
||||
progressToken = (ProgressToken)JsonSerializer.Deserialize(ref reader, options.GetTypeInfo(typeof(ProgressToken)))!;
|
||||
break;
|
||||
|
||||
case "progress":
|
||||
progress = reader.GetSingle();
|
||||
break;
|
||||
|
||||
case "total":
|
||||
total = reader.GetSingle();
|
||||
break;
|
||||
|
||||
case "message":
|
||||
message = reader.GetString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (progress is null)
|
||||
{
|
||||
throw new JsonException("Missing required property 'progress'.");
|
||||
}
|
||||
|
||||
if (progressToken is null)
|
||||
{
|
||||
throw new JsonException("Missing required property 'progressToken'.");
|
||||
}
|
||||
|
||||
return new ProgressNotification
|
||||
{
|
||||
ProgressToken = progressToken.GetValueOrDefault(),
|
||||
Progress = new ProgressNotificationValue()
|
||||
{
|
||||
Progress = progress.GetValueOrDefault(),
|
||||
Total = total,
|
||||
Message = message
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, ProgressNotification value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
|
||||
writer.WritePropertyName("progressToken");
|
||||
JsonSerializer.Serialize(writer, value.ProgressToken, options.GetTypeInfo(typeof(ProgressToken)));
|
||||
|
||||
writer.WriteNumber("progress", value.Progress.Progress);
|
||||
|
||||
if (value.Progress.Total is { } total)
|
||||
{
|
||||
writer.WriteNumber("total", total);
|
||||
}
|
||||
|
||||
if (value.Progress.Message is { } message)
|
||||
{
|
||||
writer.WriteString("message", message);
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/ModelContextProtocol/Protocol/Messages/ProgressToken.cs
Normal file
102
src/ModelContextProtocol/Protocol/Messages/ProgressToken.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using ModelContextProtocol.Utils;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a progress token, which can be either a string or an integer.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(Converter))]
|
||||
public readonly struct ProgressToken : IEquatable<ProgressToken>
|
||||
{
|
||||
/// <summary>The id, either a string or a boxed long or null.</summary>
|
||||
private readonly object? _id;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="ProgressToken"/> with a specified value.</summary>
|
||||
/// <param name="value">The required ID value.</param>
|
||||
public ProgressToken(string value)
|
||||
{
|
||||
Throw.IfNull(value);
|
||||
_id = value;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="ProgressToken"/> with a specified value.</summary>
|
||||
/// <param name="value">The required ID value.</param>
|
||||
public ProgressToken(long value)
|
||||
{
|
||||
// Box the long. Progress tokens are almost always strings in practice, so this should be rare.
|
||||
_id = value;
|
||||
}
|
||||
|
||||
/// <summary>Gets whether the identifier is uninitialized.</summary>
|
||||
public bool IsDefault => _id is null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string? ToString() =>
|
||||
_id is string stringValue ? $"\"{stringValue}\"" :
|
||||
_id is long longValue ? longValue.ToString(CultureInfo.InvariantCulture) :
|
||||
null;
|
||||
|
||||
/// <summary>
|
||||
/// Compares this ProgressToken to another ProgressToken.
|
||||
/// </summary>
|
||||
public bool Equals(ProgressToken other) => Equals(_id, other._id);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj) => obj is ProgressToken other && Equals(other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() => _id?.GetHashCode() ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Compares two ProgressTokens for equality.
|
||||
/// </summary>
|
||||
public static bool operator ==(ProgressToken left, ProgressToken right) => left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Compares two ProgressTokens for inequality.
|
||||
/// </summary>
|
||||
public static bool operator !=(ProgressToken left, ProgressToken right) => !left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for ProgressToken that handles both string and number values.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class Converter : JsonConverter<ProgressToken>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override ProgressToken Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => new(reader.GetString()!),
|
||||
JsonTokenType.Number => new(reader.GetInt64()),
|
||||
_ => throw new JsonException("progressToken must be a string or an integer"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, ProgressToken value, JsonSerializerOptions options)
|
||||
{
|
||||
Throw.IfNull(writer);
|
||||
|
||||
switch (value._id)
|
||||
{
|
||||
case string str:
|
||||
writer.WriteStringValue(str);
|
||||
return;
|
||||
|
||||
case long longValue:
|
||||
writer.WriteNumberValue(longValue);
|
||||
return;
|
||||
|
||||
case null:
|
||||
writer.WriteStringValue(string.Empty);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +1,55 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using ModelContextProtocol.Utils;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a JSON-RPC request identifier which can be either a string or a number.
|
||||
/// Represents a JSON-RPC request identifier, which can be either a string or an integer.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(RequestIdConverter))]
|
||||
[JsonConverter(typeof(Converter))]
|
||||
public readonly struct RequestId : IEquatable<RequestId>
|
||||
{
|
||||
private readonly object _value;
|
||||
/// <summary>The id, either a string or a boxed long or null.</summary>
|
||||
private readonly object? _id;
|
||||
|
||||
private RequestId(object value)
|
||||
/// <summary>Initializes a new instance of the <see cref="RequestId"/> with a specified value.</summary>
|
||||
/// <param name="value">The required ID value.</param>
|
||||
public RequestId(string value)
|
||||
{
|
||||
_value = value;
|
||||
Throw.IfNull(value);
|
||||
_id = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RequestId from a string.
|
||||
/// </summary>
|
||||
/// <param name="value">The Id</param>
|
||||
/// <returns>Wrapped Id object</returns>
|
||||
public static RequestId FromString(string value) => new(value);
|
||||
/// <summary>Initializes a new instance of the <see cref="RequestId"/> with a specified value.</summary>
|
||||
/// <param name="value">The required ID value.</param>
|
||||
public RequestId(long value)
|
||||
{
|
||||
// Box the long. Request IDs are almost always strings in practice, so this should be rare.
|
||||
_id = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RequestId from a number.
|
||||
/// </summary>
|
||||
/// <param name="value">The Id</param>
|
||||
/// <returns>Wrapped Id object</returns>
|
||||
public static RequestId FromNumber(long value) => new(value);
|
||||
/// <summary>Gets whether the identifier is uninitialized.</summary>
|
||||
public bool IsDefault => _id is null;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the RequestId is a string.
|
||||
/// </summary>
|
||||
public bool IsString => _value is string;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the RequestId is a number.
|
||||
/// </summary>
|
||||
public bool IsNumber => _value is long;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the request id is valid (has a value)
|
||||
/// </summary>
|
||||
public bool IsValid => _value != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the RequestId as a string.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the RequestId is not a string</exception>"
|
||||
public string AsString => _value as string ?? throw new InvalidOperationException("RequestId is not a string");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the RequestId as a number.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the RequestId is not a number</exception>""
|
||||
public long AsNumber => _value is long number ? number : throw new InvalidOperationException("RequestId is not a number");
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of the RequestId. Will box the value if it is a number.
|
||||
/// </summary>
|
||||
public override string ToString() => _value.ToString() ?? "";
|
||||
/// <inheritdoc />
|
||||
public override string ToString() =>
|
||||
_id is string stringValue ? $"\"{stringValue}\"" :
|
||||
_id is long longValue ? longValue.ToString(CultureInfo.InvariantCulture) :
|
||||
string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Compares this RequestId to another RequestId.
|
||||
/// </summary>
|
||||
public bool Equals(RequestId other) => _value.Equals(other._value);
|
||||
public bool Equals(RequestId other) => Equals(_id, other._id);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj) => obj is RequestId other && Equals(other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() => _value.GetHashCode();
|
||||
public override int GetHashCode() => _id?.GetHashCode() ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Compares two RequestIds for equality.
|
||||
|
|
@ -81,4 +60,43 @@ public readonly struct RequestId : IEquatable<RequestId>
|
|||
/// Compares two RequestIds for inequality.
|
||||
/// </summary>
|
||||
public static bool operator !=(RequestId left, RequestId right) => !left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for RequestId that handles both string and number values.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class Converter: JsonConverter<RequestId>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override RequestId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => new(reader.GetString()!),
|
||||
JsonTokenType.Number => new(reader.GetInt64()),
|
||||
_ => throw new JsonException("requestId must be a string or an integer"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options)
|
||||
{
|
||||
Throw.IfNull(writer);
|
||||
|
||||
switch (value._id)
|
||||
{
|
||||
case string str:
|
||||
writer.WriteStringValue(str);
|
||||
return;
|
||||
|
||||
case long longValue:
|
||||
writer.WriteNumberValue(longValue);
|
||||
return;
|
||||
|
||||
case null:
|
||||
writer.WriteStringValue(string.Empty);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
using ModelContextProtocol.Utils;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for RequestId that handles both string and number values.
|
||||
/// </summary>
|
||||
public class RequestIdConverter : JsonConverter<RequestId>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override RequestId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => RequestId.FromString(reader.GetString()!),
|
||||
JsonTokenType.Number => RequestId.FromNumber(reader.GetInt64()),
|
||||
_ => throw new JsonException("RequestId must be either a string or a number"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options)
|
||||
{
|
||||
Throw.IfNull(writer);
|
||||
|
||||
if (value.IsString)
|
||||
{
|
||||
writer.WriteStringValue(value.AsString);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNumberValue(value.AsNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/ModelContextProtocol/Protocol/Messages/RequestMethods.cs
Normal file
82
src/ModelContextProtocol/Protocol/Messages/RequestMethods.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Provides names for request methods used in the Model Context Protocol (MCP).
|
||||
/// </summary>
|
||||
public static class RequestMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// Sent from the client to request a list of tools the server has.
|
||||
/// </summary>
|
||||
public const string ToolsList = "tools/list";
|
||||
|
||||
/// <summary>
|
||||
/// Used by the client to invoke a tool provided by the server.
|
||||
/// </summary>
|
||||
public const string ToolsCall = "tools/call";
|
||||
|
||||
/// <summary>
|
||||
/// Sent from the client to request a list of prompts and prompt templates the server has.
|
||||
/// </summary>
|
||||
public const string PromptsList = "prompts/list";
|
||||
|
||||
/// <summary>
|
||||
/// Used by the client to get a prompt provided by the server.
|
||||
/// </summary>
|
||||
public const string PromptsGet = "prompts/get";
|
||||
|
||||
/// <summary>
|
||||
/// Sent from the client to request a list of resources the server has.
|
||||
/// </summary>
|
||||
public const string ResourcesList = "resources/list";
|
||||
|
||||
/// <summary>
|
||||
/// Sent from the client to the server, to read a specific resource URI.
|
||||
/// </summary>
|
||||
public const string ResourcesRead = "resources/read";
|
||||
|
||||
/// <summary>
|
||||
/// Sent from the client to request a list of resource templates the server has.
|
||||
/// </summary>
|
||||
public const string ResourcesTemplatesList = "resources/templates/list";
|
||||
|
||||
/// <summary>
|
||||
/// Sent from the client to request resources/updated notifications from the server whenever a particular resource changes.
|
||||
/// </summary>
|
||||
public const string ResourcesSubscribe = "resources/subscribe";
|
||||
|
||||
/// <summary>
|
||||
/// Sent from the client to request cancellation of resources/updated notifications from the server.
|
||||
/// </summary>
|
||||
public const string ResourcesUnsubscribe = "resources/unsubscribe";
|
||||
|
||||
/// <summary>
|
||||
/// Sent from the server to request a list of root URIs from the client.
|
||||
/// </summary>
|
||||
public const string RootsList = "roots/list";
|
||||
|
||||
/// <summary>
|
||||
/// A ping, issued by either the server or the client, to check that the other party is still alive.
|
||||
/// </summary>
|
||||
public const string Ping = "ping";
|
||||
|
||||
/// <summary>
|
||||
/// A request from the client to the server, to enable or adjust logging.
|
||||
/// </summary>
|
||||
public const string LoggingSetLevel = "logging/setLevel";
|
||||
|
||||
/// <summary>
|
||||
/// A request from the client to the server, to ask for completion options.
|
||||
/// </summary>
|
||||
public const string CompletionComplete = "completion/complete";
|
||||
|
||||
/// <summary>
|
||||
/// A request from the server to sample an LLM via the client.
|
||||
/// </summary>
|
||||
public const string SamplingCreateMessage = "sampling/createMessage";
|
||||
|
||||
/// <summary>
|
||||
/// This request is sent from the client to the server when it first connects, asking it to begin initialization.
|
||||
/// </summary>
|
||||
public const string Initialize = "initialize";
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@ internal sealed class SseClientSessionTransport : TransportBase
|
|||
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check if the message was an initialize request
|
||||
if (message is JsonRpcRequest request && request.Method == "initialize")
|
||||
if (message is JsonRpcRequest request && request.Method == RequestMethods.Initialize)
|
||||
{
|
||||
// If the response is not a JSON-RPC response, it is an SSE message
|
||||
if (responseContent.Equals("accepted", StringComparison.OrdinalIgnoreCase))
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ internal sealed class StdioClientStreamTransport : TransportBase
|
|||
private readonly ILogger _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly DataReceivedEventHandler _logProcessErrors;
|
||||
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
||||
private Process? _process;
|
||||
private Task? _readTask;
|
||||
private CancellationTokenSource? _shutdownCts;
|
||||
|
|
@ -150,6 +151,8 @@ internal sealed class StdioClientStreamTransport : TransportBase
|
|||
/// <inheritdoc/>
|
||||
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!IsConnected || _process?.HasExited == true)
|
||||
{
|
||||
_logger.TransportNotConnected(EndpointName);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
namespace ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Types;
|
||||
|
||||
/// <summary>
|
||||
/// A request from the server to get a list of root URIs from the client.
|
||||
|
|
@ -10,5 +12,5 @@ public class ListRootsRequestParams
|
|||
/// Optional progress token for out-of-band progress notifications.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonPropertyName("progressToken")]
|
||||
public string? ProgressToken { get; init; }
|
||||
public ProgressToken? ProgressToken { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Types;
|
||||
|
|
@ -11,5 +12,5 @@ public class RequestParamsMetadata
|
|||
/// If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications.
|
||||
/// </summary>
|
||||
[JsonPropertyName("progressToken")]
|
||||
public object ProgressToken { get; set; } = default!;
|
||||
public ProgressToken? ProgressToken { get; set; } = default!;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Shared;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
|
@ -98,6 +99,27 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
|
|||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IProgress<ProgressNotificationValue>))
|
||||
{
|
||||
// Bind IProgress<ProgressNotificationValue> to the progress token in the request,
|
||||
// if there is one. If we can't get one, return a nop progress.
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
{
|
||||
var requestContent = GetRequestContext(args);
|
||||
if (requestContent?.Server is { } server &&
|
||||
requestContent?.Params?.Meta?.ProgressToken is { } progressToken)
|
||||
{
|
||||
return new TokenProgress(server, progressToken);
|
||||
}
|
||||
|
||||
return NullProgress.Instance;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// We assume that if the services used to create the tool support a particular type,
|
||||
// so too do the services associated with the server. This is the same basic assumption
|
||||
// made in ASP.NET.
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
});
|
||||
};
|
||||
|
||||
AddNotificationHandler("notifications/initialized", _ =>
|
||||
AddNotificationHandler(NotificationMethods.InitializedNotification, _ =>
|
||||
{
|
||||
if (ServerOptions.Capabilities?.Tools?.ToolCollection is { } tools)
|
||||
{
|
||||
|
|
@ -176,13 +176,13 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
|
||||
private void SetPingHandler()
|
||||
{
|
||||
SetRequestHandler<JsonNode, PingResult>("ping",
|
||||
SetRequestHandler<JsonNode, PingResult>(RequestMethods.Ping,
|
||||
(request, _) => Task.FromResult(new PingResult()));
|
||||
}
|
||||
|
||||
private void SetInitializeHandler(McpServerOptions options)
|
||||
{
|
||||
SetRequestHandler<InitializeRequestParams, InitializeResult>("initialize",
|
||||
SetRequestHandler<InitializeRequestParams, InitializeResult>(RequestMethods.Initialize,
|
||||
(request, _) =>
|
||||
{
|
||||
ClientCapabilities = request?.Capabilities ?? new();
|
||||
|
|
@ -205,7 +205,7 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
private void SetCompletionHandler(McpServerOptions options)
|
||||
{
|
||||
// This capability is not optional, so return an empty result if there is no handler.
|
||||
SetRequestHandler<CompleteRequestParams, CompleteResult>("completion/complete",
|
||||
SetRequestHandler<CompleteRequestParams, CompleteResult>(RequestMethods.CompletionComplete,
|
||||
options.GetCompletionHandler is { } handler ?
|
||||
(request, ct) => handler(new(this, request), ct) :
|
||||
(request, ct) => Task.FromResult(new CompleteResult() { Completion = new() { Values = [], Total = 0, HasMore = false } }));
|
||||
|
|
@ -229,11 +229,11 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
|
||||
listResourcesHandler ??= (static (_, _) => Task.FromResult(new ListResourcesResult()));
|
||||
|
||||
SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>("resources/list", (request, ct) => listResourcesHandler(new(this, request), ct));
|
||||
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>("resources/read", (request, ct) => readResourceHandler(new(this, request), ct));
|
||||
SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>(RequestMethods.ResourcesList, (request, ct) => listResourcesHandler(new(this, request), ct));
|
||||
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>(RequestMethods.ResourcesRead, (request, ct) => readResourceHandler(new(this, request), ct));
|
||||
|
||||
listResourceTemplatesHandler ??= (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
|
||||
SetRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>("resources/templates/list", (request, ct) => listResourceTemplatesHandler(new(this, request), ct));
|
||||
SetRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>(RequestMethods.ResourcesTemplatesList, (request, ct) => listResourceTemplatesHandler(new(this, request), ct));
|
||||
|
||||
if (resourcesCapability.Subscribe is not true)
|
||||
{
|
||||
|
|
@ -247,8 +247,8 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
throw new McpServerException("Resources capability was enabled with subscribe support, but SubscribeToResources and/or UnsubscribeFromResources handlers were not specified.");
|
||||
}
|
||||
|
||||
SetRequestHandler<SubscribeRequestParams, EmptyResult>("resources/subscribe", (request, ct) => subscribeHandler(new(this, request), ct));
|
||||
SetRequestHandler<UnsubscribeRequestParams, EmptyResult>("resources/unsubscribe", (request, ct) => unsubscribeHandler(new(this, request), ct));
|
||||
SetRequestHandler<SubscribeRequestParams, EmptyResult>(RequestMethods.ResourcesSubscribe, (request, ct) => subscribeHandler(new(this, request), ct));
|
||||
SetRequestHandler<UnsubscribeRequestParams, EmptyResult>(RequestMethods.ResourcesUnsubscribe, (request, ct) => unsubscribeHandler(new(this, request), ct));
|
||||
}
|
||||
|
||||
private void SetPromptsHandler(McpServerOptions options)
|
||||
|
|
@ -264,8 +264,8 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
throw new McpServerException("Prompts capability was enabled, but ListPrompts and/or GetPrompt handlers were not specified.");
|
||||
}
|
||||
|
||||
SetRequestHandler<ListPromptsRequestParams, ListPromptsResult>("prompts/list", (request, ct) => listPromptsHandler(new(this, request), ct));
|
||||
SetRequestHandler<GetPromptRequestParams, GetPromptResult>("prompts/get", (request, ct) => getPromptHandler(new(this, request), ct));
|
||||
SetRequestHandler<ListPromptsRequestParams, ListPromptsResult>(RequestMethods.PromptsList, (request, ct) => listPromptsHandler(new(this, request), ct));
|
||||
SetRequestHandler<GetPromptRequestParams, GetPromptResult>(RequestMethods.PromptsGet, (request, ct) => getPromptHandler(new(this, request), ct));
|
||||
}
|
||||
|
||||
private void SetToolsHandler(McpServerOptions options)
|
||||
|
|
@ -363,8 +363,8 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
}
|
||||
}
|
||||
|
||||
SetRequestHandler<ListToolsRequestParams, ListToolsResult>("tools/list", (request, ct) => listToolsHandler(new(this, request), ct));
|
||||
SetRequestHandler<CallToolRequestParams, CallToolResponse>("tools/call", (request, ct) => callToolHandler(new(this, request), ct));
|
||||
SetRequestHandler<ListToolsRequestParams, ListToolsResult>(RequestMethods.ToolsList, (request, ct) => listToolsHandler(new(this, request), ct));
|
||||
SetRequestHandler<CallToolRequestParams, CallToolResponse>(RequestMethods.ToolsCall, (request, ct) => callToolHandler(new(this, request), ct));
|
||||
}
|
||||
|
||||
private void SetSetLoggingLevelHandler(McpServerOptions options)
|
||||
|
|
@ -379,6 +379,6 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
|||
throw new McpServerException("Logging capability was enabled, but SetLoggingLevelHandler was not specified.");
|
||||
}
|
||||
|
||||
SetRequestHandler<SetLevelRequestParams, EmptyResult>("logging/setLevel", (request, ct) => setLoggingLevelHandler(new(this, request), ct));
|
||||
SetRequestHandler<SetLevelRequestParams, EmptyResult>(RequestMethods.LoggingSetLevel, (request, ct) => setLoggingLevelHandler(new(this, request), ct));
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ public static class McpServerExtensions
|
|||
}
|
||||
|
||||
return server.SendRequestAsync<CreateMessageResult>(
|
||||
new JsonRpcRequest { Method = "sampling/createMessage", Params = request },
|
||||
new JsonRpcRequest { Method = RequestMethods.SamplingCreateMessage, Params = request },
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +165,7 @@ public static class McpServerExtensions
|
|||
}
|
||||
|
||||
return server.SendRequestAsync<ListRootsResult>(
|
||||
new JsonRpcRequest { Method = "roots/list", Params = request },
|
||||
new JsonRpcRequest { Method = RequestMethods.RootsList, Params = request },
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ internal sealed class McpSession : IDisposable
|
|||
private readonly ConcurrentDictionary<RequestId, TaskCompletionSource<IJsonRpcMessage>> _pendingRequests = [];
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private int _nextRequestId;
|
||||
|
||||
private readonly string _id = Guid.NewGuid().ToString("N");
|
||||
private long _nextRequestId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpSession"/> class.
|
||||
|
|
@ -141,7 +142,7 @@ internal sealed class McpSession : IDisposable
|
|||
|
||||
private void HandleMessageWithId(IJsonRpcMessage message, IJsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
if (!messageWithId.Id.IsValid)
|
||||
if (messageWithId.Id.IsDefault)
|
||||
{
|
||||
_logger.RequestHasInvalidId(EndpointName);
|
||||
}
|
||||
|
|
@ -212,7 +213,10 @@ internal sealed class McpSession : IDisposable
|
|||
}
|
||||
|
||||
// Set request ID
|
||||
request.Id = RequestId.FromNumber(Interlocked.Increment(ref _nextRequestId));
|
||||
if (request.Id.IsDefault)
|
||||
{
|
||||
request.Id = new RequestId($"{_id}-{Interlocked.Increment(ref _nextRequestId)}");
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<IJsonRpcMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_pendingRequests[request.Id] = tcs;
|
||||
|
|
|
|||
31
src/ModelContextProtocol/TokenProgress.cs
Normal file
31
src/ModelContextProtocol/TokenProgress.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Server;
|
||||
using ModelContextProtocol.Shared;
|
||||
|
||||
namespace ModelContextProtocol;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an <see cref="IProgress{ProgressNotificationValue}"/> tied to a specific progress token and that will issue
|
||||
/// progress notifications to the supplied endpoint.
|
||||
/// </summary>
|
||||
internal sealed class TokenProgress(IMcpServer server, ProgressToken progressToken) : IProgress<ProgressNotificationValue>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Report(ProgressNotificationValue value)
|
||||
{
|
||||
_ = server.SendMessageAsync(new JsonRpcNotification()
|
||||
{
|
||||
Method = NotificationMethods.ProgressNotification,
|
||||
Params = new ProgressNotification()
|
||||
{
|
||||
ProgressToken = progressToken,
|
||||
Progress = new()
|
||||
{
|
||||
Progress = value.Progress,
|
||||
Total = value.Total,
|
||||
Message = value.Message,
|
||||
},
|
||||
},
|
||||
}, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue