Overhaul McpClientFactory/McpServerFactory (#87)
Handlers are now specified before connecting the client/server. Otherwise, race conditions exist. Handlers move into being a part of the McpClientOptions/McpServerOptions, so that they're provided to the factories. Where relevant, the handlers are specified as part of the capability descriptors, so that they go hand in hand. The factories are no longer stateful. Instead of allocating a factory and then calling a create method on it, you just call a static factory method.
This commit is contained in:
parent
63b24a4036
commit
08b0a04dab
9 changed files with 1306 additions and 2243 deletions
|
|
@ -1,8 +1,4 @@
|
|||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Protocol.Types;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace McpDotNet.Protocol.Messages;
|
||||
namespace McpDotNet.Protocol.Messages;
|
||||
|
||||
/// <summary>Provides names of standard operations for use with registering handlers.</summary>
|
||||
/// <remarks>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json;
|
||||
using McpDotNet.Utils;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace McpDotNet.Protocol.Messages;
|
||||
|
|
@ -22,14 +23,15 @@ public class RequestIdConverter : JsonConverter<RequestId>
|
|||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options)
|
||||
{
|
||||
if (writer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(writer));
|
||||
}
|
||||
Throw.IfNull(writer);
|
||||
|
||||
if (value.IsString)
|
||||
{
|
||||
writer.WriteStringValue(value.AsString);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNumberValue(value.AsNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
// Copied from:
|
||||
// https://github.com/dotnet/runtime/blob/d2650b6ae7023a2d9d2c74c56116f1f18472ab04/src/libraries/System.Private.CoreLib/src/System/PasteArguments.cs
|
||||
// and changed from using ValueStringBuilder to StringBuilder.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace McpDotNet.Protocol.Transport
|
||||
{
|
||||
internal static partial class PasteArguments
|
||||
{
|
||||
internal static void AppendArgument(StringBuilder stringBuilder, string argument)
|
||||
{
|
||||
if (stringBuilder.Length != 0)
|
||||
{
|
||||
stringBuilder.Append(' ');
|
||||
}
|
||||
|
||||
// Parsing rules for non-argv[0] arguments:
|
||||
// - Backslash is a normal character except followed by a quote.
|
||||
// - 2N backslashes followed by a quote ==> N literal backslashes followed by unescaped quote
|
||||
// - 2N+1 backslashes followed by a quote ==> N literal backslashes followed by a literal quote
|
||||
// - Parsing stops at first whitespace outside of quoted region.
|
||||
// - (post 2008 rule): A closing quote followed by another quote ==> literal quote, and parsing remains in quoting mode.
|
||||
if (argument.Length != 0 && ContainsNoWhitespaceOrQuotes(argument))
|
||||
{
|
||||
// Simple case - no quoting or changes needed.
|
||||
stringBuilder.Append(argument);
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append(Quote);
|
||||
int idx = 0;
|
||||
while (idx < argument.Length)
|
||||
{
|
||||
char c = argument[idx++];
|
||||
if (c == Backslash)
|
||||
{
|
||||
int numBackSlash = 1;
|
||||
while (idx < argument.Length && argument[idx] == Backslash)
|
||||
{
|
||||
idx++;
|
||||
numBackSlash++;
|
||||
}
|
||||
|
||||
if (idx == argument.Length)
|
||||
{
|
||||
// We'll emit an end quote after this so must double the number of backslashes.
|
||||
stringBuilder.Append(Backslash, numBackSlash * 2);
|
||||
}
|
||||
else if (argument[idx] == Quote)
|
||||
{
|
||||
// Backslashes will be followed by a quote. Must double the number of backslashes.
|
||||
stringBuilder.Append(Backslash, numBackSlash * 2 + 1);
|
||||
stringBuilder.Append(Quote);
|
||||
idx++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Backslash will not be followed by a quote, so emit as normal characters.
|
||||
stringBuilder.Append(Backslash, numBackSlash);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == Quote)
|
||||
{
|
||||
// Escape the quote so it appears as a literal. This also guarantees that we won't end up generating a closing quote followed
|
||||
// by another quote (which parses differently pre-2008 vs. post-2008.)
|
||||
stringBuilder.Append(Backslash);
|
||||
stringBuilder.Append(Quote);
|
||||
continue;
|
||||
}
|
||||
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
|
||||
stringBuilder.Append(Quote);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContainsNoWhitespaceOrQuotes(string s)
|
||||
{
|
||||
for (int i = 0; i < s.Length; i++)
|
||||
{
|
||||
char c = s[i];
|
||||
if (char.IsWhiteSpace(c) || c == Quote)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private const char Quote = '\"';
|
||||
private const char Backslash = '\\';
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System.Text.Json;
|
|||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Logging;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Utils;
|
||||
using McpDotNet.Utils.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
|
@ -54,27 +55,16 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
public SseClientTransport(SseClientTransportOptions transportOptions, McpServerConfig serverConfig, HttpClient httpClient, ILoggerFactory? loggerFactory, bool ownsHttpClient = false)
|
||||
: base(loggerFactory)
|
||||
{
|
||||
if (transportOptions is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(transportOptions));
|
||||
}
|
||||
|
||||
if (serverConfig is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serverConfig));
|
||||
}
|
||||
|
||||
if (httpClient is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpClient));
|
||||
}
|
||||
Throw.IfNull(transportOptions);
|
||||
Throw.IfNull(serverConfig);
|
||||
Throw.IfNull(httpClient);
|
||||
|
||||
_options = transportOptions;
|
||||
_serverConfig = serverConfig;
|
||||
_sseEndpoint = new Uri(serverConfig.Location!);
|
||||
_httpClient = httpClient;
|
||||
_connectionCts = new CancellationTokenSource();
|
||||
_logger = loggerFactory is not null ? loggerFactory.CreateLogger<SseClientTransport>() : NullLogger.Instance;
|
||||
_logger = (ILogger?)loggerFactory?.CreateLogger<SseClientTransport>() ?? NullLogger.Instance;
|
||||
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
|
||||
_connectionEstablished = new TaskCompletionSource<bool>();
|
||||
_ownsHttpClient = ownsHttpClient;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Logging;
|
||||
|
|
@ -10,6 +8,8 @@ using McpDotNet.Utils.Json;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
#pragma warning disable CA2213 // Disposable fields should be disposed
|
||||
|
||||
namespace McpDotNet.Protocol.Transport;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -37,15 +37,8 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
public StdioClientTransport(StdioClientTransportOptions options, McpServerConfig serverConfig, ILoggerFactory? loggerFactory = null)
|
||||
: base(loggerFactory)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (serverConfig is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serverConfig));
|
||||
}
|
||||
Throw.IfNull(options);
|
||||
Throw.IfNull(serverConfig);
|
||||
|
||||
_options = options;
|
||||
_serverConfig = serverConfig;
|
||||
|
|
@ -71,23 +64,17 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = _options.Command,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = _options.WorkingDirectory ?? Environment.CurrentDirectory,
|
||||
};
|
||||
|
||||
if (_options.Arguments is { Length: > 0 })
|
||||
if (!string.IsNullOrWhiteSpace(_options.Arguments))
|
||||
{
|
||||
StringBuilder argsBuilder = new();
|
||||
foreach (var arg in _options.Arguments)
|
||||
{
|
||||
PasteArguments.AppendArgument(argsBuilder, arg);
|
||||
}
|
||||
|
||||
startInfo.Arguments = argsBuilder.ToString();
|
||||
startInfo.Arguments = _options.Arguments;
|
||||
}
|
||||
|
||||
if (_options.EnvironmentVariables != null)
|
||||
|
|
@ -105,10 +92,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
_process = new Process { StartInfo = startInfo };
|
||||
|
||||
// Set up error logging
|
||||
_process.ErrorDataReceived += (sender, args) =>
|
||||
{
|
||||
_logger.TransportError(EndpointName, args.Data ?? "(no data)");
|
||||
};
|
||||
_process.ErrorDataReceived += (sender, args) => _logger.TransportError(EndpointName, args.Data ?? "(no data)");
|
||||
|
||||
if (!_process.Start())
|
||||
{
|
||||
|
|
@ -120,7 +104,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
_process.BeginErrorReadLine();
|
||||
|
||||
// Start reading messages in the background
|
||||
_readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token).ConfigureAwait(false), CancellationToken.None);
|
||||
_readTask = Task.Run(() => ReadMessagesAsync(_shutdownCts.Token), CancellationToken.None);
|
||||
_logger.TransportReadingMessages(EndpointName);
|
||||
|
||||
SetConnected(true);
|
||||
|
|
@ -272,10 +256,10 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
_process = null;
|
||||
}
|
||||
|
||||
if (_shutdownCts != null)
|
||||
if (_shutdownCts is { } shutdownCts)
|
||||
{
|
||||
await _shutdownCts.CancelAsync().ConfigureAwait(false);
|
||||
_shutdownCts.Dispose();
|
||||
await shutdownCts.CancelAsync().ConfigureAwait(false);
|
||||
shutdownCts.Dispose();
|
||||
_shutdownCts = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public record StdioClientTransportOptions
|
|||
/// <summary>
|
||||
/// Arguments to pass to the server process.
|
||||
/// </summary>
|
||||
public string[]? Arguments { get; set; } = [];
|
||||
public string? Arguments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The working directory for the server process.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
using System.Text.Json;
|
||||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Logging;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Server;
|
||||
using McpDotNet.Utils;
|
||||
using McpDotNet.Utils.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
#pragma warning disable CA2208 // Instantiate argument exceptions correctly
|
||||
#pragma warning disable CA2213 // Disposable fields should be disposed
|
||||
|
||||
namespace McpDotNet.Protocol.Transport;
|
||||
|
||||
|
|
@ -104,12 +105,15 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
|
|||
public StdioServerTransport(string serverName, TextReader input, TextWriter output, ILoggerFactory? loggerFactory = null)
|
||||
: base(loggerFactory)
|
||||
{
|
||||
_serverName = serverName ?? throw new ArgumentNullException(nameof(serverName));
|
||||
_input = input ?? throw new ArgumentNullException(nameof(input));
|
||||
_output = output ?? throw new ArgumentNullException(nameof(output));
|
||||
Throw.IfNull(serverName);
|
||||
Throw.IfNull(input);
|
||||
Throw.IfNull(output);
|
||||
|
||||
_logger = loggerFactory is not null ? loggerFactory.CreateLogger<StdioClientTransport>() : NullLogger.Instance;
|
||||
_serverName = serverName;
|
||||
_input = input;
|
||||
_output = output;
|
||||
|
||||
_logger = (ILogger?)loggerFactory?.CreateLogger<StdioClientTransport>() ?? NullLogger.Instance;
|
||||
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
|
||||
}
|
||||
|
||||
|
|
@ -274,15 +278,8 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
|
|||
/// <summary>Validates the <paramref name="serverOptions"/> and extracts from it the server name to use.</summary>
|
||||
private static string GetServerName(McpServerOptions serverOptions)
|
||||
{
|
||||
if (serverOptions is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serverOptions));
|
||||
}
|
||||
|
||||
if (serverOptions.ServerInfo is null)
|
||||
{
|
||||
throw new ArgumentNullException($"{nameof(serverOptions)}.{nameof(serverOptions.ServerInfo)}");
|
||||
}
|
||||
Throw.IfNull(serverOptions);
|
||||
Throw.IfNull(serverOptions.ServerInfo);
|
||||
|
||||
return serverOptions.ServerInfo.Name;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public abstract class TransportBase : ITransport
|
|||
SingleReader = true,
|
||||
SingleWriter = true,
|
||||
});
|
||||
_logger = loggerFactory is not null ? loggerFactory.CreateLogger<TransportBase>() : NullLogger.Instance;
|
||||
_logger = (ILogger?)loggerFactory?.CreateLogger<TransportBase>() ?? NullLogger.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using McpDotNet.Server;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace McpDotNet.Protocol.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the capabilities that a client may support.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
|
|
@ -37,6 +39,10 @@ public record RootsCapability
|
|||
/// </summary>
|
||||
[JsonPropertyName("listChanged")]
|
||||
public bool? ListChanged { get; init; }
|
||||
|
||||
/// <summary>Gets or sets the handler for sampling requests.</summary>
|
||||
[JsonIgnore]
|
||||
public Func<ListRootsRequestParams?, CancellationToken, Task<ListRootsResult>>? RootsHandler { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -46,6 +52,10 @@ public record RootsCapability
|
|||
public record SamplingCapability
|
||||
{
|
||||
// Currently empty in the spec, but may be extended in the future
|
||||
|
||||
/// <summary>Gets or sets the handler for sampling requests.</summary>
|
||||
[JsonIgnore]
|
||||
public Func<CreateMessageRequestParams?, CancellationToken, Task<CreateMessageResult>>? SamplingHandler { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -68,6 +78,18 @@ public record PromptsCapability
|
|||
/// </summary>
|
||||
[JsonPropertyName("listChanged")]
|
||||
public bool? ListChanged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for list prompts requests.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for get prompt requests.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -87,6 +109,30 @@ public record ResourcesCapability
|
|||
/// </summary>
|
||||
[JsonPropertyName("listChanged")]
|
||||
public bool? ListChanged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for list resources requests.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for read resources requests.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for subscribe to resources messages.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<string>, CancellationToken, Task>? SubscribeToResourcesHandler { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for unsubscribe from resources messages.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<string>, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -100,4 +146,16 @@ public record ToolsCapability
|
|||
/// </summary>
|
||||
[JsonPropertyName("listChanged")]
|
||||
public bool? ListChanged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for list tools requests.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for call tool requests.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; init; }
|
||||
}
|
||||
Loading…
Reference in a new issue