Skip to content

Commit c2aa6a5

Browse files
Copilotstephentoub
andauthored
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>
1 parent 79b2dbe commit c2aa6a5

File tree

6 files changed

+164
-3
lines changed

6 files changed

+164
-3
lines changed

docs/list-of-diagnostics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T
2323

2424
| Diagnostic ID | Description |
2525
| :------------ | :---------- |
26-
| `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. |
26+
| `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)). |

src/Common/Experimentals.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,25 @@ internal static class Experimentals
3535
/// URL for the experimental MCP Tasks feature.
3636
/// </summary>
3737
public const string Tasks_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001";
38+
39+
/// <summary>
40+
/// Diagnostic ID for the experimental MCP Extensions feature.
41+
/// </summary>
42+
/// <remarks>
43+
/// This uses the same diagnostic ID as <see cref="Tasks_DiagnosticId"/> because both
44+
/// Tasks and Extensions are covered by the same MCPEXP001 diagnostic for experimental
45+
/// MCP features. Having separate constants improves code clarity while maintaining a
46+
/// single diagnostic suppression point.
47+
/// </remarks>
48+
public const string Extensions_DiagnosticId = "MCPEXP001";
49+
50+
/// <summary>
51+
/// Message for the experimental MCP Extensions feature.
52+
/// </summary>
53+
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.";
54+
55+
/// <summary>
56+
/// URL for the experimental MCP Extensions feature.
57+
/// </summary>
58+
public const string Extensions_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001";
3859
}

src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,31 @@ public McpTasksCapability? Tasks
9393
[JsonInclude]
9494
[JsonPropertyName("tasks")]
9595
internal McpTasksCapability? TasksCore { get; set; }
96+
97+
/// <summary>
98+
/// Gets or sets optional MCP extensions that the client supports.
99+
/// </summary>
100+
/// <remarks>
101+
/// <para>
102+
/// Keys are extension identifiers in reverse domain notation with an extension name
103+
/// (e.g., <c>"io.modelcontextprotocol/oauth-client-credentials"</c>), and values are
104+
/// per-extension settings objects. An empty object indicates support with no additional settings.
105+
/// </para>
106+
/// <para>
107+
/// Extensions provide a framework for extending the Model Context Protocol while maintaining
108+
/// interoperability. Clients advertise extension support via this field during the initialization handshake.
109+
/// </para>
110+
/// </remarks>
111+
[Experimental(Experimentals.Extensions_DiagnosticId, UrlFormat = Experimentals.Extensions_Url)]
112+
[JsonIgnore]
113+
public IDictionary<string, object>? Extensions
114+
{
115+
get => ExtensionsCore;
116+
set => ExtensionsCore = value;
117+
}
118+
119+
// See ExperimentalInternalPropertyTests.cs before modifying this property.
120+
[JsonInclude]
121+
[JsonPropertyName("extensions")]
122+
internal IDictionary<string, object>? ExtensionsCore { get; set; }
96123
}

src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,31 @@ public McpTasksCapability? Tasks
9292
[JsonInclude]
9393
[JsonPropertyName("tasks")]
9494
internal McpTasksCapability? TasksCore { get; set; }
95+
96+
/// <summary>
97+
/// Gets or sets optional MCP extensions that the server supports.
98+
/// </summary>
99+
/// <remarks>
100+
/// <para>
101+
/// Keys are extension identifiers in reverse domain notation with an extension name
102+
/// (e.g., <c>"io.modelcontextprotocol/apps"</c>), and values are per-extension settings
103+
/// objects. An empty object indicates support with no additional settings.
104+
/// </para>
105+
/// <para>
106+
/// Extensions provide a framework for extending the Model Context Protocol while maintaining
107+
/// interoperability. Servers advertise extension support via this field during the initialization handshake.
108+
/// </para>
109+
/// </remarks>
110+
[Experimental(Experimentals.Extensions_DiagnosticId, UrlFormat = Experimentals.Extensions_Url)]
111+
[JsonIgnore]
112+
public IDictionary<string, object>? Extensions
113+
{
114+
get => ExtensionsCore;
115+
set => ExtensionsCore = value;
116+
}
117+
118+
// See ExperimentalInternalPropertyTests.cs before modifying this property.
119+
[JsonInclude]
120+
[JsonPropertyName("extensions")]
121+
internal IDictionary<string, object>? ExtensionsCore { get; set; }
95122
}

tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ public static void ClientCapabilities_SerializationRoundTrip_PreservesAllPropert
2121
Form = new FormElicitationCapability(),
2222
Url = new UrlElicitationCapability()
2323
},
24-
Tasks = new McpTasksCapability()
24+
Tasks = new McpTasksCapability(),
25+
Extensions = new Dictionary<string, object>
26+
{
27+
["io.modelcontextprotocol/test"] = new object()
28+
}
2529
};
2630

