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>
This commit is contained in:
Copilot 2026-02-21 00:08:20 -08:00 • committed by GitHub
commit c2aa6a5123
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 164 additions and 3 deletions

View file

@ -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. |
| `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)). |

View file

@ -35,4 +35,25 @@ internal static class Experimentals
/// URL for the experimental MCP Tasks feature.
/// </summary>
public const string Tasks_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001";
/// <summary>
/// Diagnostic ID for the experimental MCP Extensions feature.
/// </summary>
/// <remarks>
/// This uses the same diagnostic ID as <see cref="Tasks_DiagnosticId"/> 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.
/// </remarks>
public const string Extensions_DiagnosticId = "MCPEXP001";
/// <summary>
/// Message for the experimental MCP Extensions feature.
/// </summary>
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.";
/// <summary>
/// URL for the experimental MCP Extensions feature.
/// </summary>
public const string Extensions_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001";
}

View file

@ -93,4 +93,31 @@ public sealed class ClientCapabilities
[JsonInclude]
[JsonPropertyName("tasks")]
internal McpTasksCapability? TasksCore { get; set; }
/// <summary>
/// Gets or sets optional MCP extensions that the client supports.
/// </summary>
/// <remarks>
/// <para>
/// Keys are extension identifiers in reverse domain notation with an extension name
/// (e.g., <c>"io.modelcontextprotocol/oauth-client-credentials"</c>), and values are
/// per-extension settings objects. An empty object indicates support with no additional settings.
/// </para>
/// <para>
/// Extensions provide a framework for extending the Model Context Protocol while maintaining
/// interoperability. Clients advertise extension support via this field during the initialization handshake.
/// </para>
/// </remarks>
[Experimental(Experimentals.Extensions_DiagnosticId, UrlFormat = Experimentals.Extensions_Url)]
[JsonIgnore]
public IDictionary<string, object>? Extensions
{
get => ExtensionsCore;
set => ExtensionsCore = value;
}
// See ExperimentalInternalPropertyTests.cs before modifying this property.
[JsonInclude]
[JsonPropertyName("extensions")]
internal IDictionary<string, object>? ExtensionsCore { get; set; }
}

View file

@ -92,4 +92,31 @@ public sealed class ServerCapabilities
[JsonInclude]
[JsonPropertyName("tasks")]
internal McpTasksCapability? TasksCore { get; set; }
/// <summary>
/// Gets or sets optional MCP extensions that the server supports.
/// </summary>
/// <remarks>
/// <para>
/// Keys are extension identifiers in reverse domain notation with an extension name
/// (e.g., <c>"io.modelcontextprotocol/apps"</c>), and values are per-extension settings
/// objects. An empty object indicates support with no additional settings.
/// </para>
/// <para>
/// Extensions provide a framework for extending the Model Context Protocol while maintaining
/// interoperability. Servers advertise extension support via this field during the initialization handshake.
/// </para>
/// </remarks>
[Experimental(Experimentals.Extensions_DiagnosticId, UrlFormat = Experimentals.Extensions_Url)]
[JsonIgnore]
public IDictionary<string, object>? Extensions
{
get => ExtensionsCore;
set => ExtensionsCore = value;
}
// See ExperimentalInternalPropertyTests.cs before modifying this property.
[JsonInclude]
[JsonPropertyName("extensions")]
internal IDictionary<string, object>? ExtensionsCore { get; set; }
}

View file

@ -21,7 +21,11 @@ public static class ClientCapabilitiesTests
Form = new FormElicitationCapability(),
Url = new UrlElicitationCapability()
},
Tasks = new McpTasksCapability()
Tasks = new McpTasksCapability(),
Extensions = new Dictionary<string, object>
{
["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<ClientCapabilities>(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<ClientCapabilities>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.NotNull(deserialized.Extensions);
Assert.Empty(deserialized.Extensions);
}
}

View file

@ -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<string, object>
{
["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<ServerCapabilities>(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<ServerCapabilities>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.NotNull(deserialized.Extensions);
Assert.Empty(deserialized.Extensions);
}
}