Skip to main content

protocol_droid/application/client/
handler.rs

1//! Answering the requests a server sends the client.
2//!
3//! Through `2025-11-25` a server can ask the client to run a model
4//! (`sampling/createMessage`), ask its user a question (`elicitation/create`)
5//! or list its roots (`roots/list`), and it does so as a JSON-RPC request on
6//! the response stream of whatever call needed the answer. The call is blocked
7//! until the client replies, so a client that declared one of these
8//! capabilities and then did not answer left the server's tool waiting forever.
9//!
10//! [`ServerRequestHandler`] is that answer. It is a port the caller implements:
11//! this crate cannot know what a person would say, and answering on their
12//! behalf would be inventing consent.
13//!
14//! `2026-07-28` removed server-to-client requests. The same three questions
15//! now arrive as an `InputRequiredResult` and are answered through
16//! [`call_resolving`](super::inputs::call_resolving).
17
18use core::future::Future;
19use core::pin::Pin;
20use std::sync::Arc;
21
22use serde_json::{Map, Value};
23
24use crate::domain::CapabilitySet;
25use crate::domain::ir::JsonObject;
26
27/// Why a handler declined a request.
28///
29/// Becomes the JSON-RPC `error` the server reads, so the server's tool learns
30/// that the client said no rather than hanging.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct HandlerRefusal {
33    /// The JSON-RPC error code.
34    pub code: i64,
35    /// A short reason.
36    pub message: String,
37}
38
39impl HandlerRefusal {
40    /// `-32601`: the client does not do this.
41    #[must_use]
42    pub fn unsupported(method: &str) -> Self {
43        Self {
44            code: -32601,
45            message: format!("this client does not handle `{method}`"),
46        }
47    }
48}
49
50/// Answers the requests a server sends the client.
51///
52/// # Examples
53///
54/// ```
55/// use std::pin::Pin;
56///
57/// use protocol_droid::application::client::handler::{HandlerRefusal, ServerRequestHandler};
58/// use protocol_droid::domain::ir::JsonObject;
59///
60/// struct Decline;
61///
62/// impl ServerRequestHandler for Decline {
63///     fn handle<'a>(
64///         &'a self,
65///         method: &'a str,
66///         _params: &'a JsonObject,
67///     ) -> Pin<Box<dyn Future<Output = Result<JsonObject, HandlerRefusal>> + Send + 'a>> {
68///         Box::pin(async move { Err(HandlerRefusal::unsupported(method)) })
69///     }
70/// }
71/// ```
72pub trait ServerRequestHandler: Send + Sync {
73    /// Answers one request with its `result`, or declines it.
74    fn handle<'a>(
75        &'a self,
76        method: &'a str,
77        params: &'a JsonObject,
78    ) -> Pin<Box<dyn Future<Output = Result<JsonObject, HandlerRefusal>> + Send + 'a>>;
79}
80
81/// What a connection declares and how it answers the server.
82#[derive(Clone, Default)]
83pub struct ConnectOptions {
84    pub(crate) capabilities: CapabilitySet,
85    pub(crate) handler: Option<Arc<dyn ServerRequestHandler>>,
86    pub(crate) auth: Option<Arc<dyn super::bearer::BearerAuth>>,
87}
88
89impl core::fmt::Debug for ConnectOptions {
90    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91        f.debug_struct("ConnectOptions")
92            .field("capabilities", &self.capabilities)
93            .field("handler", &self.handler.is_some())
94            .field("auth", &self.auth.is_some())
95            .finish()
96    }
97}
98
99impl ConnectOptions {
100    /// Options that declare nothing and answer nothing.
101    #[must_use]
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    /// Declares `capabilities` to the server.
107    ///
108    /// A server asks for sampling, elicitation or roots only when the client
109    /// declared them, so declare exactly what the handler can answer.
110    #[must_use]
111    pub const fn declaring(mut self, capabilities: CapabilitySet) -> Self {
112        self.capabilities = capabilities;
113        self
114    }
115
116    /// Authenticates to the server through `auth`.
117    ///
118    /// For an OAuth-protected server, pass an
119    /// [`OAuthAuthorizer`](crate::application::oauth::authorizer::OAuthAuthorizer).
120    #[must_use]
121    pub fn authorizing(mut self, auth: Arc<dyn super::bearer::BearerAuth>) -> Self {
122        self.auth = Some(auth);
123        self
124    }
125
126    /// Answers the server's requests through `handler`.
127    #[must_use]
128    pub fn answering(mut self, handler: Arc<dyn ServerRequestHandler>) -> Self {
129        self.handler = Some(handler);
130        self
131    }
132}
133
134/// Fills in the `default` of every property an `accept`ed elicitation omitted.
135///
136/// SEP-1034: a client that declared `elicitation.applyDefaults` applies the
137/// schema's defaults for fields the user left out, so a server receives a
138/// complete object. `request` is the `elicitation/create` params; `answer` is
139/// the `ElicitResult` the handler produced. Only an `accept` is touched, and
140/// only absent keys: a value the user gave is never overwritten.
141#[must_use]
142pub fn apply_elicitation_defaults(request: &JsonObject, answer: &JsonObject) -> JsonObject {
143    let Ok(Value::Object(mut result)) = serde_json::from_str::<Value>(answer.as_str()) else {
144        return answer.clone();
145    };
146    if result.get("action").and_then(Value::as_str) != Some("accept") {
147        return answer.clone();
148    }
149    let Ok(Value::Object(params)) = serde_json::from_str::<Value>(request.as_str()) else {
150        return answer.clone();
151    };
152    let Some(properties) = params
153        .get("requestedSchema")
154        .and_then(|schema| schema.get("properties"))
155        .and_then(Value::as_object)
156    else {
157        return answer.clone();
158    };
159    let content = result
160        .entry("content")
161        .or_insert_with(|| Value::Object(Map::new()));
162    if let Some(content) = content.as_object_mut() {
163        for (name, schema) in properties {
164            if !content.contains_key(name)
165                && let Some(default) = schema.get("default")
166            {
167                content.insert(name.clone(), default.clone());
168            }
169        }
170    }
171    JsonObject::from_value(Value::Object(result)).unwrap_or_else(|_| answer.clone())
172}
173
174#[cfg(test)]
175mod tests {
176    use super::apply_elicitation_defaults;
177    use crate::domain::ir::JsonObject;
178
179    fn object(text: &str) -> JsonObject {
180        JsonObject::new(text).unwrap_or_default()
181    }
182
183    #[test]
184    fn omitted_fields_take_their_default() {
185        let request = object(
186            r#"{"message":"m","requestedSchema":{"type":"object","properties":{"name":{"type":"string","default":"John"},"age":{"type":"integer","default":30},"plain":{"type":"string"}}}}"#,
187        );
188        let filled =
189            apply_elicitation_defaults(&request, &object(r#"{"action":"accept","content":{}}"#));
190        let parsed: serde_json::Value = serde_json::from_str(filled.as_str()).unwrap_or_default();
191        assert_eq!(
192            parsed.pointer("/content/name"),
193            Some(&serde_json::json!("John"))
194        );
195        assert_eq!(parsed.pointer("/content/age"), Some(&serde_json::json!(30)));
196        assert_eq!(
197            parsed.pointer("/content/plain"),
198            None,
199            "no default, nothing invented"
200        );
201    }
202
203    #[test]
204    fn a_value_the_user_gave_is_kept() {
205        let request = object(
206            r#"{"requestedSchema":{"properties":{"name":{"type":"string","default":"John"}}}}"#,
207        );
208        let filled = apply_elicitation_defaults(
209            &request,
210            &object(r#"{"action":"accept","content":{"name":"Ada"}}"#),
211        );
212        assert!(filled.as_str().contains("Ada") && !filled.as_str().contains("John"));
213    }
214
215    #[test]
216    fn a_decline_is_untouched() {
217        let request = object(
218            r#"{"requestedSchema":{"properties":{"name":{"type":"string","default":"John"}}}}"#,
219        );
220        let answer = object(r#"{"action":"decline"}"#);
221        assert_eq!(apply_elicitation_defaults(&request, &answer), answer);
222    }
223}