Clean up (use of) McpException and McpTransportException (#321)

* Clean up (use of) McpException

* Replace McpTransportException with InvalidOperationException

* Add cancellation test
This commit is contained in:
Stephen Toub 2025-04-18 12:25:29 -04:00 • committed by GitHub
commit 7d77be68cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 231 additions and 219 deletions

View file

@ -1,32 +0,0 @@
namespace ModelContextProtocol.Protocol.Messages;
/// <summary>
/// Standard JSON-RPC error codes as defined in the MCP specification.
/// </summary>
internal static class ErrorCodes
{
/// <summary>
/// Invalid JSON was received by the server.
/// </summary>
public const int ParseError = -32700;
/// <summary>
/// The JSON sent is not a valid Request object.
/// </summary>
public const int InvalidRequest = -32600;
/// <summary>
/// The method does not exist / is not available.
/// </summary>
public const int MethodNotFound = -32601;
/// <summary>
/// Invalid method parameter(s).
/// </summary>
public const int InvalidParams = -32602;
/// <summary>
/// Internal JSON-RPC error.
/// </summary>
public const int InternalError = -32603;
}

View file

@ -42,6 +42,6 @@ public interface IClientTransport
/// This method is used by <see cref="McpClientFactory"/> to initialize the connection.
/// </para>
/// </remarks>
/// <exception cref="McpTransportException">The transport connection could not be established.</exception>
/// <exception cref="InvalidOperationException">The transport connection could not be established.</exception>
Task<ITransport> ConnectAsync(CancellationToken cancellationToken = default);
}

View file

@ -1,34 +0,0 @@
namespace ModelContextProtocol.Protocol.Transport;
/// <summary>
/// Represents errors that occur in MCP transport operations.
/// </summary>
public class McpTransportException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="McpTransportException"/> class.
/// </summary>
public McpTransportException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="McpTransportException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public McpTransportException(string message)
: base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="McpTransportException"/> class with a specified error message
/// and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The message that describes the error.</param>
/// <param name="innerException">The exception that is the cause of the current exception.</param>
public McpTransportException(string message, Exception? innerException)
: base(message, innerException)
{
}
}

View file

@ -58,11 +58,11 @@ internal sealed partial class SseClientSessionTransport : TransportBase
await _connectionEstablished.Task.WaitAsync(_options.ConnectionTimeout, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (ex is not McpTransportException) // propagate transport exceptions
catch (Exception ex)
{
LogTransportConnectFailed(Name, ex);
await CloseAsync().ConfigureAwait(false);
throw new McpTransportException("Failed to connect transport", ex);
throw new InvalidOperationException("Failed to connect transport", ex);
}
}
@ -110,7 +110,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
else
{
JsonRpcResponse initializeResponse = JsonSerializer.Deserialize(responseContent, McpJsonUtilities.JsonContext.Default.JsonRpcResponse) ??
throw new McpTransportException("Failed to initialize client");
throw new InvalidOperationException("Failed to initialize client");
LogTransportReceivedMessage(Name, messageId);
await WriteMessageAsync(initializeResponse, cancellationToken).ConfigureAwait(false);
@ -136,7 +136,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
LogRejectedPost(Name, messageId);
}
throw new McpTransportException("Failed to send message");
throw new InvalidOperationException("Failed to send message");
}
}
@ -273,34 +273,18 @@ internal sealed partial class SseClientSessionTransport : TransportBase
private void HandleEndpointEvent(string data)
{
try
if (string.IsNullOrEmpty(data))
{
if (string.IsNullOrEmpty(data))
{
LogTransportEndpointEventInvalid(Name);
return;
}
// If data is an absolute URL, the Uri will be constructed entirely from it and not the _sseEndpoint.
_messageEndpoint = new Uri(_sseEndpoint, data);
// Set connected state
SetConnected(true);
_connectionEstablished.TrySetResult(true);
LogTransportEndpointEventInvalid(Name);
return;
}
catch (JsonException ex)
{
if (_logger.IsEnabled(LogLevel.Trace))
{
LogTransportEndpointEventParseFailedSensitive(Name, data, ex);
}
else
{
LogTransportEndpointEventParseFailed(Name, ex);
}
throw new McpTransportException("Failed to parse endpoint event", ex);
}
// If data is an absolute URL, the Uri will be constructed entirely from it and not the _sseEndpoint.
_messageEndpoint = new Uri(_sseEndpoint, data);
// Set connected state
SetConnected(true);
_connectionEstablished.TrySetResult(true);
}
private void CopyAdditionalHeaders(HttpRequestHeaders headers)

