From c2aa6a5123fa9b2c7347f79c15219c1ba7cfea3a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 00:08:20 -0800 Subject: [PATCH] Add Extensions support to ClientCapabilities and ServerCapabilities (#1317) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- docs/list-of-diagnostics.md | 2 +- src/Common/Experimentals.cs | 21 +++++++++ .../Protocol/ClientCapabilities.cs | 27 +++++++++++ .../Protocol/ServerCapabilities.cs | 27 +++++++++++ .../Protocol/ClientCapabilitiesTests.cs | 45 ++++++++++++++++++- .../Protocol/ServerCapabilitiesTests.cs | 45 ++++++++++++++++++- 6 files changed, 164 insertions(+), 3 deletions(-) diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index 386b90b..573d97f 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/Common/Experimentals.cs b/src/Common/Experimentals.cs index 2ccde07..ec2c7c5 100644 --- a/src/Common/Experimentals.cs +++ b/src/Common/Experimentals.cs @@ -35,4 +35,25 @@ 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. + /// + /// + /// 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"; + + /// + /// Message for the experimental MCP Extensions feature. + /// + 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. + /// + 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 9841e3d..77b2bef 100644 --- a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs @@ -93,4 +93,31 @@ public sealed class ClientCapabilities [JsonInclude] [JsonPropertyName("tasks")] internal McpTasksCapability? TasksCore { 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. Clients advertise extension support via this field during the initialization handshake. + /// + /// + [Experimental(Experimentals.Extensions_DiagnosticId, UrlFormat = Experimentals.Extensions_Url)] + [JsonIgnore] + public IDictionary? Extensions + { + get => ExtensionsCore; + set => ExtensionsCore = value; + } + + // See ExperimentalInternalPropertyTests.cs before modifying this property. + [JsonInclude] + [JsonPropertyName("extensions")] + internal IDictionary? ExtensionsCore { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs index d4e5565..d4e23a6 100644 --- a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs @@ -92,4 +92,31 @@ public sealed class ServerCapabilities [JsonInclude] [JsonPropertyName("tasks")] internal McpTasksCapability? TasksCore { 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. Servers advertise extension support via this field during the initialization handshake. + /// + /// + [Experimental(Experimentals.Extensions_DiagnosticId, UrlFormat = Experimentals.Extensions_Url)] + [JsonIgnore] + public IDictionary? Extensions + { + get => ExtensionsCore; + set => ExtensionsCore = value; + } + + // See ExperimentalInternalPropertyTests.cs before modifying this property. + [JsonInclude] + [JsonPropertyName("extensions")] + internal IDictionary? ExtensionsCore { get; set; } } diff --git a/tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs index 188326a..cacb7e8 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs @@ -21,7 +21,11 @@ public static class ClientCapabilitiesTests Form = new FormElicitationCapability(), Url = new UrlElicitationCapability() }, - Tasks = new McpTasksCapability() + Tasks = new McpTasksCapability(), + Extensions = new Dictionary + { + ["io.modelcontextprotocol/test"] = new object() + } }; string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions); @@ -37,6 +41,8 @@ public static class ClientCapabilitiesTests Assert.NotNull(deserialized.Elicitation.Form); Assert.NotNull(deserialized.Elicitation.Url); Assert.NotNull(deserialized.Tasks); + Assert.NotNull(deserialized.Extensions); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/test")); } [Fact] @@ -53,5 +59,42 @@ public static class ClientCapabilitiesTests Assert.Null(deserialized.Sampling); Assert.Null(deserialized.Elicitation); Assert.Null(deserialized.Tasks); + Assert.Null(deserialized.Extensions); + } + + [Fact] + public static void ClientCapabilities_Extensions_DeserializesFromJson() + { + string json = """ + { + "extensions": { + "io.modelcontextprotocol/oauth-client-credentials": {}, + "io.modelcontextprotocol/test-extension": { + "setting1": "value1", + "setting2": 42 + } + } + } + """; + + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + 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")); + } + + [Fact] + public static void ClientCapabilities_Extensions_EmptyObjectDeserializesAsEmptyDictionary() + { + string json = """{"extensions": {}}"""; + + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.Extensions); + Assert.Empty(deserialized.Extensions); } } diff --git a/tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs index ca698ef..a6f8265 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs @@ -15,7 +15,11 @@ public static class ServerCapabilitiesTests Resources = new ResourcesCapability { Subscribe = true, ListChanged = true }, Tools = new ToolsCapability { ListChanged = false }, Completions = new CompletionsCapability(), - Tasks = new McpTasksCapability() + Tasks = new McpTasksCapability(), + Extensions = new Dictionary + { + ["io.modelcontextprotocol/apps"] = new object() + } }; string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions); @@ -32,6 +36,8 @@ public static class ServerCapabilitiesTests Assert.False(deserialized.Tools.ListChanged); Assert.NotNull(deserialized.Completions); Assert.NotNull(deserialized.Tasks); + Assert.NotNull(deserialized.Extensions); + Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/apps")); } [Fact] @@ -50,5 +56,42 @@ public static class ServerCapabilitiesTests Assert.Null(deserialized.Tools); Assert.Null(deserialized.Completions); Assert.Null(deserialized.Tasks); + Assert.Null(deserialized.Extensions); + } + + [Fact] + public static void ServerCapabilities_Extensions_DeserializesFromJson() + { + string json = """ + { + "extensions": { + "io.modelcontextprotocol/apps": {}, + "io.modelcontextprotocol/custom": { + "option": 42, + "enabled": true + } + } + } + """; + + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + 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")); + } + + [Fact] + public static void ServerCapabilities_Extensions_EmptyObjectDeserializesAsEmptyDictionary() + { + string json = """{"extensions": {}}"""; + + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.Extensions); + Assert.Empty(deserialized.Extensions); } }