From db0f4d13b4c9780e237d2c2e26b134130de8ddd7 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 15:37:54 -0800
Subject: [PATCH] Add support for `data` to `McpProtocolException` (#1028)
---
.../McpSessionHandler.cs | 86 +++++++++-
.../McpProtocolExceptionDataTests.cs | 160 ++++++++++++++++++
.../PlatformDetection.cs | 9 +
.../Server/McpServerTests.cs | 58 +++++++
4 files changed, 311 insertions(+), 2 deletions(-)
create mode 100644 tests/ModelContextProtocol.Tests/McpProtocolExceptionDataTests.cs
diff --git a/src/ModelContextProtocol.Core/McpSessionHandler.cs b/src/ModelContextProtocol.Core/McpSessionHandler.cs
index fcd7980..78de23a 100644
--- a/src/ModelContextProtocol.Core/McpSessionHandler.cs
+++ b/src/ModelContextProtocol.Core/McpSessionHandler.cs
@@ -186,10 +186,10 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
{
Code = (int)mcpProtocolException.ErrorCode,
Message = mcpProtocolException.Message,
+ Data = ConvertExceptionData(mcpProtocolException.Data),
} : ex is McpException mcpException ?
new()
{
-
Code = (int)McpErrorCode.InternalError,
Message = mcpException.Message,
} :
@@ -206,6 +206,7 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
Error = detail,
Context = new JsonRpcMessageContext { RelatedTransport = request.Context?.RelatedTransport },
};
+
await SendMessageAsync(errorMessage, cancellationToken).ConfigureAwait(false);
}
else if (ex is not OperationCanceledException)
@@ -452,7 +453,40 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
if (response is JsonRpcError error)
{
LogSendingRequestFailed(EndpointName, request.Method, error.Error.Message, error.Error.Code);
- throw new McpProtocolException($"Request failed (remote): {error.Error.Message}", (McpErrorCode)error.Error.Code);
+ var exception = new McpProtocolException($"Request failed (remote): {error.Error.Message}", (McpErrorCode)error.Error.Code);
+
+ // Populate exception.Data with the error data if present.
+ // When deserializing JSON, Data will be a JsonElement.
+ // We extract primitive values (strings, numbers, bools) for broader compatibility,
+ // as JsonElement is not [Serializable] and cannot be stored in Exception.Data on .NET Framework.
+ if (error.Error.Data is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Object)
+ {
+ foreach (var property in jsonElement.EnumerateObject())
+ {
+ object? value = property.Value.ValueKind switch
+ {
+ JsonValueKind.String => property.Value.GetString(),
+ JsonValueKind.Number => property.Value.GetDouble(),
+ JsonValueKind.True => true,
+ JsonValueKind.False => false,
+ JsonValueKind.Null => null,
+#if NET
+ // Objects and arrays are stored as JsonElement on .NET Core only
+ _ => property.Value,
+#else
+ // Skip objects/arrays on .NET Framework as JsonElement is not serializable
+ _ => (object?)null,
+#endif
+ };
+
+ if (value is not null || property.Value.ValueKind == JsonValueKind.Null)
+ {
+ exception.Data[property.Name] = value;
+ }
+ }
+ }
+
+ throw exception;
}
if (response is JsonRpcResponse success)
@@ -769,6 +803,54 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
return null;
}
+ ///
+ /// Converts the dictionary to a serializable .
+ /// Returns null if the data dictionary is empty or contains no string keys with serializable values.
+ ///
+ ///
+ ///
+ /// Only entries with string keys are included in the result. Entries with non-string keys are ignored.
+ ///
+ ///
+ /// Each value is serialized to a to ensure it can be safely included in the
+ /// JSON-RPC error response. Values that cannot be serialized are silently skipped.
+ ///
+ ///
+ private static Dictionary? ConvertExceptionData(System.Collections.IDictionary data)
+ {
+ if (data.Count == 0)
+ {
+ return null;
+ }
+
+ var typeInfo = McpJsonUtilities.DefaultOptions.GetTypeInfo