View file

@ -23,14 +23,14 @@ internal sealed class StdioClientSessionTransport : StreamClientSessionTransport
/// <para>
/// For stdio-based transports, this implementation first verifies that the underlying process
/// is still running before attempting to send the message. If the process has exited or cannot
/// be accessed, a <see cref="McpTransportException"/> is thrown with details about the failure.
/// be accessed, a <see cref="InvalidOperationException"/> is thrown with details about the failure.
/// </para>
/// <para>
/// After verifying the process state, this method delegates to the base class implementation
/// to handle the actual message serialization and transmission to the process's standard input stream.
/// </para>
/// </remarks>
/// <exception cref="McpTransportException">
/// <exception cref="InvalidOperationException">
/// Thrown when the underlying process has exited or cannot be accessed.
/// </exception>
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
@ -49,7 +49,7 @@ internal sealed class StdioClientSessionTransport : StreamClientSessionTransport
if (hasExited)
{
throw new McpTransportException("Transport is not connected", processException);
throw new InvalidOperationException("Transport is not connected", processException);
}
await base.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);

View file

@ -154,7 +154,7 @@ public sealed partial class StdioClientTransport : IClientTransport
if (!processStarted)
{
LogTransportProcessStartFailed(logger, endpointName);
throw new McpTransportException("Failed to start MCP server process");
throw new InvalidOperationException("Failed to start MCP server process");
}
LogTransportProcessStarted(logger, endpointName, process.Id);
@ -176,7 +176,7 @@ public sealed partial class StdioClientTransport : IClientTransport
LogTransportShutdownFailed(logger, endpointName, ex2);
}
throw new McpTransportException("Failed to connect transport", ex);
throw new InvalidOperationException("Failed to connect transport", ex);
}
}

View file

@ -57,28 +57,11 @@ internal class StreamClientSessionTransport : TransportBase
}
/// <inheritdoc/>
/// <remarks>
/// <para>
/// For stream-based transports, this implementation serializes the JSON-RPC message to the
/// underlying output stream. The specific serialization format includes:
/// <list type="bullet">
/// <item>A Content-Length header that specifies the byte length of the JSON message</item>
/// <item>A blank line separator</item>
/// <item>The UTF-8 encoded JSON representation of the message</item>
/// </list>
/// </para>
/// <para>
/// This implementation first checks if the transport is connected and throws a <see cref="McpTransportException"/>
/// if it's not. It then extracts the message ID (if present) for logging purposes, serializes the message,
/// and writes it to the output stream.
/// </para>
/// </remarks>
/// <exception cref="McpTransportException">Thrown when the transport is not connected.</exception>
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
{
if (!IsConnected)
{
throw new McpTransportException("Transport is not connected");
throw new InvalidOperationException("Transport is not connected");
}
string id = "(no id)";
@ -99,7 +82,7 @@ internal class StreamClientSessionTransport : TransportBase
catch (Exception ex)
{
LogTransportSendFailed(Name, id, ex);
throw new McpTransportException("Failed to send message", ex);
throw new InvalidOperationException("Failed to send message", ex);
}
}

View file

@ -60,7 +60,7 @@ public class StreamServerTransport : TransportBase
{
if (!IsConnected)
{
throw new McpTransportException("Transport is not connected");
throw new InvalidOperationException("Transport is not connected");
}
using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false);
@ -80,7 +80,7 @@ public class StreamServerTransport : TransportBase
catch (Exception ex)
{
LogTransportSendFailed(Name, id, ex);
throw new McpTransportException("Failed to send message", ex);
throw new InvalidOperationException("Failed to send message", ex);
}
}

View file

@ -73,7 +73,7 @@ public abstract partial class TransportBase : ITransport
{
if (!IsConnected)
{
throw new McpTransportException("Transport is not connected");
throw new InvalidOperationException("Transport is not connected");
}
await _messageChannel.Writer.WriteAsync(message, cancellationToken).ConfigureAwait(false);