2731
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
@@ -37,6 +41,8 @@ public static void ClientCapabilities_SerializationRoundTrip_PreservesAllPropert
3741
Assert.NotNull(deserialized.Elicitation.Form);
3842
Assert.NotNull(deserialized.Elicitation.Url);
3943
Assert.NotNull(deserialized.Tasks);
44+
Assert.NotNull(deserialized.Extensions);
45+
Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/test"));
4046
}
4147

4248
[Fact]
@@ -53,5 +59,42 @@ public static void ClientCapabilities_SerializationRoundTrip_WithMinimalProperti
5359
Assert.Null(deserialized.Sampling);
5460
Assert.Null(deserialized.Elicitation);
5561
Assert.Null(deserialized.Tasks);
62+
Assert.Null(deserialized.Extensions);
63+
}
64+
65+
[Fact]
66+
public static void ClientCapabilities_Extensions_DeserializesFromJson()
67+
{
68+
string json = """
69+
{
70+
"extensions": {
71+
"io.modelcontextprotocol/oauth-client-credentials": {},
72+
"io.modelcontextprotocol/test-extension": {
73+
"setting1": "value1",
74+
"setting2": 42
75+
}
76+
}
77+
}
78+
""";
79+
80+
var deserialized = JsonSerializer.Deserialize<ClientCapabilities>(json, McpJsonUtilities.DefaultOptions);
81+
82+
Assert.NotNull(deserialized);
83+
Assert.NotNull(deserialized.Extensions);
84+
Assert.Equal(2, deserialized.Extensions.Count);
85+
Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/oauth-client-credentials"));
86+
Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/test-extension"));
87+
}
88+
89+
[Fact]
90+
public static void ClientCapabilities_Extensions_EmptyObjectDeserializesAsEmptyDictionary()
91+
{
92+
string json = """{"extensions": {}}""";
93+
94+
var deserialized = JsonSerializer.Deserialize<ClientCapabilities>(json, McpJsonUtilities.DefaultOptions);
95+
96+
Assert.NotNull(deserialized);
97+
Assert.NotNull(deserialized.Extensions);
98+
Assert.Empty(deserialized.Extensions);
5699
}
57100
}

tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ public static void ServerCapabilities_SerializationRoundTrip_PreservesAllPropert
1515
Resources = new ResourcesCapability { Subscribe = true, ListChanged = true },
1616
Tools = new ToolsCapability { ListChanged = false },
1717
Completions = new CompletionsCapability(),
18-
Tasks = new McpTasksCapability()
18+
Tasks = new McpTasksCapability(),
19+
Extensions = new Dictionary<string, object>
20+
{
21+
["io.modelcontextprotocol/apps"] = new object()
22+
}
1923
};
2024

2125
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
@@ -32,6 +36,8 @@ public static void ServerCapabilities_SerializationRoundTrip_PreservesAllPropert
3236
Assert.False(deserialized.Tools.ListChanged);
3337
Assert.NotNull(deserialized.Completions);
3438
Assert.NotNull(deserialized.Tasks);
39+
Assert.NotNull(deserialized.Extensions);
40+
Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/apps"));
3541
}
3642

3743
[Fact]
@@ -50,5 +56,42 @@ public static void ServerCapabilities_SerializationRoundTrip_WithMinimalProperti
5056
Assert.Null(deserialized.Tools);
5157
Assert.Null(deserialized.Completions);
5258
Assert.Null(deserialized.Tasks);
59+
Assert.Null(deserialized.Extensions);
60+
}
61+
62+
[Fact]
63+
public static void ServerCapabilities_Extensions_DeserializesFromJson()
64+
{
65+
string json = """
66+
{
67+
"extensions": {
68+
"io.modelcontextprotocol/apps": {},
69+
"io.modelcontextprotocol/custom": {
70+
"option": 42,
71+
"enabled": true
72+
}
73+
}
74+
}
75+
""";
76+
77+
var deserialized = JsonSerializer.Deserialize<ServerCapabilities>(json, McpJsonUtilities.DefaultOptions);
78+
79+
Assert.NotNull(deserialized);
80+
Assert.NotNull(deserialized.Extensions);
81+
Assert.Equal(2, deserialized.Extensions.Count);
82+
Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/apps"));
83+
Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/custom"));
84+
}
85+
86+
[Fact]
87+
public static void ServerCapabilities_Extensions_EmptyObjectDeserializesAsEmptyDictionary()
88+
{
89+
string json = """{"extensions": {}}""";
90+
91+
var deserialized = JsonSerializer.Deserialize<ServerCapabilities>(json, McpJsonUtilities.DefaultOptions);
92+
93+
Assert.NotNull(deserialized);
94+
Assert.NotNull(deserialized.Extensions);
95+
Assert.Empty(deserialized.Extensions);
5396
}
5497
}

0 commit comments

Comments
 (0)