Skip to content

Commit 1794fe1

Browse files
authored
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
1 parent 32a68aa commit 1794fe1

File tree

5 files changed

+197
-10
lines changed

5 files changed

+197
-10
lines changed

crates/rmcp/src/model/capabilities.rs

Lines changed: 153 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@ use serde::{Deserialize, Serialize};
66
use super::JsonObject;
77
pub type ExperimentalCapabilities = BTreeMap<String, JsonObject>;
88

9+
/// MCP extension capabilities map.
10+
///
11+
/// Keys are extension identifiers in the format `{vendor-prefix}/{extension-name}`
12+
/// (e.g., `io.modelcontextprotocol/ui`, `io.modelcontextprotocol/oauth-client-credentials`).
13+
/// Values are per-extension settings objects. An empty object indicates support with no settings.
14+
///
15+
/// # Example
16+
///
17+
/// ```rust
18+
/// use rmcp::model::ExtensionCapabilities;
19+
/// use serde_json::json;
20+
///
21+
/// let mut extensions = ExtensionCapabilities::new();
22+
/// extensions.insert(
23+
/// "io.modelcontextprotocol/ui".to_string(),
24+
/// serde_json::from_value(json!({
25+
/// "mimeTypes": ["text/html;profile=mcp-app"]
26+
/// })).unwrap()
27+
/// );
28+
/// ```
29+
pub type ExtensionCapabilities = BTreeMap<String, JsonObject>;
30+
931
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
1032
#[serde(rename_all = "camelCase")]
1133
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@@ -187,6 +209,12 @@ pub struct ElicitationCapability {
187209
pub struct ClientCapabilities {
188210
#[serde(skip_serializing_if = "Option::is_none")]
189211
pub experimental: Option<ExperimentalCapabilities>,
212+
/// Optional MCP extensions that the client supports (SEP-1724).
213+
/// Keys are extension identifiers (e.g., `"io.modelcontextprotocol/ui"`),
214+
/// values are per-extension settings objects. An empty object indicates
215+
/// support with no settings.
216+
#[serde(skip_serializing_if = "Option::is_none")]
217+
pub extensions: Option<ExtensionCapabilities>,
190218
#[serde(skip_serializing_if = "Option::is_none")]
191219
pub roots: Option<RootsCapabilities>,
192220
#[serde(skip_serializing_if = "Option::is_none")]
@@ -217,6 +245,12 @@ pub struct ClientCapabilities {
217245
pub struct ServerCapabilities {
218246
#[serde(skip_serializing_if = "Option::is_none")]
219247
pub experimental: Option<ExperimentalCapabilities>,
248+
/// Optional MCP extensions that the server supports (SEP-1724).
249+
/// Keys are extension identifiers (e.g., `"io.modelcontextprotocol/apps"`),
250+
/// values are per-extension settings objects. An empty object indicates
251+
/// support with no settings.
252+
#[serde(skip_serializing_if = "Option::is_none")]
253+
pub extensions: Option<ExtensionCapabilities>,
220254
#[serde(skip_serializing_if = "Option::is_none")]
221255
pub logging: Option<JsonObject>,
222256
#[serde(skip_serializing_if = "Option::is_none")]
@@ -339,6 +373,7 @@ macro_rules! builder {
339373
builder! {
340374
ServerCapabilities {
341375
experimental: ExperimentalCapabilities,
376+
extensions: ExtensionCapabilities,
342377
logging: JsonObject,
343378
completions: JsonObject,
344379
prompts: PromptsCapability,
@@ -348,8 +383,15 @@ builder! {
348383
}
349384
}
350385

351-
impl<const E: bool, const L: bool, const C: bool, const P: bool, const R: bool, const TASKS: bool>
352-
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, C, P, R, true, TASKS>>
386+
impl<
387+
const E: bool,
388+
const EXT: bool,
389+
const L: bool,
390+
const C: bool,
391+
const P: bool,
392+
const R: bool,
393+
const TASKS: bool,
394+
> ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, EXT, L, C, P, R, true, TASKS>>
353395
{
354396
pub fn enable_tool_list_changed(mut self) -> Self {
355397
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,
359401
}
360402
}
361403

362-
impl<const E: bool, const L: bool, const C: bool, const R: bool, const T: bool, const TASKS: bool>
363-
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, C, true, R, T, TASKS>>
404+
impl<
405+
const E: bool,
406+
const EXT: bool,
407+
const L: bool,
408+
const C: bool,
409+
const R: bool,
410+
const T: bool,
411+
const TASKS: bool,
412+
> ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, EXT, L, C, true, R, T, TASKS>>
364413
{
365414
pub fn enable_prompts_list_changed(mut self) -> Self {
366415
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,
370419
}
371420
}
372421

373-
impl<const E: bool, const L: bool, const C: bool, const P: bool, const T: bool, const TASKS: bool>
374-
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, C, P, true, T, TASKS>>
422+
impl<
423+
const E: bool,
424+
const EXT: bool,
425+
const L: bool,
426+
const C: bool,
427+
const P: bool,
428+
const T: bool,
429+
const TASKS: bool,
430+
> ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, EXT, L, C, P, true, T, TASKS>>
375431
{
376432
pub fn enable_resources_list_changed(mut self) -> Self {
377433
if let Some(c) = self.resources.as_mut() {
@@ -391,15 +447,16 @@ impl<const E: bool, const L: bool, const C: bool, const P: bool, const T: bool,
391447
builder! {
392448
ClientCapabilities{
393449
experimental: ExperimentalCapabilities,
450+
extensions: ExtensionCapabilities,
394451
roots: RootsCapabilities,
395452
sampling: JsonObject,
396453
elicitation: ElicitationCapability,
397454
tasks: TasksCapability,
398455
}
399456
}
400457

401-
impl<const E: bool, const S: bool, const EL: bool, const TASKS: bool>
402-
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, true, S, EL, TASKS>>
458+
impl<const E: bool, const EXT: bool, const S: bool, const EL: bool, const TASKS: bool>
459+
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, true, S, EL, TASKS>>
403460
{
404461
pub fn enable_roots_list_changed(mut self) -> Self {
405462
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>
410467
}
411468

412469
#[cfg(feature = "elicitation")]
413-
impl<const E: bool, const R: bool, const S: bool, const TASKS: bool>
414-
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, R, S, true, TASKS>>
470+
impl<const E: bool, const EXT: bool, const R: bool, const S: bool, const TASKS: bool>
471+
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, R, S, true, TASKS>>
415472
{
416473
/// Enable JSON Schema validation for elicitation responses.
417474
/// When enabled, the client will validate user input against the requested_schema
@@ -528,4 +585,90 @@ mod test {
528585
assert_eq!(json["cancel"], serde_json::json!({}));
529586
assert_eq!(json["requests"]["tools"]["call"], serde_json::json!({}));
530587
}
588+
589+
#[test]
590+
fn test_client_extensions_capability() {
591+
// Test building ClientCapabilities with extensions (MCP Apps support)
592+
let mut extensions = ExtensionCapabilities::new();
593+
extensions.insert(
594+
"io.modelcontextprotocol/ui".to_string(),
595+
serde_json::from_value(serde_json::json!({
596+
"mimeTypes": ["text/html;profile=mcp-app"]
597+
}))
598+
.unwrap(),
599+
);
600+
601+
let capabilities = ClientCapabilities::builder()
602+
.enable_extensions_with(extensions)
603+
.enable_sampling()
604+
.build();
605+
606+
// Verify serialization matches MCP Apps spec format
607+
let json = serde_json::to_value(&capabilities).unwrap();
608+
assert_eq!(
609+
json["extensions"]["io.modelcontextprotocol/ui"]["mimeTypes"],
610+
serde_json::json!(["text/html;profile=mcp-app"])
611+
);
612+
assert!(json["sampling"].is_object());
613+
}
614+
615+
#[test]
616+
fn test_server_extensions_capability() {
617+
// Test building ServerCapabilities with extensions
618+
let mut extensions = ExtensionCapabilities::new();
619+
extensions.insert(
620+
"io.modelcontextprotocol/apps".to_string(),
621+
serde_json::from_value(serde_json::json!({})).unwrap(),
622+
);
623+
624+
let capabilities = ServerCapabilities::builder()
625+
.enable_extensions_with(extensions)
626+
.enable_tools()
627+
.build();
628+
629+
// Verify serialization
630+
let json = serde_json::to_value(&capabilities).unwrap();
631+
assert!(json["extensions"]["io.modelcontextprotocol/apps"].is_object());
632+
assert!(json["tools"].is_object());
633+
}
634+
635+
#[test]
636+
fn test_extensions_deserialization() {
637+
// Test deserializing capabilities with extensions from JSON
638+
let json = serde_json::json!({
639+
"extensions": {
640+
"io.modelcontextprotocol/ui": {
641+
"mimeTypes": ["text/html;profile=mcp-app"]
642+
}
643+
},
644+
"sampling": {}
645+
});
646+
647+
let capabilities: ClientCapabilities = serde_json::from_value(json).unwrap();
648+
assert!(capabilities.extensions.is_some());
649+
let extensions = capabilities.extensions.unwrap();
650+
assert!(extensions.contains_key("io.modelcontextprotocol/ui"));
651+
let ui_ext = extensions.get("io.modelcontextprotocol/ui").unwrap();
652+
assert!(ui_ext.contains_key("mimeTypes"));
653+
}
654+
655+
#[test]
656+
fn test_extensions_empty_settings() {
657+
// Test that empty extension settings work (indicates support with no settings)
658+
let mut extensions = ExtensionCapabilities::new();
659+
extensions.insert(
660+
"io.modelcontextprotocol/oauth-client-credentials".to_string(),
661+
JsonObject::new(),
662+
);
663+
664+
let capabilities = ClientCapabilities::builder()
665+
.enable_extensions_with(extensions)
666+
.build();
667+
668+
let json = serde_json::to_value(&capabilities).unwrap();
669+
assert_eq!(
670+
json["extensions"]["io.modelcontextprotocol/oauth-client-credentials"],
671+
serde_json::json!({})
672+
);
673+
}
531674
}

crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,17 @@
296296
"additionalProperties": true
297297
}
298298
},
299+
"extensions": {
300+
"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.",
301+
"type": [
302+
"object",
303+
"null"
304+
],
305+
"additionalProperties": {
306+
"type": "object",
307+
"additionalProperties": true
308+
}
309+
},
299310
"roots": {
300311
"anyOf": [
301312
{

crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,17 @@
296296
"additionalProperties": true
297297
}
298298
},
299+
"extensions": {
300+
"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.",
301+
"type": [
302+
"object",
303+
"null"
304+
],
305+
"additionalProperties": {
306+
"type": "object",
307+
"additionalProperties": true
308+
}
309+
},
299310
"roots": {
300311
"anyOf": [
301312
{

crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2369,6 +2369,17 @@
23692369
"additionalProperties": true
23702370
}
23712371
},
2372+
"extensions": {
2373+
"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.",
2374+
"type": [
2375+
"object",
2376+
"null"
2377+
],
2378+
"additionalProperties": {
2379+
"type": "object",
2380+
"additionalProperties": true
2381+
}
2382+
},
23722383
"logging": {
23732384
"type": [
23742385
"object",

crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2369,6 +2369,17 @@
23692369
"additionalProperties": true
23702370
}
23712371
},
2372+
"extensions": {
2373+
"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.",
2374+
"type": [
2375+
"object",
2376+
"null"
2377+
],
2378+
"additionalProperties": {
2379+
"type": "object",
2380+
"additionalProperties": true
2381+
}
2382+
},
23722383
"logging": {
23732384
"type": [
23742385
"object",

0 commit comments

Comments
 (0)