Skip to main content

tower_mcp/
extension.rs

1//! Protocol extension declarations and runtime negotiation.
2//!
3//! MCP extensions are opt-in. A client and server each declare extension
4//! support under `capabilities.extensions`; an extension is active only when
5//! both peers declare the same identifier. Unknown extensions remain
6//! round-trippable protocol data but are never activated implicitly.
7
8use std::collections::{BTreeMap, HashMap};
9
10use serde::Serialize;
11use serde_json::Value;
12
13use crate::protocol::{
14    ClientCapabilities, MetaValidationError, ServerCapabilities, validate_extension_identifier,
15};
16
17/// A validated local MCP protocol-extension declaration.
18///
19/// Extension identifiers use the mandatory vendor-prefix form defined by
20/// SEP-2133, for example `io.modelcontextprotocol/ui`. Settings must serialize
21/// to a JSON object; use [`empty`](Self::empty) when an extension has no
22/// settings.
23#[derive(Debug, Clone, PartialEq)]
24pub struct ExtensionDeclaration {
25    identifier: String,
26    settings: Value,
27}
28
29impl ExtensionDeclaration {
30    /// Declare an extension with an empty settings object.
31    pub fn empty(identifier: impl Into<String>) -> Result<Self, MetaValidationError> {
32        Self::new(identifier, serde_json::json!({}))
33    }
34
35    /// Declare an extension with typed settings.
36    pub fn new(
37        identifier: impl Into<String>,
38        settings: impl Serialize,
39    ) -> Result<Self, MetaValidationError> {
40        let identifier = identifier.into();
41        validate_extension_identifier(&identifier)?;
42        let settings = serde_json::to_value(settings)
43            .map_err(|_| MetaValidationError::InvalidExtensionSettings(identifier.clone()))?;
44        if !settings.is_object() {
45            return Err(MetaValidationError::InvalidExtensionSettings(identifier));
46        }
47        Ok(Self {
48            identifier,
49            settings,
50        })
51    }
52
53    /// Extension identifier.
54    pub fn identifier(&self) -> &str {
55        &self.identifier
56    }
57
58    /// Extension-defined local settings object.
59    pub fn settings(&self) -> &Value {
60        &self.settings
61    }
62
63    pub(crate) fn into_parts(self) -> (String, Value) {
64        (self.identifier, self.settings)
65    }
66}
67
68/// The settings each peer declared for one negotiated extension.
69#[derive(Debug, Clone, PartialEq)]
70pub struct NegotiatedExtension {
71    client_settings: Value,
72    server_settings: Value,
73}
74
75impl NegotiatedExtension {
76    /// Settings advertised by the MCP client.
77    pub fn client_settings(&self) -> &Value {
78        &self.client_settings
79    }
80
81    /// Settings advertised by the MCP server.
82    pub fn server_settings(&self) -> &Value {
83        &self.server_settings
84    }
85}
86
87/// Protocol extensions declared by both the client and server.
88///
89/// This is inserted into each [`RequestContext`](crate::RequestContext), so
90/// handlers and per-capability middleware can make extension-specific policy
91/// decisions without inspecting raw capability maps.
92#[derive(Debug, Clone, Default, PartialEq)]
93pub struct NegotiatedExtensions {
94    extensions: BTreeMap<String, NegotiatedExtension>,
95}
96
97impl NegotiatedExtensions {
98    /// Compute the exact identifier intersection of two capability maps.
99    pub fn from_capabilities(client: &ClientCapabilities, server: &ServerCapabilities) -> Self {
100        Self::from_maps(client.extensions.as_ref(), server.extensions.as_ref())
101    }
102
103    pub(crate) fn from_maps(
104        client: Option<&HashMap<String, Value>>,
105        server: Option<&HashMap<String, Value>>,
106    ) -> Self {
107        let mut extensions = BTreeMap::new();
108        let (Some(client), Some(server)) = (client, server) else {
109            return Self { extensions };
110        };
111
112        for (identifier, client_settings) in client {
113            let Some(server_settings) = server.get(identifier) else {
114                continue;
115            };
116            if validate_extension_identifier(identifier).is_err()
117                || !client_settings.is_object()
118                || !server_settings.is_object()
119            {
120                continue;
121            }
122            extensions.insert(
123                identifier.clone(),
124                NegotiatedExtension {
125                    client_settings: client_settings.clone(),
126                    server_settings: server_settings.clone(),
127                },
128            );
129        }
130        Self { extensions }
131    }
132
133    /// Return the negotiated declaration for an extension identifier.
134    pub fn get(&self, identifier: &str) -> Option<&NegotiatedExtension> {
135        self.extensions.get(identifier)
136    }
137
138    /// Return whether both peers declared an extension identifier.
139    pub fn contains(&self, identifier: &str) -> bool {
140        self.extensions.contains_key(identifier)
141    }
142
143    /// Iterate over negotiated extensions in identifier order.
144    pub fn iter(&self) -> impl Iterator<Item = (&str, &NegotiatedExtension)> {
145        self.extensions
146            .iter()
147            .map(|(identifier, extension)| (identifier.as_str(), extension))
148    }
149
150    /// Number of negotiated extension identifiers.
151    pub fn len(&self) -> usize {
152        self.extensions.len()
153    }
154
155    /// Return whether no extension was declared by both peers.
156    pub fn is_empty(&self) -> bool {
157        self.extensions.is_empty()
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn declaration_requires_vendor_prefix_and_object_settings() {
167        assert!(ExtensionDeclaration::empty("com.example/feature").is_ok());
168        assert!(matches!(
169            ExtensionDeclaration::empty("feature"),
170            Err(MetaValidationError::MissingExtensionPrefix(_))
171        ));
172        assert!(matches!(
173            ExtensionDeclaration::new("com.example/feature", true),
174            Err(MetaValidationError::InvalidExtensionSettings(_))
175        ));
176    }
177
178    #[test]
179    fn negotiation_is_the_exact_identifier_intersection() {
180        let client = ClientCapabilities {
181            extensions: Some(HashMap::from([
182                (
183                    "com.example/shared".to_string(),
184                    serde_json::json!({"client": true}),
185                ),
186                ("com.example/client-only".to_string(), serde_json::json!({})),
187            ])),
188            ..ClientCapabilities::default()
189        };
190        let server = ServerCapabilities {
191            extensions: Some(HashMap::from([
192                (
193                    "com.example/shared".to_string(),
194                    serde_json::json!({"server": true}),
195                ),
196                ("com.example/server-only".to_string(), serde_json::json!({})),
197            ])),
198            ..ServerCapabilities::default()
199        };
200
201        let negotiated = NegotiatedExtensions::from_capabilities(&client, &server);
202
203        assert_eq!(negotiated.len(), 1);
204        let shared = negotiated.get("com.example/shared").unwrap();
205        assert_eq!(shared.client_settings()["client"], true);
206        assert_eq!(shared.server_settings()["server"], true);
207        assert!(!negotiated.contains("com.example/client-only"));
208        assert!(!negotiated.contains("com.example/server-only"));
209    }
210}