Server support (stdio) (#19)
* Server support (stdio) * TestServer project * Refactored McpClient class to extract shared endpoint logic (with McpServer) to a base class, McpJsonRpcEndpoint. * Improved xmldoc comments for protocol types, mirroring specification schema comments. * Update README.MD
This commit is contained in:
parent
c384c7d3de
commit
8fe797e1f3
53 changed files with 2668 additions and 659 deletions
13
src/mcpdotnet/Protocol/Transport/IClientTransport.cs
Normal file
13
src/mcpdotnet/Protocol/Transport/IClientTransport.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
namespace McpDotNet.Protocol.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a transport mechanism for MCP communication (from the client).
|
||||
/// </summary>
|
||||
public interface IClientTransport : ITransport
|
||||
{
|
||||
/// <summary>
|
||||
/// Establishes the transport connection.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task ConnectAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
13
src/mcpdotnet/Protocol/Transport/IServerTransport.cs
Normal file
13
src/mcpdotnet/Protocol/Transport/IServerTransport.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
namespace McpDotNet.Protocol.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a transport mechanism for MCP communication (from the server).
|
||||
/// </summary>
|
||||
public interface IServerTransport : ITransport
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts listening for incoming messages.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task StartListeningAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
27
src/mcpdotnet/Protocol/Transport/ITransport.cs
Normal file
27
src/mcpdotnet/Protocol/Transport/ITransport.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using System.Threading.Channels;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
|
||||
namespace McpDotNet.Protocol.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a transport mechanism for MCP communication.
|
||||
/// </summary>
|
||||
public interface ITransport : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether the transport is currently connected.
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Channel for receiving messages from the transport.
|
||||
/// </summary>
|
||||
ChannelReader<IJsonRpcMessage> MessageReader { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message through the transport.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
|
||||
}
|
||||
308
src/mcpdotnet/Protocol/Transport/SseClientTransport.cs
Normal file
308
src/mcpdotnet/Protocol/Transport/SseClientTransport.cs
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using McpDotNet.Logging;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json.Serialization;
|
||||
using McpDotNet.Utils.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace McpDotNet.Protocol.Transport;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public sealed class SseClientTransport : TransportBase, IClientTransport
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly SseClientTransportOptions _options;
|
||||
private readonly Uri _sseEndpoint;
|
||||
private Uri? _messageEndpoint;
|
||||
private readonly CancellationTokenSource _connectionCts;
|
||||
private Task? _receiveTask;
|
||||
private readonly ILogger<SseClientTransport> _logger;
|
||||
private readonly McpServerConfig _serverConfig;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private TaskCompletionSource<bool> _connectionEstablished;
|
||||
|
||||
private string EndpointName => $"Client (SSE) for ({_serverConfig.Id}: {_serverConfig.Name})";
|
||||
|
||||
/// <summary>
|
||||
/// SSE transport for client endpoints. Unlike stdio it does not launch a process, but connects to an existing server.
|
||||
/// The HTTP server can be local or remote, and must support the SSE protocol.
|
||||
/// </summary>
|
||||
/// <param name="transportOptions">Configuration options for the transport.</param>
|
||||
/// <param name="serverConfig">The configuration object indicating which server to connect to.</param>
|
||||
/// <param name="loggerFactory">Logger factory for creating loggers.</param>
|
||||
public SseClientTransport(SseClientTransportOptions transportOptions, McpServerConfig serverConfig, ILoggerFactory loggerFactory)
|
||||
: base(loggerFactory)
|
||||
{
|
||||
_options = transportOptions;
|
||||
_serverConfig = serverConfig;
|
||||
_sseEndpoint = new Uri(serverConfig.Location!);
|
||||
_httpClient = new HttpClient();
|
||||
_connectionCts = new CancellationTokenSource();
|
||||
_logger = loggerFactory.CreateLogger<SseClientTransport>();
|
||||
_jsonOptions = new JsonSerializerOptions().ConfigureForMcp(loggerFactory);
|
||||
_connectionEstablished = new TaskCompletionSource<bool>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsConnected)
|
||||
{
|
||||
_logger.TransportAlreadyConnected(EndpointName);
|
||||
throw new McpTransportException("Transport is already connected");
|
||||
}
|
||||
|
||||
// Start message receiving loop
|
||||
_receiveTask = ReceiveMessagesAsync(_connectionCts.Token);
|
||||
|
||||
_logger.TransportReadingMessages(EndpointName);
|
||||
|
||||
await Task.WhenAny(
|
||||
_connectionEstablished.Task,
|
||||
Task.Delay(_options.ConnectionTimeout, cancellationToken)
|
||||
);
|
||||
|
||||
if (!IsConnected)
|
||||
throw new TimeoutException("Failed to receive endpoint event");
|
||||
}
|
||||
catch (McpTransportException)
|
||||
{
|
||||
// Rethrow transport exceptions
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.TransportConnectFailed(EndpointName, ex);
|
||||
await CloseAsync();
|
||||
throw new McpTransportException("Failed to connect transport", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task SendMessageAsync(
|
||||
IJsonRpcMessage message,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_messageEndpoint == null)
|
||||
throw new InvalidOperationException("Transport not connected");
|
||||
|
||||
var content = new StringContent(
|
||||
JsonSerializer.Serialize(message, _jsonOptions),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
|
||||
var response = await _httpClient.PostAsync(
|
||||
_messageEndpoint,
|
||||
content,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Handle notifications, which don't have responses
|
||||
if (message is JsonRpcNotification)
|
||||
{
|
||||
await HandleNotificationResponseAsync(message, responseContent, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var responseMessage = JsonSerializer.Deserialize<IJsonRpcMessage>(responseContent, _jsonOptions);
|
||||
if (responseMessage != null)
|
||||
{
|
||||
string messageId = "(no id)";
|
||||
if (responseMessage is IJsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
messageId = messageWithId.Id.ToString();
|
||||
}
|
||||
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
|
||||
await WriteMessageAsync(responseMessage, cancellationToken);
|
||||
_logger.TransportMessageWritten(EndpointName, messageId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.TransportMessageParseUnexpectedType(EndpointName, responseContent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task CloseAsync()
|
||||
{
|
||||
_connectionCts.Cancel();
|
||||
if (_receiveTask != null)
|
||||
await _receiveTask;
|
||||
|
||||
_httpClient.Dispose();
|
||||
_connectionCts.Dispose();
|
||||
SetConnected(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await CloseAsync();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private async Task HandleNotificationResponseAsync(
|
||||
IJsonRpcMessage message,
|
||||
string responseContent,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Check for error response, but don't require one
|
||||
try
|
||||
{
|
||||
var errorResponse = JsonSerializer.Deserialize<JsonRpcError>(responseContent, _jsonOptions);
|
||||
if (errorResponse is not null)
|
||||
{
|
||||
await WriteMessageAsync(errorResponse, cancellationToken);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Ignore deserialization errors for notifications
|
||||
// This is expected as most notifications won't have responses
|
||||
}
|
||||
}
|
||||
|
||||
internal Uri? MessageEndpoint => _messageEndpoint;
|
||||
|
||||
internal SseClientTransportOptions Options => _options;
|
||||
|
||||
private async Task ReceiveMessagesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
int reconnectAttempts = 0;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, _sseEndpoint);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||||
|
||||
using var response = await _httpClient.SendAsync(
|
||||
request,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
// Reset reconnect attempts on successful connection
|
||||
reconnectAttempts = 0;
|
||||
|
||||
string? currentEvent = null;
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null)
|
||||
{
|
||||
if (line.StartsWith("event: "))
|
||||
{
|
||||
currentEvent = line.Substring(7);
|
||||
}
|
||||
else if (line.StartsWith("data: "))
|
||||
{
|
||||
var data = line.Substring(6);
|
||||
if (currentEvent == "endpoint")
|
||||
{
|
||||
HandleEndpointEvent(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ProcessSseMessage(data, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.TransportConnectionError(EndpointName, ex);
|
||||
|
||||
reconnectAttempts++;
|
||||
if (reconnectAttempts >= _options.MaxReconnectAttempts)
|
||||
{
|
||||
throw new McpTransportException("Exceeded reconnect limit", ex);
|
||||
}
|
||||
|
||||
await Task.Delay(_options.ReconnectDelay, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessSseMessage(string data, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
_logger.TransportMessageReceivedBeforeConnected(EndpointName, data);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var message = JsonSerializer.Deserialize<IJsonRpcMessage>(data, _jsonOptions);
|
||||
if (message == null)
|
||||
{
|
||||
_logger.TransportMessageParseUnexpectedType(EndpointName, data);
|
||||
return;
|
||||
}
|
||||
|
||||
string messageId = "(no id)";
|
||||
if (message is IJsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
messageId = messageWithId.Id.ToString();
|
||||
}
|
||||
|
||||
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
|
||||
await WriteMessageAsync(message, cancellationToken);
|
||||
_logger.TransportMessageWritten(EndpointName, messageId);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.TransportMessageParseFailed(EndpointName, data, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleEndpointEvent(string data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var endpointData = JsonSerializer.Deserialize<EndpointEventData>(data, _jsonOptions);
|
||||
if (endpointData?.Uri == null)
|
||||
{
|
||||
_logger.TransportEndpointEventInvalid(EndpointName, data);
|
||||
return;
|
||||
}
|
||||
|
||||
_messageEndpoint = new Uri(endpointData.Uri);
|
||||
SetConnected(true);
|
||||
_connectionEstablished.TrySetResult(true);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.TransportEndpointEventParseFailed(EndpointName, data, ex);
|
||||
throw new McpTransportException("Failed to parse endpoint event", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private record EndpointEventData
|
||||
{
|
||||
[JsonPropertyName("uri")]
|
||||
public string? Uri { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
namespace McpDotNet.Protocol.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring the SSE transport.
|
||||
/// </summary>
|
||||
public record SseClientTransportOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Timeout for initial connection and endpoint event.
|
||||
/// </summary>
|
||||
public TimeSpan ConnectionTimeout { get; init; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Number of reconnection attempts for SSE connection.
|
||||
/// </summary>
|
||||
public int MaxReconnectAttempts { get; init; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Delay between reconnection attempts.
|
||||
/// </summary>
|
||||
public TimeSpan ReconnectDelay { get; init; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Headers to include in HTTP requests.
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? AdditionalHeaders { get; init; }
|
||||
}
|
||||
289
src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs
Normal file
289
src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Utils.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using McpDotNet.Logging;
|
||||
|
||||
namespace McpDotNet.Protocol.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the MCP transport protocol over standard input/output streams.
|
||||
/// </summary>
|
||||
public sealed class StdioClientTransport : TransportBase, IClientTransport
|
||||
{
|
||||
private readonly StdioClientTransportOptions _options;
|
||||
private readonly McpServerConfig _serverConfig;
|
||||
private readonly ILogger<StdioClientTransport> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private Process? _process;
|
||||
private Task? _readTask;
|
||||
private CancellationTokenSource? _shutdownCts;
|
||||
private bool _processStarted;
|
||||
|
||||
private string EndpointName => $"Client (stdio) for ({_serverConfig.Id}: {_serverConfig.Name})";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the StdioTransport class.
|
||||
/// </summary>
|
||||
/// <param name="options">Configuration options for the transport.</param>
|
||||
/// <param name="serverConfig">The server configuration for the transport.</param>
|
||||
/// <param name="loggerFactory">A logger factory for creating loggers.</param>
|
||||
public StdioClientTransport(StdioClientTransportOptions options, McpServerConfig serverConfig, ILoggerFactory loggerFactory)
|
||||
: base(loggerFactory)
|
||||
{
|
||||
_options = options;
|
||||
_serverConfig = serverConfig;
|
||||
_logger = loggerFactory.CreateLogger<StdioClientTransport>();
|
||||
_jsonOptions = new JsonSerializerOptions().ConfigureForMcp(loggerFactory);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (IsConnected)
|
||||
{
|
||||
_logger.TransportAlreadyConnected(EndpointName);
|
||||
throw new McpTransportException("Transport is already connected");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.TransportConnecting(EndpointName);
|
||||
|
||||
_shutdownCts = new CancellationTokenSource();
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = _options.Command,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = _options.WorkingDirectory ?? Environment.CurrentDirectory,
|
||||
};
|
||||
|
||||
if (_options.Arguments != null)
|
||||
{
|
||||
foreach (var arg in _options.Arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(arg);
|
||||
}
|
||||
}
|
||||
|
||||
if (_options.EnvironmentVariables != null)
|
||||
{
|
||||
foreach (var (key, value) in _options.EnvironmentVariables)
|
||||
{
|
||||
startInfo.Environment[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.CreateProcessForTransport(EndpointName, _options.Command,
|
||||
string.Join(", ", startInfo.ArgumentList), string.Join(", ", startInfo.Environment.Select(kvp => kvp.Key + "=" + kvp.Value)),
|
||||
startInfo.WorkingDirectory, _options.ShutdownTimeout.ToString());
|
||||
|
||||
_process = new Process { StartInfo = startInfo };
|
||||
|
||||
// Set up error logging
|
||||
_process.ErrorDataReceived += (sender, args) =>
|
||||
{
|
||||
_logger.TransportError(EndpointName, args.Data ?? "(no data)");
|
||||
};
|
||||
|
||||
if (!_process.Start())
|
||||
{
|
||||
_logger.TransportProcessStartFailed(EndpointName);
|
||||
throw new McpTransportException("Failed to start MCP server process");
|
||||
}
|
||||
_logger.TransportProcessStarted(EndpointName, _process.Id);
|
||||
_processStarted = true;
|
||||
_process.BeginErrorReadLine();
|
||||
|
||||
|
||||
// Start reading messages in the background
|
||||
_readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token));
|
||||
_logger.TransportReadingMessages(EndpointName);
|
||||
|
||||
SetConnected(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.TransportConnectFailed(EndpointName, ex);
|
||||
await CleanupAsync(cancellationToken);
|
||||
throw new McpTransportException("Failed to connect transport", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsConnected || _process?.HasExited == true)
|
||||
{
|
||||
_logger.TransportNotConnected(EndpointName);
|
||||
throw new McpTransportException("Transport is not connected");
|
||||
}
|
||||
|
||||
string id = "(no id)";
|
||||
if (message is IJsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
id = messageWithId.Id.ToString();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(message, _jsonOptions);
|
||||
_logger.TransportSendingMessage(EndpointName, id, json);
|
||||
|
||||
// Write the message followed by a newline
|
||||
await _process!.StandardInput.WriteLineAsync(json.AsMemory(), cancellationToken);
|
||||
await _process.StandardInput.FlushAsync();
|
||||
|
||||
_logger.TransportSentMessage(EndpointName, id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.TransportSendFailed(EndpointName, id, ex);
|
||||
throw new McpTransportException("Failed to send message", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await CleanupAsync(CancellationToken.None);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private async Task ReadMessagesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.TransportEnteringReadMessagesLoop(EndpointName);
|
||||
|
||||
using var reader = _process!.StandardOutput;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested && !_process.HasExited)
|
||||
{
|
||||
_logger.TransportWaitingForMessage(EndpointName);
|
||||
var line = await reader.ReadLineAsync(cancellationToken);
|
||||
if (line == null)
|
||||
{
|
||||
_logger.TransportEndOfStream(EndpointName);
|
||||
break;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.TransportReceivedMessage(EndpointName, line);
|
||||
|
||||
try
|
||||
{
|
||||
var message = JsonSerializer.Deserialize<IJsonRpcMessage>(line, _jsonOptions);
|
||||
if (message != null)
|
||||
{
|
||||
string messageId = "(no id)";
|
||||
if (message is IJsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
messageId = messageWithId.Id.ToString();
|
||||
}
|
||||
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
|
||||
await WriteMessageAsync(message, cancellationToken);
|
||||
_logger.TransportMessageWritten(EndpointName, messageId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.TransportMessageParseUnexpectedType(EndpointName, line);
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.TransportMessageParseFailed(EndpointName, line, ex);
|
||||
// Continue reading even if we fail to parse a message
|
||||
}
|
||||
}
|
||||
_logger.TransportExitingReadMessagesLoop(EndpointName);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.TransportReadMessagesCancelled(EndpointName);
|
||||
// Normal shutdown
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.TransportReadMessagesFailed(EndpointName, ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await CleanupAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanupAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.TransportCleaningUp(EndpointName);
|
||||
if (_process != null && _processStarted && !_process.HasExited)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to close stdin to signal the process to exit
|
||||
_logger.TransportClosingStdin(EndpointName);
|
||||
_process.StandardInput.Close();
|
||||
|
||||
// Wait for the process to exit
|
||||
_logger.TransportWaitingForShutdown(EndpointName);
|
||||
if (!_process.WaitForExit((int)_options.ShutdownTimeout.TotalMilliseconds))
|
||||
{
|
||||
// If it doesn't exit gracefully, terminate it
|
||||
_logger.TransportKillingProcess(EndpointName);
|
||||
_process.Kill(true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.TransportShutdownFailed(EndpointName, ex);
|
||||
}
|
||||
|
||||
_process.Dispose();
|
||||
_process = null;
|
||||
}
|
||||
|
||||
_shutdownCts?.Cancel();
|
||||
_shutdownCts?.Dispose();
|
||||
_shutdownCts = null;
|
||||
|
||||
if (_readTask != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.TransportWaitingForReadTask(EndpointName);
|
||||
await _readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
_logger.TransportCleanupReadTaskTimeout(EndpointName);
|
||||
// Continue with cleanup
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.TransportCleanupReadTaskCancelled(EndpointName);
|
||||
// Ignore cancellation
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.TransportCleanupReadTaskFailed(EndpointName, ex);
|
||||
}
|
||||
_readTask = null;
|
||||
_logger.TransportReadTaskCleanedUp(EndpointName);
|
||||
}
|
||||
|
||||
SetConnected(false);
|
||||
_logger.TransportCleanedUp(EndpointName);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Protocol/Transport/StdioTransport.cs
|
||||
namespace McpDotNet.Protocol.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Represents configuration options for the stdio transport.
|
||||
/// </summary>
|
||||
public record StdioClientTransportOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The command to execute to start the server process.
|
||||
/// </summary>
|
||||
public required string Command { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Arguments to pass to the server process.
|
||||
/// </summary>
|
||||
public string[]? Arguments { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// The working directory for the server process.
|
||||
/// </summary>
|
||||
public string? WorkingDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment variables to set for the server process.
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? EnvironmentVariables { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The timeout to wait for the server to shut down gracefully.
|
||||
/// </summary>
|
||||
public TimeSpan ShutdownTimeout { get; init; } = TimeSpan.FromSeconds(5);
|
||||
}
|
||||
195
src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs
Normal file
195
src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Utils.Json;
|
||||
using McpDotNet.Logging;
|
||||
|
||||
namespace McpDotNet.Protocol.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the MCP transport protocol over standard input/output streams.
|
||||
/// </summary>
|
||||
public sealed class StdioServerTransport : TransportBase, IServerTransport
|
||||
{
|
||||
private readonly string _serverName;
|
||||
private readonly ILogger<StdioClientTransport> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private Task? _readTask;
|
||||
private CancellationTokenSource? _shutdownCts;
|
||||
|
||||
private string EndpointName => $"Server (stdio) ({_serverName})";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the StdioServerTransport class.
|
||||
/// </summary>
|
||||
/// <param name="serverName">The name of the server.</param>
|
||||
/// <param name="loggerFactory">A logger factory for creating loggers.</param>
|
||||
public StdioServerTransport(string serverName, ILoggerFactory loggerFactory)
|
||||
: base(loggerFactory)
|
||||
{
|
||||
_serverName = serverName;
|
||||
_logger = loggerFactory.CreateLogger<StdioClientTransport>();
|
||||
_jsonOptions = new JsonSerializerOptions().ConfigureForMcp(loggerFactory);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StartListeningAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_shutdownCts = new CancellationTokenSource();
|
||||
|
||||
_readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token));
|
||||
|
||||
SetConnected(true);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
_logger.TransportNotConnected(EndpointName);
|
||||
throw new McpTransportException("Transport is not connected");
|
||||
}
|
||||
|
||||
string id = "(no id)";
|
||||
if (message is IJsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
id = messageWithId.Id.ToString();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(message, _jsonOptions);
|
||||
_logger.TransportSendingMessage(EndpointName, id, json);
|
||||
|
||||
using (Console.OpenStandardOutput())
|
||||
{
|
||||
await Console.Out.WriteLineAsync(json.AsMemory(), cancellationToken);
|
||||
await Console.Out.FlushAsync();
|
||||
}
|
||||
|
||||
_logger.TransportSentMessage(EndpointName, id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.TransportSendFailed(EndpointName, id, ex);
|
||||
throw new McpTransportException("Failed to send message", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await CleanupAsync(CancellationToken.None);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private async Task ReadMessagesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.TransportEnteringReadMessagesLoop(EndpointName);
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.TransportWaitingForMessage(EndpointName);
|
||||
using (Console.OpenStandardInput())
|
||||
{
|
||||
var reader = Console.In;
|
||||
var line = await reader.ReadLineAsync(cancellationToken);
|
||||
if (line == null)
|
||||
{
|
||||
_logger.TransportEndOfStream(EndpointName);
|
||||
break;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.TransportReceivedMessage(EndpointName, line);
|
||||
|
||||
try
|
||||
{
|
||||
var message = JsonSerializer.Deserialize<IJsonRpcMessage>(line, _jsonOptions);
|
||||
if (message != null)
|
||||
{
|
||||
string messageId = "(no id)";
|
||||
if (message is IJsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
messageId = messageWithId.Id.ToString();
|
||||
}
|
||||
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
|
||||
await WriteMessageAsync(message, cancellationToken);
|
||||
_logger.TransportMessageWritten(EndpointName, messageId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.TransportMessageParseUnexpectedType(EndpointName, line);
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.TransportMessageParseFailed(EndpointName, line, ex);
|
||||
// Continue reading even if we fail to parse a message
|
||||
}
|
||||
}
|
||||
}
|
||||
_logger.TransportExitingReadMessagesLoop(EndpointName);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.TransportReadMessagesCancelled(EndpointName);
|
||||
// Normal shutdown
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.TransportReadMessagesFailed(EndpointName, ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await CleanupAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanupAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.TransportCleaningUp(EndpointName);
|
||||
|
||||
_shutdownCts?.Cancel();
|
||||
_shutdownCts?.Dispose();
|
||||
_shutdownCts = null;
|
||||
|
||||
if (_readTask != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.TransportWaitingForReadTask(EndpointName);
|
||||
await _readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
_logger.TransportCleanupReadTaskTimeout(EndpointName);
|
||||
// Continue with cleanup
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.TransportCleanupReadTaskCancelled(EndpointName);
|
||||
// Ignore cancellation
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.TransportCleanupReadTaskFailed(EndpointName, ex);
|
||||
}
|
||||
_readTask = null;
|
||||
_logger.TransportReadTaskCleanedUp(EndpointName);
|
||||
}
|
||||
|
||||
SetConnected(false);
|
||||
_logger.TransportCleanedUp(EndpointName);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Threading.Channels;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using McpDotNet.Logging;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
|
||||
namespace McpDotNet.Protocol.Transport;
|
||||
|
|
@ -6,15 +8,16 @@ namespace McpDotNet.Protocol.Transport;
|
|||
/// <summary>
|
||||
/// Base class for implementing MCP transports with common functionality.
|
||||
/// </summary>
|
||||
public abstract class TransportBase : IMcpTransport
|
||||
public abstract class TransportBase : ITransport
|
||||
{
|
||||
private readonly Channel<IJsonRpcMessage> _messageChannel;
|
||||
private readonly ILogger<TransportBase> _logger;
|
||||
private bool _isConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TransportBase"/> class.
|
||||
/// </summary>
|
||||
protected TransportBase()
|
||||
protected TransportBase(ILoggerFactory loggerFactory)
|
||||
{
|
||||
// Unbounded channel to prevent blocking on writes
|
||||
_messageChannel = Channel.CreateUnbounded<IJsonRpcMessage>(new UnboundedChannelOptions
|
||||
|
|
@ -22,6 +25,7 @@ public abstract class TransportBase : IMcpTransport
|
|||
SingleReader = true,
|
||||
SingleWriter = true,
|
||||
});
|
||||
_logger = loggerFactory.CreateLogger<TransportBase>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -30,9 +34,6 @@ public abstract class TransportBase : IMcpTransport
|
|||
/// <inheritdoc/>
|
||||
public ChannelReader<IJsonRpcMessage> MessageReader => _messageChannel.Reader;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task ConnectAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
|
||||
|
||||
|
|
@ -51,7 +52,9 @@ public abstract class TransportBase : IMcpTransport
|
|||
throw new McpTransportException("Transport is not connected");
|
||||
}
|
||||
|
||||
_logger.TransportWritingMessageToChannel(message);
|
||||
await _messageChannel.Writer.WriteAsync(message, cancellationToken);
|
||||
_logger.TransportMessageWrittenToChannel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace McpDotNet.Protocol.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Represents annotations that can be attached to content.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public record Annotations
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace McpDotNet.Protocol.Types;
|
|||
|
||||
/// <summary>
|
||||
/// Used for completion requests to provide additional context for the completion options.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public class Argument
|
||||
{
|
||||
|
|
|
|||
20
src/mcpdotnet/Protocol/Types/CallToolRequestParams.cs
Normal file
20
src/mcpdotnet/Protocol/Types/CallToolRequestParams.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
namespace McpDotNet.Protocol.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Used by the client to invoke a tool provided by the server.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public class CallToolRequestParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Tool name.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional arguments to pass to the tool.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonPropertyName("arguments")]
|
||||
public Dictionary<string,object>? Arguments { get; init; }
|
||||
}
|
||||
|
|
@ -1,8 +1,17 @@
|
|||
namespace McpDotNet.Protocol.Types;
|
||||
|
||||
/// <summary>
|
||||
/// A response to a request to call a tool on the server.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/schema.json">See the schema for details</see>
|
||||
/// The server's response to a tool call.
|
||||
///
|
||||
/// Any errors that originate from the tool SHOULD be reported inside the result
|
||||
/// object, with `isError` set to true, _not_ as an MCP protocol-level error
|
||||
/// response. Otherwise, the LLM would not be able to see that an error occurred
|
||||
/// and self-correct.
|
||||
///
|
||||
/// However, any errors in _finding_ the tool, an error indicating that the
|
||||
/// server does not support tool calls, or any other exceptional conditions,
|
||||
/// should be reported as an MCP error response.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public class CallToolResponse
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
|
||||
using System.Text.Json.Serialization;
|
||||
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>
|
||||
/// </summary>
|
||||
public record ClientCapabilities
|
||||
{
|
||||
|
|
@ -28,6 +28,7 @@ public record ClientCapabilities
|
|||
|
||||
/// <summary>
|
||||
/// Represents the roots capability configuration.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public record RootsCapability
|
||||
{
|
||||
|
|
@ -40,6 +41,7 @@ public record RootsCapability
|
|||
|
||||
/// <summary>
|
||||
/// Represents the sampling capability configuration.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public record SamplingCapability
|
||||
{
|
||||
|
|
@ -48,6 +50,7 @@ public record SamplingCapability
|
|||
|
||||
/// <summary>
|
||||
/// Represents the logging capability configuration.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public record LoggingCapability
|
||||
{
|
||||
|
|
@ -56,6 +59,7 @@ public record LoggingCapability
|
|||
|
||||
/// <summary>
|
||||
/// Represents the prompts capability configuration.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public record PromptsCapability
|
||||
{
|
||||
|
|
@ -68,6 +72,7 @@ public record PromptsCapability
|
|||
|
||||
/// <summary>
|
||||
/// Represents the resources capability configuration.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public record ResourcesCapability
|
||||
{
|
||||
|
|
@ -86,6 +91,7 @@ public record ResourcesCapability
|
|||
|
||||
/// <summary>
|
||||
/// Represents the tools capability configuration.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public record ToolsCapability
|
||||
{
|
||||
|
|
|
|||
20
src/mcpdotnet/Protocol/Types/CompleteRequestParams.cs
Normal file
20
src/mcpdotnet/Protocol/Types/CompleteRequestParams.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
namespace McpDotNet.Protocol.Types;
|
||||
|
||||
/// <summary>
|
||||
/// A request from the client to the server, to ask for completion options.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public class CompleteRequestParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The reference's information
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonPropertyName("ref")]
|
||||
public required Reference Ref { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The argument's information
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonPropertyName("argument")]
|
||||
public required Argument Argument { get; init; }
|
||||
}
|
||||
|
|
@ -3,7 +3,8 @@
|
|||
namespace McpDotNet.Protocol.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a completion object in the server's response.
|
||||
/// Represents a completion object in the server's response
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public class Completion
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ namespace McpDotNet.Protocol.Types;
|
|||
|
||||
/// <summary>
|
||||
/// Represents the content of a tool response.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/schema.json">See the schema for details</see>
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// There are multiple subtypes of content, depending on the "type" field, these are represented as separate classes.
|
||||
/// </summary>
|
||||
public class Content
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
/// <summary>
|
||||
/// A request to include context from one or more MCP servers (including the caller), to be attached to the prompt.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public enum ContextInclusion
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
///
|
||||
/// While these align with the protocol specification,
|
||||
/// clients have full discretion over model selection and should inform users before sampling.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public class CreateMessageRequestParams
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ namespace McpDotNet.Protocol.Types;
|
|||
/// The client's response to a sampling/create_message request from the server.
|
||||
/// The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop)
|
||||
/// and decide whether to allow the server to see it.
|
||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
|
||||
/// </summary>
|
||||
public class CreateMessageResult
|
||||
{
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue