Skip to main content

agent_client_protocol/
capabilities.rs

1//! Capability management for the `_meta.symposium` object in ACP messages.
2//!
3//! This module provides traits and types for working with capabilities stored in
4//! the `_meta.symposium` field of `InitializeRequest` and `InitializeResponse`.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use agent_client_protocol::{MetaCapability, MetaCapabilityExt};
10//! # use agent_client_protocol::schema::v1::InitializeResponse;
11//! # let init_response: InitializeResponse = unimplemented!();
12//!
13//! struct Proxy;
14//! impl MetaCapability for Proxy {
15//!     fn key(&self) -> &'static str { "proxy" }
16//! }
17//!
18//! let response = init_response.add_meta_capability(Proxy);
19//! if response.has_meta_capability(Proxy) {
20//!     // Agent has the proxy capability
21//! }
22//! ```
23
24use crate::schema::v1::{InitializeRequest, InitializeResponse};
25use serde_json::{Value, json};
26
27/// Trait for capabilities stored in the `_meta.symposium` object.
28///
29/// Capabilities are key-value pairs that signal features or context to components
30/// in the proxy chain. Implement this trait to define new capabilities.
31pub trait MetaCapability {
32    /// The key name in the `_meta.symposium` object (e.g., "proxy", "mcp_acp_transport")
33    fn key(&self) -> &'static str;
34
35    /// The value to set when adding this capability (defaults to `true`)
36    fn value(&self) -> serde_json::Value {
37        serde_json::Value::Bool(true)
38    }
39}
40
41/// Extension trait for checking and modifying capabilities in `InitializeRequest`.
42pub trait MetaCapabilityExt {
43    /// Check if a capability is present in `_meta.symposium`
44    fn has_meta_capability(&self, capability: impl MetaCapability) -> bool;
45
46    /// Add a capability to `_meta.symposium`, creating the structure if needed
47    #[must_use]
48    fn add_meta_capability(self, capability: impl MetaCapability) -> Self;
49
50    /// Remove a capability from `_meta.symposium` if present
51    #[must_use]
52    fn remove_meta_capability(self, capability: impl MetaCapability) -> Self;
53}
54
55impl MetaCapabilityExt for InitializeRequest {
56    fn has_meta_capability(&self, capability: impl MetaCapability) -> bool {
57        self.client_capabilities
58            .meta
59            .as_ref()
60            .and_then(|meta| meta.get("symposium"))
61            .and_then(|symposium| symposium.get(capability.key()))
62            .is_some_and(|v| !matches!(v, Value::Bool(false) | Value::Null))
63    }
64
65    fn add_meta_capability(mut self, capability: impl MetaCapability) -> Self {
66        let meta = self
67            .client_capabilities
68            .meta
69            .get_or_insert_with(Default::default);
70
71        let symposium = meta.entry("symposium").or_insert_with(|| json!({}));
72
73        if let Some(symposium_obj) = symposium.as_object_mut() {
74            symposium_obj.insert("version".to_string(), json!("1.0"));
75            symposium_obj.insert(capability.key().to_string(), capability.value());
76        }
77
78        self
79    }
80
81    fn remove_meta_capability(mut self, capability: impl MetaCapability) -> Self {
82        if let Some(ref mut meta) = self.client_capabilities.meta
83            && let Some(symposium) = meta.get_mut("symposium")
84            && let Some(symposium_obj) = symposium.as_object_mut()
85        {
86            symposium_obj.remove(capability.key());
87        }
88        self
89    }
90}
91
92impl MetaCapabilityExt for InitializeResponse {
93    fn has_meta_capability(&self, capability: impl MetaCapability) -> bool {
94        self.agent_capabilities
95            .meta
96            .as_ref()
97            .and_then(|meta| meta.get("symposium"))
98            .and_then(|symposium| symposium.get(capability.key()))
99            .is_some_and(|v| !matches!(v, Value::Bool(false) | Value::Null))
100    }
101
102    fn add_meta_capability(mut self, capability: impl MetaCapability) -> Self {
103        let meta = self
104            .agent_capabilities
105            .meta
106            .get_or_insert_with(Default::default);
107
108        let symposium = meta.entry("symposium").or_insert_with(|| json!({}));
109
110        if let Some(symposium_obj) = symposium.as_object_mut() {
111            symposium_obj.insert("version".to_string(), json!("1.0"));
112            symposium_obj.insert(capability.key().to_string(), capability.value());
113        }
114
115        self
116    }
117
118    fn remove_meta_capability(mut self, capability: impl MetaCapability) -> Self {
119        if let Some(ref mut meta) = self.agent_capabilities.meta
120            && let Some(symposium) = meta.get_mut("symposium")
121            && let Some(symposium_obj) = symposium.as_object_mut()
122        {
123            symposium_obj.remove(capability.key());
124        }
125        self
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::schema::ProtocolVersion;
133    use crate::schema::v1::ClientCapabilities;
134    use serde_json::json;
135
136    struct TestCapability;
137    impl MetaCapability for TestCapability {
138        fn key(&self) -> &'static str {
139            "test_cap"
140        }
141    }
142
143    #[test]
144    fn test_add_capability_to_request() {
145        let request = InitializeRequest::new(ProtocolVersion::LATEST);
146
147        let request = request.add_meta_capability(TestCapability);
148
149        assert!(request.has_meta_capability(TestCapability));
150        assert_eq!(
151            request.client_capabilities.meta.as_ref().unwrap()["symposium"]["test_cap"],
152            json!(true)
153        );
154    }
155
156    #[test]
157    fn test_remove_capability_from_request() {
158        let mut meta = serde_json::Map::new();
159        meta.insert(
160            "symposium".to_string(),
161            json!({
162                "version": "1.0",
163                "test_cap": true
164            }),
165        );
166        let client_capabilities = ClientCapabilities::new().meta(meta);
167
168        let request = InitializeRequest::new(ProtocolVersion::LATEST)
169            .client_capabilities(client_capabilities);
170
171        let request = request.remove_meta_capability(TestCapability);
172
173        assert!(!request.has_meta_capability(TestCapability));
174    }
175
176    #[test]
177    fn test_add_capability_to_response() {
178        let response = InitializeResponse::new(ProtocolVersion::LATEST);
179
180        let response = response.add_meta_capability(TestCapability);
181
182        assert!(response.has_meta_capability(TestCapability));
183        assert_eq!(
184            response.agent_capabilities.meta.as_ref().unwrap()["symposium"]["test_cap"],
185            json!(true)
186        );
187    }
188
189    #[test]
190    fn test_has_meta_capability_false_value() {
191        let mut meta = serde_json::Map::new();
192        meta.insert(
193            "symposium".to_string(),
194            json!({
195                "version": "1.0",
196                "test_cap": false
197            }),
198        );
199        let client_capabilities = ClientCapabilities::new().meta(meta);
200
201        let request = InitializeRequest::new(ProtocolVersion::LATEST)
202            .client_capabilities(client_capabilities);
203
204        assert!(!request.has_meta_capability(TestCapability));
205    }
206
207    #[test]
208    fn test_has_meta_capability_null_value() {
209        let mut meta = serde_json::Map::new();
210        meta.insert(
211            "symposium".to_string(),
212            json!({
213                "version": "1.0",
214                "test_cap": null
215            }),
216        );
217        let client_capabilities = ClientCapabilities::new().meta(meta);
218
219        let request = InitializeRequest::new(ProtocolVersion::LATEST)
220            .client_capabilities(client_capabilities);
221
222        assert!(!request.has_meta_capability(TestCapability));
223    }
224}