feat(capabilities): add extensions field for SEP-1724 (#643)

Add support for MCP extension capabilities in both ClientCapabilities
and ServerCapabilities structs, as specified in SEP-1724.

Changes:
- Add ExtensionCapabilities type alias (BTreeMap<String, JsonObject>)
- Add 'extensions' field to ClientCapabilities struct
- Add 'extensions' field to ServerCapabilities struct
- Update builder macros and impl blocks for both structs
- Add comprehensive tests for extension capabilities
- Update JSON schema test fixtures

This enables clients to advertise extension support during initialize,
such as:

  {
    "capabilities": {
      "extensions": {
        "io.modelcontextprotocol/ui": {
          "mimeTypes": ["text/html;profile=mcp-app"]
        }
      }
    }
  }

Closes #530
This commit is contained in:
Andrew Harvard 2026-02-03 19:14:22 -05:00 • committed by GitHub
commit 1794fe1548
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 197 additions and 10 deletions

View file

@ -6,6 +6,28 @@ use serde::{Deserialize, Serialize};
use super::JsonObject;
pub type ExperimentalCapabilities = BTreeMap<String, JsonObject>;
/// MCP extension capabilities map.
///
/// Keys are extension identifiers in the format `{vendor-prefix}/{extension-name}`
/// (e.g., `io.modelcontextprotocol/ui`, `io.modelcontextprotocol/oauth-client-credentials`).
/// Values are per-extension settings objects. An empty object indicates support with no settings.
///
/// # Example
///
/// ```rust
/// use rmcp::model::ExtensionCapabilities;
/// use serde_json::json;
///
/// let mut extensions = ExtensionCapabilities::new();
/// extensions.insert(
/// "io.modelcontextprotocol/ui".to_string(),
/// serde_json::from_value(json!({
/// "mimeTypes": ["text/html;profile=mcp-app"]
/// })).unwrap()
/// );
/// ```
pub type ExtensionCapabilities = BTreeMap<String, JsonObject>;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@ -187,6 +209,12 @@ pub struct ElicitationCapability {
pub struct ClientCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental: Option<ExperimentalCapabilities>,
/// Optional MCP extensions that the client supports (SEP-1724).
/// Keys are extension identifiers (e.g., `"io.modelcontextprotocol/ui"`),
/// values are per-extension settings objects. An empty object indicates
/// support with no settings.
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<ExtensionCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roots: Option<RootsCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
@ -217,6 +245,12 @@ pub struct ClientCapabilities {
pub struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental: Option<ExperimentalCapabilities>,
/// Optional MCP extensions that the server supports (SEP-1724).
/// Keys are extension identifiers (e.g., `"io.modelcontextprotocol/apps"`),
/// values are per-extension settings objects. An empty object indicates
/// support with no settings.
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<ExtensionCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logging: Option<JsonObject>,
#[serde(skip_serializing_if = "Option::is_none")]
@ -339,6 +373,7 @@ macro_rules! builder {
builder! {
ServerCapabilities {
experimental: ExperimentalCapabilities,
extensions: ExtensionCapabilities,
logging: JsonObject,
completions: JsonObject,
prompts: PromptsCapability,
@ -348,8 +383,15 @@ builder! {
}
}
impl<const E: bool, const L: bool, const C: bool, const P: bool, const R: bool, const TASKS: bool>
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, C, P, R, true, TASKS>>
impl<
const E: bool,
const EXT: bool,
const L: bool,
const C: bool,
const P: bool,
const R: bool,
const TASKS: bool,
> ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, EXT, L, C, P, R, true, TASKS>>
{
pub fn enable_tool_list_changed(mut self) -> Self {
if let Some(c) = self.tools.as_mut() {
@ -359,8 +401,15 @@ impl<const E: bool, const L: bool, const C: bool, const P: bool, const R: bool,
}
}
impl<const E: bool, const L: bool, const C: bool, const R: bool, const T: bool, const TASKS: bool>
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, C, true, R, T, TASKS>>
impl<
const E: bool,
const EXT: bool,
const L: bool,
const C: bool,
const R: bool,
const T: bool,
const TASKS: bool,
> ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, EXT, L, C, true, R, T, TASKS>>
{
pub fn enable_prompts_list_changed(mut self) -> Self {
if let Some(c) = self.prompts.as_mut() {
@ -370,8 +419,15 @@ impl<const E: bool, const L: bool, const C: bool, const R: bool, const T: bool,
}
}
impl<const E: bool, const L: bool, const C: bool, const P: bool, const T: bool, const TASKS: bool>
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, C, P, true, T, TASKS>>
impl<
const E: bool,
const EXT: bool,
const L: bool,
const C: bool,
const P: bool,
const T: bool,
const TASKS: bool,
> ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, EXT, L, C, P, true, T, TASKS>>
{
pub fn enable_resources_list_changed(mut self) -> Self {
if let Some(c) = self.resources.as_mut() {
@ -391,6 +447,7 @@ impl<const E: bool, const L: bool, const C: bool, const P: bool, const T: bool,
builder! {
ClientCapabilities{
experimental: ExperimentalCapabilities,
extensions: ExtensionCapabilities,
roots: RootsCapabilities,
sampling: JsonObject,
elicitation: ElicitationCapability,
@ -398,8 +455,8 @@ builder! {
}
}
impl<const E: bool, const S: bool, const EL: bool, const TASKS: bool>
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, true, S, EL, TASKS>>
impl<const E: bool, const EXT: bool, const S: bool, const EL: bool, const TASKS: bool>
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, true, S, EL, TASKS>>
{
pub fn enable_roots_list_changed(mut self) -> Self {
if let Some(c) = self.roots.as_mut() {
@ -410,8 +467,8 @@ impl<const E: bool, const S: bool, const EL: bool, const TASKS: bool>
}
#[cfg(feature = "elicitation")]
impl<const E: bool, const R: bool, const S: bool, const TASKS: bool>
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, R, S, true, TASKS>>
impl<const E: bool, const EXT: bool, const R: bool, const S: bool, const TASKS: bool>
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, R, S, true, TASKS>>
{
/// Enable JSON Schema validation for elicitation responses.
/// When enabled, the client will validate user input against the requested_schema
@ -528,4 +585,90 @@ mod test {
assert_eq!(json["cancel"], serde_json::json!({}));
assert_eq!(json["requests"]["tools"]["call"], serde_json::json!({}));
}
#[test]
fn test_client_extensions_capability() {
// Test building ClientCapabilities with extensions (MCP Apps support)
let mut extensions = ExtensionCapabilities::new();
extensions.insert(
"io.modelcontextprotocol/ui".to_string(),
serde_json::from_value(serde_json::json!({
"mimeTypes": ["text/html;profile=mcp-app"]
}))
.unwrap(),
);
let capabilities = ClientCapabilities::builder()
.enable_extensions_with(extensions)
.enable_sampling()
.build();
// Verify serialization matches MCP Apps spec format
let json = serde_json::to_value(&capabilities).unwrap();
assert_eq!(
json["extensions"]["io.modelcontextprotocol/ui"]["mimeTypes"],
serde_json::json!(["text/html;profile=mcp-app"])
);
assert!(json["sampling"].is_object());
}
#[test]
fn test_server_extensions_capability() {
// Test building ServerCapabilities with extensions
let mut extensions = ExtensionCapabilities::new();
extensions.insert(
"io.modelcontextprotocol/apps".to_string(),
serde_json::from_value(serde_json::json!({})).unwrap(),
);
let capabilities = ServerCapabilities::builder()
.enable_extensions_with(extensions)
.enable_tools()
.build();
// Verify serialization
let json = serde_json::to_value(&capabilities).unwrap();
assert!(json["extensions"]["io.modelcontextprotocol/apps"].is_object());
assert!(json["tools"].is_object());
}
#[test]
fn test_extensions_deserialization() {
// Test deserializing capabilities with extensions from JSON
let json = serde_json::json!({
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
},
"sampling": {}
});
let capabilities: ClientCapabilities = serde_json::from_value(json).unwrap();
assert!(capabilities.extensions.is_some());
let extensions = capabilities.extensions.unwrap();
assert!(extensions.contains_key("io.modelcontextprotocol/ui"));
let ui_ext = extensions.get("io.modelcontextprotocol/ui").unwrap();
assert!(ui_ext.contains_key("mimeTypes"));
}
#[test]
fn test_extensions_empty_settings() {
// Test that empty extension settings work (indicates support with no settings)
let mut extensions = ExtensionCapabilities::new();
extensions.insert(
"io.modelcontextprotocol/oauth-client-credentials".to_string(),
JsonObject::new(),
);
let capabilities = ClientCapabilities::builder()
.enable_extensions_with(extensions)
.build();
let json = serde_json::to_value(&capabilities).unwrap();
assert_eq!(
json["extensions"]["io.modelcontextprotocol/oauth-client-credentials"],
serde_json::json!({})
);
}
}

View file

@ -296,6 +296,17 @@
"additionalProperties": true
}
},
"extensions": {
"description": "Optional MCP extensions that the client supports (SEP-1724).\nKeys are extension identifiers (e.g., `\"io.modelcontextprotocol/ui\"`),\nvalues are per-extension settings objects. An empty object indicates\nsupport with no settings.",
"type": [
"object",
"null"
],
"additionalProperties": {
"type": "object",
"additionalProperties": true
}
},
"roots": {
"anyOf": [
{

View file

@ -296,6 +296,17 @@
"additionalProperties": true
}
},
"extensions": {
"description": "Optional MCP extensions that the client supports (SEP-1724).\nKeys are extension identifiers (e.g., `\"io.modelcontextprotocol/ui\"`),\nvalues are per-extension settings objects. An empty object indicates\nsupport with no settings.",
"type": [
"object",
"null"
],
"additionalProperties": {
"type": "object",
"additionalProperties": true
}
},
"roots": {
"anyOf": [
{

View file

@ -2369,6 +2369,17 @@
"additionalProperties": true
}
},
"extensions": {
"description": "Optional MCP extensions that the server supports (SEP-1724).\nKeys are extension identifiers (e.g., `\"io.modelcontextprotocol/apps\"`),\nvalues are per-extension settings objects. An empty object indicates\nsupport with no settings.",
"type": [
"object",
"null"
],
"additionalProperties": {
"type": "object",
"additionalProperties": true
}
},
"logging": {
"type": [
"object",

View file

@ -2369,6 +2369,17 @@
"additionalProperties": true
}
},
"extensions": {
"description": "Optional MCP extensions that the server supports (SEP-1724).\nKeys are extension identifiers (e.g., `\"io.modelcontextprotocol/apps\"`),\nvalues are per-extension settings objects. An empty object indicates\nsupport with no settings.",
"type": [
"object",
"null"
],
"additionalProperties": {
"type": "object",
"additionalProperties": true
}
},
"logging": {
"type": [
"object",