Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)). |
21 changes: 21 additions & 0 deletions src/Common/Experimentals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
27 changes: 27 additions & 0 deletions src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,31 @@ public McpTasksCapability? Tasks
[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; }
}
27 changes: 27 additions & 0 deletions src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,31 @@ public McpTasksCapability? Tasks
[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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ public static void ClientCapabilities_SerializationRoundTrip_PreservesAllPropert
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);
Expand All @@ -37,6 +41,8 @@ public static void ClientCapabilities_SerializationRoundTrip_PreservesAllPropert
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]
Expand All @@ -53,5 +59,42 @@ public static void ClientCapabilities_SerializationRoundTrip_WithMinimalProperti
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ public static void ServerCapabilities_SerializationRoundTrip_PreservesAllPropert
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);
Expand All @@ -32,6 +36,8 @@ public static void ServerCapabilities_SerializationRoundTrip_PreservesAllPropert
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]
Expand All @@ -50,5 +56,42 @@ public static void ServerCapabilities_SerializationRoundTrip_WithMinimalProperti
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);
}
}
Loading