Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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";
}
28 changes: 28 additions & 0 deletions src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,32 @@ 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. Both clients and 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; }
}
28 changes: 28 additions & 0 deletions src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,32 @@ 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. Both clients and servers advertise extension support via this field during
Comment thread
jeffhandley marked this conversation as resolved.
Outdated
/// 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);
}
}