Skip to content

Clean up (use of) McpException and McpTransportException #321

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion samples/EverythingServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ await ctx.Server.RequestSamplingAsync([
{
if (ctx.Params?.Level is null)
{
throw new McpException("Missing required argument 'level'");
throw new McpException("Missing required argument 'level'", McpErrorCode.InvalidParams);
}

_minimumLoggingLevel = ctx.Params.Level;
Expand Down
4 changes: 2 additions & 2 deletions src/ModelContextProtocol/Client/McpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,10 @@ await SendMessageAsync(
new JsonRpcNotification { Method = NotificationMethods.InitializedNotification },
initializationCts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested)
catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
LogClientInitializationTimeout(EndpointName);
throw new McpException("Initialization timed out", oce);
throw new TimeoutException("Initialization timed out", oce);
}
}
catch (Exception e)
Expand Down
49 changes: 49 additions & 0 deletions src/ModelContextProtocol/McpErrorCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace ModelContextProtocol;

/// <summary>
/// Represents standard JSON-RPC error codes as defined in the MCP specification.
/// </summary>
public enum McpErrorCode
{
/// <summary>
/// Indicates that the JSON received could not be parsed.
/// </summary>
/// <remarks>
/// This error occurs when the input contains malformed JSON or incorrect syntax.
/// </remarks>
ParseError = -32700,

/// <summary>
/// Indicates that the JSON payload does not conform to the expected Request object structure.
/// </summary>
/// <remarks>
/// The request is considered invalid if it lacks required fields or fails to follow the JSON-RPC protocol.
/// </remarks>
InvalidRequest = -32600,

/// <summary>
/// Indicates that the requested method does not exist or is not available on the server.
/// </summary>
/// <remarks>
/// This error is returned when the method name specified in the request cannot be found.
/// </remarks>
MethodNotFound = -32601,

/// <summary>
/// Indicates that one or more parameters provided in the request are invalid.
/// </summary>
/// <remarks>
/// This error is returned when the parameters do not match the expected method signature or constraints.
/// This includes cases where required parameters are missing or not understood, such as when a name for
/// a tool or prompt is not recognized.
/// </remarks>
InvalidParams = -32602,

/// <summary>
/// Indicates that an internal error occurred while processing the request.
/// </summary>
/// <remarks>
/// This error is used when the endpoint encounters an unexpected condition that prevents it from fulfilling the request.
/// </remarks>
InternalError = -32603,
}
24 changes: 14 additions & 10 deletions src/ModelContextProtocol/McpException.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
namespace ModelContextProtocol;

/// <summary>
/// Represents an exception that is thrown when a Model Context Protocol (MCP) error occurs.
/// Represents an exception that is thrown when an Model Context Protocol (MCP) error occurs.
/// </summary>
/// <remarks>
/// This exception is used to represent failures to do with protocol-level concerns, such as invalid JSON-RPC requests,
/// invalid parameters, or internal errors. It is not intended to be used for application-level errors.
/// <see cref="Exception.Message"/> or <see cref="ErrorCode"/> from a <see cref="McpException"/> may be
/// propagated to the remote endpoint; sensitive information should not be included. If sensitive details need
/// to be included, a different exception type should be used.
/// </remarks>
public class McpException : Exception
{
/// <summary>
Expand Down Expand Up @@ -33,8 +40,8 @@ public McpException(string message, Exception? innerException) : base(message, i
/// Initializes a new instance of the <see cref="McpException"/> class with a specified error message and JSON-RPC error code.
/// </summary>
/// <param name="message">The message that describes the error.</param>
/// <param name="errorCode">A JSON-RPC error code from <see cref="Protocol.Messages.ErrorCodes"/> class.</param>
public McpException(string message, int? errorCode) : this(message, null, errorCode)
/// <param name="errorCode">A <see cref="McpErrorCode"/>.</param>
public McpException(string message, McpErrorCode errorCode) : this(message, null, errorCode)
{
}

Expand All @@ -43,18 +50,15 @@ public McpException(string message, int? errorCode) : this(message, null, errorC
/// </summary>
/// <param name="message">The message that describes the error.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
/// <param name="errorCode">A JSON-RPC error code from <see cref="Protocol.Messages.ErrorCodes"/> class.</param>
public McpException(string message, Exception? innerException, int? errorCode) : base(message, innerException)
/// <param name="errorCode">A <see cref="McpErrorCode"/>.</param>
public McpException(string message, Exception? innerException, McpErrorCode errorCode) : base(message, innerException)
{
ErrorCode = errorCode;
}

/// <summary>
/// Gets the JSON-RPC error code associated with this exception.
/// Gets the error code associated with this exception.
/// </summary>
/// <value>
/// A standard JSON-RPC error code, or <see langword="null"/> if the exception wasn't caused by a JSON-RPC error.
/// </value>
/// <remarks>
/// This property contains a standard JSON-RPC error code as defined in the MCP specification. Common error codes include:
/// <list type="bullet">
Expand All @@ -65,5 +69,5 @@ public McpException(string message, Exception? innerException, int? errorCode) :
/// <item><description>-32603: Internal error - Internal JSON-RPC error</description></item>
/// </list>
/// </remarks>
public int? ErrorCode { get; }
public McpErrorCode ErrorCode { get; } = McpErrorCode.InternalError;
}
32 changes: 0 additions & 32 deletions src/ModelContextProtocol/Protocol/Messages/ErrorCodes.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)

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);
}
}

Expand Down Expand Up @@ -110,7 +110,7 @@ public override async Task SendMessageAsync(
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);
Expand All @@ -136,7 +136,7 @@ public override async Task SendMessageAsync(
LogRejectedPost(Name, messageId);
}

throw new McpTransportException("Failed to send message");
throw new InvalidOperationException("Failed to send message");
}
}

Expand Down Expand Up @@ -273,34 +273,18 @@ private async Task ProcessSseMessage(string data, CancellationToken cancellation

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ public StdioClientSessionTransport(StdioClientTransportOptions options, Process
/// <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)
Expand All @@ -49,7 +49,7 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public async Task<ITransport> ConnectAsync(CancellationToken cancellationToken =
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);
Expand All @@ -176,7 +176,7 @@ public async Task<ITransport> ConnectAsync(CancellationToken cancellationToken =
LogTransportShutdownFailed(logger, endpointName, ex2);
}

throw new McpTransportException("Failed to connect transport", ex);
throw new InvalidOperationException("Failed to connect transport", ex);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,11 @@ public StreamClientSessionTransport(
}

/// <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)";
Expand All @@ -99,7 +82,7 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio
catch (Exception ex)
{
LogTransportSendFailed(Name, id, ex);
throw new McpTransportException("Failed to send message", ex);
throw new InvalidOperationException("Failed to send message", ex);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio
{
if (!IsConnected)
{
throw new McpTransportException("Transport is not connected");
throw new InvalidOperationException("Transport is not connected");
}

using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false);
Expand All @@ -80,7 +80,7 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio
catch (Exception ex)
{
LogTransportSendFailed(Name, id, ex);
throw new McpTransportException("Failed to send message", ex);
throw new InvalidOperationException("Failed to send message", ex);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ protected async Task WriteMessageAsync(IJsonRpcMessage message, CancellationToke
{
if (!IsConnected)
{
throw new McpTransportException("Transport is not connected");
throw new InvalidOperationException("Transport is not connected");
}

await _messageChannel.Writer.WriteAsync(message, cancellationToken).ConfigureAwait(false);
Expand Down
Loading