From ee37372117054b11b43eebcd48451edc1608997e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 08:17:07 +0000 Subject: [PATCH 1/6] Initial plan From 31e2e479f92e045fa542ac7d485c83c81847dd81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 08:22:46 +0000 Subject: [PATCH 2/6] Add MCP Extensions support to ClientCapabilities and ServerCapabilities Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- docs/list-of-diagnostics.md | 2 +- .../Protocol/ClientCapabilities.cs | 20 +++++++++++++++++++ .../Protocol/ServerCapabilities.cs | 20 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index 386b90bc2..573d97fea 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -23,4 +23,4 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T | Diagnostic ID | Description | | :------------ | :---------- | -| `MCPEXP001` | MCP task-related APIs are experimental. Tasks provide a mechanism for asynchronous long-running operations that can be polled for status and results. See [MCP Tasks specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks) for details. | \ No newline at end of file +| `MCPEXP001` | MCP experimental APIs including Tasks and Extensions. Tasks provide a mechanism for asynchronous long-running operations that can be polled for status and results (see [MCP Tasks specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks)). Extensions provide a framework for extending the Model Context Protocol while maintaining interoperability (see [SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)). | \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs index cb85ef5e3..199bd53e3 100644 --- a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using ModelContextProtocol.Client; using ModelContextProtocol.Server; @@ -82,4 +83,23 @@ public sealed class ClientCapabilities /// [JsonPropertyName("tasks")] public McpTasksCapability? Tasks { get; set; } + + /// + /// Gets or sets optional MCP extensions that the client supports. + /// + /// + /// + /// Keys are extension identifiers in reverse domain notation with an extension name + /// (e.g., "io.modelcontextprotocol/oauth-client-credentials"), and values are + /// per-extension settings objects. An empty object indicates support with no additional settings. + /// + /// + /// Extensions provide a framework for extending the Model Context Protocol while maintaining + /// interoperability. Both clients and servers advertise extension support via this field during + /// the initialization handshake. + /// + /// + [JsonPropertyName("extensions")] + [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)] // SEP-2133: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133 + public IDictionary? Extensions { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs index 499819662..a17e8ef63 100644 --- a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using ModelContextProtocol.Server; @@ -81,4 +82,23 @@ public sealed class ServerCapabilities /// [JsonPropertyName("tasks")] public McpTasksCapability? Tasks { get; set; } + + /// + /// Gets or sets optional MCP extensions that the server supports. + /// + /// + /// + /// Keys are extension identifiers in reverse domain notation with an extension name + /// (e.g., "io.modelcontextprotocol/apps"), and values are per-extension settings + /// objects. An empty object indicates support with no additional settings. + /// + /// + /// Extensions provide a framework for extending the Model Context Protocol while maintaining + /// interoperability. Both clients and servers advertise extension support via this field during + /// the initialization handshake. + /// + /// + [JsonPropertyName("extensions")] + [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)] // SEP-2133: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133 + public IDictionary? Extensions { get; set; } } From f6526ba2f4b5632066498f20b6e6e42c24d52c8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 08:41:12 +0000 Subject: [PATCH 3/6] Add dedicated Extensions constants to Experimentals class Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- src/Common/Experimentals.cs | 15 +++++++++++++++ .../Protocol/ClientCapabilities.cs | 2 +- .../Protocol/ServerCapabilities.cs | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Common/Experimentals.cs b/src/Common/Experimentals.cs index 2ccde0796..e38d3deef 100644 --- a/src/Common/Experimentals.cs +++ b/src/Common/Experimentals.cs @@ -35,4 +35,19 @@ internal static class Experimentals /// URL for the experimental MCP Tasks feature. /// public const string Tasks_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001"; + + /// + /// Diagnostic ID for the experimental MCP Extensions feature. + /// + public const string Extensions_DiagnosticId = "MCPEXP001"; + + /// + /// Message for the experimental MCP Extensions feature. + /// + public const string Extensions_Message = "The Extensions feature is experimental per the MCP specification and is subject to change."; + + /// + /// URL for the experimental MCP Extensions feature. + /// + public const string Extensions_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001"; } diff --git a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs index 199bd53e3..132dafc2e 100644 --- a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs @@ -100,6 +100,6 @@ public sealed class ClientCapabilities /// /// [JsonPropertyName("extensions")] - [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)] // SEP-2133: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133 + [Experimental(Experimentals.Extensions_DiagnosticId, UrlFormat = Experimentals.Extensions_Url)] // SEP-2133: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133 public IDictionary? Extensions { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs index a17e8ef63..aec553609 100644 --- a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs @@ -99,6 +99,6 @@ public sealed class ServerCapabilities /// /// [JsonPropertyName("extensions")] - [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)] // SEP-2133: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133 + [Experimental(Experimentals.Extensions_DiagnosticId, UrlFormat = Experimentals.Extensions_Url)] // SEP-2133: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133 public IDictionary? Extensions { get; set; } } From f0153c69a3c68e033e89e06c23d9c46bbf850628 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 08:43:13 +0000 Subject: [PATCH 4/6] Add clarifying documentation for shared diagnostic ID Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- src/Common/Experimentals.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Common/Experimentals.cs b/src/Common/Experimentals.cs index e38d3deef..0268aab84 100644 --- a/src/Common/Experimentals.cs +++ b/src/Common/Experimentals.cs @@ -39,6 +39,12 @@ internal static class Experimentals /// /// Diagnostic ID for the experimental MCP Extensions feature. /// + /// + /// This uses the same diagnostic ID as because both + /// Tasks and Extensions are covered by the same MCPEXP001 diagnostic for experimental + /// MCP features. Having separate constants improves code clarity while maintaining a + /// single diagnostic suppression point. + /// public const string Extensions_DiagnosticId = "MCPEXP001"; /// From b314d3c09ae9025a377abd7f210669a17a81e5f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 08:52:38 +0000 Subject: [PATCH 5/6] Add serialization roundtrip tests for Extensions property and update message Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- src/Common/Experimentals.cs | 2 +- .../Protocol/CapabilitiesTests.cs | 269 ++++++++++++++++++ 2 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 tests/ModelContextProtocol.Tests/Protocol/CapabilitiesTests.cs diff --git a/src/Common/Experimentals.cs b/src/Common/Experimentals.cs index 0268aab84..ec2c7c550 100644 --- a/src/Common/Experimentals.cs +++ b/src/Common/Experimentals.cs @@ -50,7 +50,7 @@ internal static class Experimentals /// /// Message for the experimental MCP Extensions feature. /// - public const string Extensions_Message = "The Extensions feature is experimental per the MCP specification and is subject to change."; + public const string Extensions_Message = "The Extensions feature is part of a future MCP specification version that has not yet been ratified and is subject to change."; /// /// URL for the experimental MCP Extensions feature. diff --git a/tests/ModelContextProtocol.Tests/Protocol/CapabilitiesTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CapabilitiesTests.cs new file mode 100644 index 000000000..875283e7c --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/CapabilitiesTests.cs @@ -0,0 +1,269 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Protocol; + +public static class CapabilitiesTests +{ + [Fact] + public static void ClientCapabilities_ExtensionsProperty_SerializationRoundTrip() + { + // Arrange - Use raw JSON instead of objects for source generation compatibility + string json = """ + { + "extensions": { + "io.modelcontextprotocol/oauth-client-credentials": {}, + "io.modelcontextprotocol/test-extension": { + "setting1": "value1", + "setting2": 42 + } + } + } + """; + + // Act - Deserialize from JSON + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.Extensions); + Assert.Equal(2, deserialized.Extensions.Count); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/oauth-client-credentials")); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/test-extension")); + + // Act - Serialize back to JSON + string roundtrippedJson = JsonSerializer.Serialize(deserialized, McpJsonUtilities.DefaultOptions); + + // Assert - Deserialize again to verify + var deserialized2 = JsonSerializer.Deserialize(roundtrippedJson, McpJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized2); + Assert.NotNull(deserialized2.Extensions); + Assert.Equal(2, deserialized2.Extensions.Count); + } + + [Fact] + public static void ClientCapabilities_ExtensionsProperty_DeserializesCorrectly() + { + // Arrange + string json = """ + { + "extensions": { + "io.modelcontextprotocol/test": {} + } + } + """; + + // Act + var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(capabilities); + Assert.NotNull(capabilities.Extensions); + Assert.Single(capabilities.Extensions); + Assert.True(capabilities.Extensions.ContainsKey("io.modelcontextprotocol/test")); + } + + [Fact] + public static void ClientCapabilities_WithoutExtensions_DeserializesWithNullExtensions() + { + // Arrange + string json = "{}"; + + // Act + var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(capabilities); + Assert.Null(capabilities.Extensions); + } + + [Fact] + public static void ClientCapabilities_WithEmptyExtensions_DeserializesAsEmptyDictionary() + { + // Arrange + string json = """ + { + "extensions": {} + } + """; + + // Act + var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(capabilities); + Assert.NotNull(capabilities.Extensions); + Assert.Empty(capabilities.Extensions); + } + + [Fact] + public static void ServerCapabilities_ExtensionsProperty_SerializationRoundTrip() + { + // Arrange - Use raw JSON instead of objects for source generation compatibility + string json = """ + { + "extensions": { + "io.modelcontextprotocol/apps": {}, + "io.modelcontextprotocol/custom": { + "option": 42, + "enabled": true + } + } + } + """; + + // Act - Deserialize from JSON + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.Extensions); + Assert.Equal(2, deserialized.Extensions.Count); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/apps")); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/custom")); + + // Act - Serialize back to JSON + string roundtrippedJson = JsonSerializer.Serialize(deserialized, McpJsonUtilities.DefaultOptions); + + // Assert - Deserialize again to verify + var deserialized2 = JsonSerializer.Deserialize(roundtrippedJson, McpJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized2); + Assert.NotNull(deserialized2.Extensions); + Assert.Equal(2, deserialized2.Extensions.Count); + } + + [Fact] + public static void ServerCapabilities_ExtensionsProperty_DeserializesCorrectly() + { + // Arrange + string json = """ + { + "extensions": { + "io.modelcontextprotocol/test": {} + } + } + """; + + // Act + var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(capabilities); + Assert.NotNull(capabilities.Extensions); + Assert.Single(capabilities.Extensions); + Assert.True(capabilities.Extensions.ContainsKey("io.modelcontextprotocol/test")); + } + + [Fact] + public static void ServerCapabilities_WithoutExtensions_DeserializesWithNullExtensions() + { + // Arrange + string json = "{}"; + + // Act + var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(capabilities); + Assert.Null(capabilities.Extensions); + } + + [Fact] + public static void ServerCapabilities_WithEmptyExtensions_DeserializesAsEmptyDictionary() + { + // Arrange + string json = """ + { + "extensions": {} + } + """; + + // Act + var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(capabilities); + Assert.NotNull(capabilities.Extensions); + Assert.Empty(capabilities.Extensions); + } + + [Fact] + public static void ClientCapabilities_ExtensionsWithComplexValues_RoundTrips() + { + // Arrange + string json = """ + { + "extensions": { + "io.modelcontextprotocol/complex": { + "stringValue": "test", + "numberValue": 123, + "boolValue": true, + "arrayValue": [1, 2, 3] + } + } + } + """; + + // Act - Deserialize from JSON + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.Extensions); + Assert.Single(deserialized.Extensions); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/complex")); + + // Verify the complex value can be accessed as JsonElement + var complexValue = deserialized.Extensions["io.modelcontextprotocol/complex"]; + Assert.NotNull(complexValue); + + // Act - Serialize back to JSON + string roundtrippedJson = JsonSerializer.Serialize(deserialized, McpJsonUtilities.DefaultOptions); + + // Assert - Verify it can deserialize again + var deserialized2 = JsonSerializer.Deserialize(roundtrippedJson, McpJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized2); + Assert.NotNull(deserialized2.Extensions); + Assert.Single(deserialized2.Extensions); + } + + [Fact] + public static void ServerCapabilities_ExtensionsWithComplexValues_RoundTrips() + { + // Arrange + string json = """ + { + "extensions": { + "io.modelcontextprotocol/complex": { + "stringValue": "test", + "numberValue": 456, + "boolValue": false, + "arrayValue": ["a", "b", "c"] + } + } + } + """; + + // Act - Deserialize from JSON + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.Extensions); + Assert.Single(deserialized.Extensions); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/complex")); + + // Verify the complex value can be accessed as JsonElement + var complexValue = deserialized.Extensions["io.modelcontextprotocol/complex"]; + Assert.NotNull(complexValue); + + // Act - Serialize back to JSON + string roundtrippedJson = JsonSerializer.Serialize(deserialized, McpJsonUtilities.DefaultOptions); + + // Assert - Verify it can deserialize again + var deserialized2 = JsonSerializer.Deserialize(roundtrippedJson, McpJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized2); + Assert.NotNull(deserialized2.Extensions); + Assert.Single(deserialized2.Extensions); + } +} From b19aefda8cce5f5525b7eb8786501b5dd0fa71fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:18:53 +0000 Subject: [PATCH 6/6] Split capabilities tests into separate files by type Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Protocol/CapabilitiesTests.cs | 269 ------------------ .../Protocol/ClientCapabilitiesTests.cs | 138 +++++++++ .../Protocol/ServerCapabilitiesTests.cs | 138 +++++++++ 3 files changed, 276 insertions(+), 269 deletions(-) delete mode 100644 tests/ModelContextProtocol.Tests/Protocol/CapabilitiesTests.cs create mode 100644 tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs create mode 100644 tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs diff --git a/tests/ModelContextProtocol.Tests/Protocol/CapabilitiesTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CapabilitiesTests.cs deleted file mode 100644 index 875283e7c..000000000 --- a/tests/ModelContextProtocol.Tests/Protocol/CapabilitiesTests.cs +++ /dev/null @@ -1,269 +0,0 @@ -using ModelContextProtocol.Protocol; -using System.Text.Json; - -namespace ModelContextProtocol.Tests.Protocol; - -public static class CapabilitiesTests -{ - [Fact] - public static void ClientCapabilities_ExtensionsProperty_SerializationRoundTrip() - { - // Arrange - Use raw JSON instead of objects for source generation compatibility - string json = """ - { - "extensions": { - "io.modelcontextprotocol/oauth-client-credentials": {}, - "io.modelcontextprotocol/test-extension": { - "setting1": "value1", - "setting2": 42 - } - } - } - """; - - // Act - Deserialize from JSON - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - Assert.NotNull(deserialized.Extensions); - Assert.Equal(2, deserialized.Extensions.Count); - Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/oauth-client-credentials")); - Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/test-extension")); - - // Act - Serialize back to JSON - string roundtrippedJson = JsonSerializer.Serialize(deserialized, McpJsonUtilities.DefaultOptions); - - // Assert - Deserialize again to verify - var deserialized2 = JsonSerializer.Deserialize(roundtrippedJson, McpJsonUtilities.DefaultOptions); - Assert.NotNull(deserialized2); - Assert.NotNull(deserialized2.Extensions); - Assert.Equal(2, deserialized2.Extensions.Count); - } - - [Fact] - public static void ClientCapabilities_ExtensionsProperty_DeserializesCorrectly() - { - // Arrange - string json = """ - { - "extensions": { - "io.modelcontextprotocol/test": {} - } - } - """; - - // Act - var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(capabilities); - Assert.NotNull(capabilities.Extensions); - Assert.Single(capabilities.Extensions); - Assert.True(capabilities.Extensions.ContainsKey("io.modelcontextprotocol/test")); - } - - [Fact] - public static void ClientCapabilities_WithoutExtensions_DeserializesWithNullExtensions() - { - // Arrange - string json = "{}"; - - // Act - var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(capabilities); - Assert.Null(capabilities.Extensions); - } - - [Fact] - public static void ClientCapabilities_WithEmptyExtensions_DeserializesAsEmptyDictionary() - { - // Arrange - string json = """ - { - "extensions": {} - } - """; - - // Act - var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(capabilities); - Assert.NotNull(capabilities.Extensions); - Assert.Empty(capabilities.Extensions); - } - - [Fact] - public static void ServerCapabilities_ExtensionsProperty_SerializationRoundTrip() - { - // Arrange - Use raw JSON instead of objects for source generation compatibility - string json = """ - { - "extensions": { - "io.modelcontextprotocol/apps": {}, - "io.modelcontextprotocol/custom": { - "option": 42, - "enabled": true - } - } - } - """; - - // Act - Deserialize from JSON - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - Assert.NotNull(deserialized.Extensions); - Assert.Equal(2, deserialized.Extensions.Count); - Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/apps")); - Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/custom")); - - // Act - Serialize back to JSON - string roundtrippedJson = JsonSerializer.Serialize(deserialized, McpJsonUtilities.DefaultOptions); - - // Assert - Deserialize again to verify - var deserialized2 = JsonSerializer.Deserialize(roundtrippedJson, McpJsonUtilities.DefaultOptions); - Assert.NotNull(deserialized2); - Assert.NotNull(deserialized2.Extensions); - Assert.Equal(2, deserialized2.Extensions.Count); - } - - [Fact] - public static void ServerCapabilities_ExtensionsProperty_DeserializesCorrectly() - { - // Arrange - string json = """ - { - "extensions": { - "io.modelcontextprotocol/test": {} - } - } - """; - - // Act - var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(capabilities); - Assert.NotNull(capabilities.Extensions); - Assert.Single(capabilities.Extensions); - Assert.True(capabilities.Extensions.ContainsKey("io.modelcontextprotocol/test")); - } - - [Fact] - public static void ServerCapabilities_WithoutExtensions_DeserializesWithNullExtensions() - { - // Arrange - string json = "{}"; - - // Act - var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(capabilities); - Assert.Null(capabilities.Extensions); - } - - [Fact] - public static void ServerCapabilities_WithEmptyExtensions_DeserializesAsEmptyDictionary() - { - // Arrange - string json = """ - { - "extensions": {} - } - """; - - // Act - var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(capabilities); - Assert.NotNull(capabilities.Extensions); - Assert.Empty(capabilities.Extensions); - } - - [Fact] - public static void ClientCapabilities_ExtensionsWithComplexValues_RoundTrips() - { - // Arrange - string json = """ - { - "extensions": { - "io.modelcontextprotocol/complex": { - "stringValue": "test", - "numberValue": 123, - "boolValue": true, - "arrayValue": [1, 2, 3] - } - } - } - """; - - // Act - Deserialize from JSON - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - Assert.NotNull(deserialized.Extensions); - Assert.Single(deserialized.Extensions); - Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/complex")); - - // Verify the complex value can be accessed as JsonElement - var complexValue = deserialized.Extensions["io.modelcontextprotocol/complex"]; - Assert.NotNull(complexValue); - - // Act - Serialize back to JSON - string roundtrippedJson = JsonSerializer.Serialize(deserialized, McpJsonUtilities.DefaultOptions); - - // Assert - Verify it can deserialize again - var deserialized2 = JsonSerializer.Deserialize(roundtrippedJson, McpJsonUtilities.DefaultOptions); - Assert.NotNull(deserialized2); - Assert.NotNull(deserialized2.Extensions); - Assert.Single(deserialized2.Extensions); - } - - [Fact] - public static void ServerCapabilities_ExtensionsWithComplexValues_RoundTrips() - { - // Arrange - string json = """ - { - "extensions": { - "io.modelcontextprotocol/complex": { - "stringValue": "test", - "numberValue": 456, - "boolValue": false, - "arrayValue": ["a", "b", "c"] - } - } - } - """; - - // Act - Deserialize from JSON - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - Assert.NotNull(deserialized.Extensions); - Assert.Single(deserialized.Extensions); - Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/complex")); - - // Verify the complex value can be accessed as JsonElement - var complexValue = deserialized.Extensions["io.modelcontextprotocol/complex"]; - Assert.NotNull(complexValue); - - // Act - Serialize back to JSON - string roundtrippedJson = JsonSerializer.Serialize(deserialized, McpJsonUtilities.DefaultOptions); - - // Assert - Verify it can deserialize again - var deserialized2 = JsonSerializer.Deserialize(roundtrippedJson, McpJsonUtilities.DefaultOptions); - Assert.NotNull(deserialized2); - Assert.NotNull(deserialized2.Extensions); - Assert.Single(deserialized2.Extensions); - } -} diff --git a/tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs new file mode 100644 index 000000000..b7b2766a7 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs @@ -0,0 +1,138 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Protocol; + +public static class ClientCapabilitiesTests +{ + [Fact] + public static void ExtensionsProperty_SerializationRoundTrip() + { + // Arrange - Use raw JSON instead of objects for source generation compatibility + string json = """ + { + "extensions": { + "io.modelcontextprotocol/oauth-client-credentials": {}, + "io.modelcontextprotocol/test-extension": { + "setting1": "value1", + "setting2": 42 + } + } + } + """; + + // Act - Deserialize from JSON + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.Extensions); + Assert.Equal(2, deserialized.Extensions.Count); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/oauth-client-credentials")); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/test-extension")); + + // Act - Serialize back to JSON + string roundtrippedJson = JsonSerializer.Serialize(deserialized, McpJsonUtilities.DefaultOptions); + + // Assert - Deserialize again to verify + var deserialized2 = JsonSerializer.Deserialize(roundtrippedJson, McpJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized2); + Assert.NotNull(deserialized2.Extensions); + Assert.Equal(2, deserialized2.Extensions.Count); + } + + [Fact] + public static void ExtensionsProperty_DeserializesCorrectly() + { + // Arrange + string json = """ + { + "extensions": { + "io.modelcontextprotocol/test": {} + } + } + """; + + // Act + var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(capabilities); + Assert.NotNull(capabilities.Extensions); + Assert.Single(capabilities.Extensions); + Assert.True(capabilities.Extensions.ContainsKey("io.modelcontextprotocol/test")); + } + + [Fact] + public static void WithoutExtensions_DeserializesWithNullExtensions() + { + // Arrange + string json = "{}"; + + // Act + var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(capabilities); + Assert.Null(capabilities.Extensions); + } + + [Fact] + public static void WithEmptyExtensions_DeserializesAsEmptyDictionary() + { + // Arrange + string json = """ + { + "extensions": {} + } + """; + + // Act + var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(capabilities); + Assert.NotNull(capabilities.Extensions); + Assert.Empty(capabilities.Extensions); + } + + [Fact] + public static void ExtensionsWithComplexValues_RoundTrips() + { + // Arrange + string json = """ + { + "extensions": { + "io.modelcontextprotocol/complex": { + "stringValue": "test", + "numberValue": 123, + "boolValue": true, + "arrayValue": [1, 2, 3] + } + } + } + """; + + // Act - Deserialize from JSON + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.Extensions); + Assert.Single(deserialized.Extensions); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/complex")); + + // Verify the complex value can be accessed as JsonElement + var complexValue = deserialized.Extensions["io.modelcontextprotocol/complex"]; + Assert.NotNull(complexValue); + + // Act - Serialize back to JSON + string roundtrippedJson = JsonSerializer.Serialize(deserialized, McpJsonUtilities.DefaultOptions); + + // Assert - Verify it can deserialize again + var deserialized2 = JsonSerializer.Deserialize(roundtrippedJson, McpJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized2); + Assert.NotNull(deserialized2.Extensions); + Assert.Single(deserialized2.Extensions); + } +} diff --git a/tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs new file mode 100644 index 000000000..30f1e7bf7 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs @@ -0,0 +1,138 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Protocol; + +public static class ServerCapabilitiesTests +{ + [Fact] + public static void ExtensionsProperty_SerializationRoundTrip() + { + // Arrange - Use raw JSON instead of objects for source generation compatibility + string json = """ + { + "extensions": { + "io.modelcontextprotocol/apps": {}, + "io.modelcontextprotocol/custom": { + "option": 42, + "enabled": true + } + } + } + """; + + // Act - Deserialize from JSON + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.Extensions); + Assert.Equal(2, deserialized.Extensions.Count); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/apps")); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/custom")); + + // Act - Serialize back to JSON + string roundtrippedJson = JsonSerializer.Serialize(deserialized, McpJsonUtilities.DefaultOptions); + + // Assert - Deserialize again to verify + var deserialized2 = JsonSerializer.Deserialize(roundtrippedJson, McpJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized2); + Assert.NotNull(deserialized2.Extensions); + Assert.Equal(2, deserialized2.Extensions.Count); + } + + [Fact] + public static void ExtensionsProperty_DeserializesCorrectly() + { + // Arrange + string json = """ + { + "extensions": { + "io.modelcontextprotocol/test": {} + } + } + """; + + // Act + var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(capabilities); + Assert.NotNull(capabilities.Extensions); + Assert.Single(capabilities.Extensions); + Assert.True(capabilities.Extensions.ContainsKey("io.modelcontextprotocol/test")); + } + + [Fact] + public static void WithoutExtensions_DeserializesWithNullExtensions() + { + // Arrange + string json = "{}"; + + // Act + var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(capabilities); + Assert.Null(capabilities.Extensions); + } + + [Fact] + public static void WithEmptyExtensions_DeserializesAsEmptyDictionary() + { + // Arrange + string json = """ + { + "extensions": {} + } + """; + + // Act + var capabilities = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(capabilities); + Assert.NotNull(capabilities.Extensions); + Assert.Empty(capabilities.Extensions); + } + + [Fact] + public static void ExtensionsWithComplexValues_RoundTrips() + { + // Arrange + string json = """ + { + "extensions": { + "io.modelcontextprotocol/complex": { + "stringValue": "test", + "numberValue": 456, + "boolValue": false, + "arrayValue": ["a", "b", "c"] + } + } + } + """; + + // Act - Deserialize from JSON + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.Extensions); + Assert.Single(deserialized.Extensions); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/complex")); + + // Verify the complex value can be accessed as JsonElement + var complexValue = deserialized.Extensions["io.modelcontextprotocol/complex"]; + Assert.NotNull(complexValue); + + // Act - Serialize back to JSON + string roundtrippedJson = JsonSerializer.Serialize(deserialized, McpJsonUtilities.DefaultOptions); + + // Assert - Verify it can deserialize again + var deserialized2 = JsonSerializer.Deserialize(roundtrippedJson, McpJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized2); + Assert.NotNull(deserialized2.Extensions); + Assert.Single(deserialized2.Extensions); + } +}