Skip to main content

mcp/
inbound.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//! **Server→client requests**: the half of MCP a client usually forgets.
3//!
4//! MCP is bidirectional. A server may send the client a *request* — not just a
5//! notification — and the spec is unambiguous that the receiver answers:
6//! `ping` MUST be responded to by either side, and a server that declared the
7//! matching client capability may call `elicitation/create` (ask the human) or
8//! `roots/list` (what may I operate on?).
9//!
10//! Dropping an inbound request on the floor is not a neutral act: a server that
11//! pings and hears silence is entitled to consider the connection dead, and a
12//! server that wants to ask the operator a question is left with no channel.
13//!
14//! The rules this module encodes:
15//!
16//! * **Answer what we advertised, refuse what we did not.** A capability we do
17//!   not declare gets `-32601 Method not found` rather than a half-answer, so a
18//!   server can feature-detect by asking.
19//! * **The host owns the human.** `elicitation/create` is delegated to a
20//!   [`Handler`] the embedder supplies (agentd routes it to `ask_human`, whose
21//!   gates already render in every attached client and survive a restart). The
22//!   crate never invents an answer.
23//! * **Decline is not an error.** The elicitation schema has three outcomes —
24//!   `accept`, `decline`, `cancel` — and a user who says no is a successful
25//!   response carrying `"decline"`, not a JSON-RPC error.
26
27use crate::rpc::{self, Id, Response};
28use serde_json::{Value, json};
29
30/// A server→client request the host may be asked to answer.
31#[derive(Debug, Clone)]
32pub enum Inbound {
33    /// `elicitation/create` — the server needs input from the human operator.
34    /// Carries the server's message and the requested-schema, verbatim.
35    Elicit {
36        message: String,
37        requested_schema: Value,
38    },
39    /// `roots/list` — the server is asking which URI roots it may operate on.
40    ListRoots,
41}
42
43/// What the host decided. Mirrors the spec's elicitation outcomes so a refusal
44/// is expressible without inventing content.
45#[derive(Debug, Clone)]
46pub enum Answer {
47    /// The user answered; `content` matches the requested schema.
48    Accept(Value),
49    /// The user actively refused. Not an error.
50    Decline,
51    /// The user dismissed it without deciding (or nothing could ask).
52    Cancel,
53    /// The roots this client exposes.
54    Roots(Vec<Root>),
55}
56
57/// One entry of the `roots/list` result.
58#[derive(Debug, Clone)]
59pub struct Root {
60    pub uri: String,
61    pub name: Option<String>,
62}
63
64/// The host's answering surface. Implemented by the embedder; `None` anywhere
65/// means "we did not advertise that capability", and the request is refused.
66pub trait Handler: Send + Sync {
67    /// Answer a server→client request. Returning `None` declines the capability
68    /// itself (the caller turns that into `-32601`).
69    fn handle(&self, req: Inbound) -> Option<Answer>;
70}
71
72/// Which client capabilities a [`Handler`] backs. Declared in the `initialize`
73/// handshake (legacy) and in `_meta` client capabilities (modern), so a server
74/// only calls what we can actually answer.
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
76pub struct Capabilities {
77    pub elicitation: bool,
78    pub roots: bool,
79}
80
81impl Capabilities {
82    /// The JSON object a client advertises. `ping` is not a capability — it is
83    /// unconditional — so it never appears here.
84    pub fn to_json(self) -> Value {
85        let mut caps = serde_json::Map::new();
86        if self.elicitation {
87            caps.insert("elicitation".into(), json!({}));
88        }
89        if self.roots {
90            // `listChanged` stays false: there is no notification path for root
91            // changes, and advertising a notification we never send leaves a
92            // server waiting on an update that cannot arrive.
93            caps.insert("roots".into(), json!({"listChanged": false}));
94        }
95        Value::Object(caps)
96    }
97
98    pub fn is_empty(self) -> bool {
99        !self.elicitation && !self.roots
100    }
101}
102
103/// Answer one inbound JSON-RPC request.
104///
105/// `ping` is answered unconditionally (spec MUST) even with no handler at all —
106/// that is the whole point: a liveness probe must not depend on what the host
107/// chose to implement.
108pub fn answer(req: &rpc::Request, caps: Capabilities, handler: Option<&dyn Handler>) -> Response {
109    let id = req.id.clone();
110    match req.method.as_str() {
111        // Both sides MUST respond; the result is an empty object.
112        "ping" => Response::ok(id, json!({})),
113
114        "elicitation/create" if caps.elicitation => {
115            let params = req.params.clone().unwrap_or(Value::Null);
116            let message = params
117                .get("message")
118                .and_then(Value::as_str)
119                .unwrap_or("")
120                .to_string();
121            let requested_schema = params
122                .get("requestedSchema")
123                .cloned()
124                .unwrap_or_else(|| json!({"type": "object"}));
125            match handler.and_then(|h| {
126                h.handle(Inbound::Elicit {
127                    message,
128                    requested_schema,
129                })
130            }) {
131                Some(Answer::Accept(content)) => {
132                    Response::ok(id, json!({"action": "accept", "content": content}))
133                }
134                Some(Answer::Decline) => Response::ok(id, json!({"action": "decline"})),
135                // No handler, or nothing could ask: cancel is the honest answer.
136                Some(Answer::Cancel) | None => Response::ok(id, json!({"action": "cancel"})),
137                Some(Answer::Roots(_)) => Response::err(
138                    id,
139                    rpc::INTERNAL_ERROR,
140                    "handler answered elicitation with roots",
141                ),
142            }
143        }
144
145        "roots/list" if caps.roots => match handler.and_then(|h| h.handle(Inbound::ListRoots)) {
146            Some(Answer::Roots(roots)) => Response::ok(
147                id,
148                json!({
149                    "roots": roots.iter().map(|r| match &r.name {
150                        Some(n) => json!({"uri": r.uri, "name": n}),
151                        None => json!({"uri": r.uri}),
152                    }).collect::<Vec<_>>()
153                }),
154            ),
155            _ => Response::ok(id, json!({"roots": []})),
156        },
157
158        // Anything we did not advertise — including elicitation/roots when the
159        // capability is off. Feature detection by asking is legitimate, so this
160        // is a clean refusal, not a fault.
161        other => Response::err(
162            id,
163            rpc::METHOD_NOT_FOUND,
164            format!("client does not implement {other}"),
165        ),
166    }
167}
168
169/// Classify a raw inbound JSON-RPC frame. A frame with an `id` AND a `method` is
170/// a request we must answer; with a `method` and no `id` it is a notification;
171/// anything else (a response to something we sent) is not ours to route here.
172pub fn as_request(v: &Value) -> Option<rpc::Request> {
173    if v.get("method").is_some() && v.get("id").is_some() {
174        serde_json::from_value::<rpc::Request>(v.clone()).ok()
175    } else {
176        None
177    }
178}
179
180/// The JSON-RPC id of a frame, for logging a dropped/failed answer.
181pub fn frame_id(v: &Value) -> Option<Id> {
182    serde_json::from_value::<Id>(v.get("id")?.clone()).ok()
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    struct Yes(Answer);
190    impl Handler for Yes {
191        fn handle(&self, _req: Inbound) -> Option<Answer> {
192            Some(self.0.clone())
193        }
194    }
195    struct No;
196    impl Handler for No {
197        fn handle(&self, _req: Inbound) -> Option<Answer> {
198            None
199        }
200    }
201
202    fn req(method: &str, params: Value) -> rpc::Request {
203        rpc::Request::new(1, method, Some(params))
204    }
205
206    #[test]
207    fn ping_is_answered_even_with_no_handler_or_capabilities() {
208        // The liveness probe must not depend on what the host implements:
209        // silence here lets a server conclude the connection is dead.
210        let r = answer(&req("ping", json!({})), Capabilities::default(), None);
211        assert_eq!(r.result, Some(json!({})));
212        assert!(r.error.is_none());
213    }
214
215    #[test]
216    fn elicitation_maps_the_three_outcomes() {
217        let caps = Capabilities {
218            elicitation: true,
219            roots: false,
220        };
221        let p = json!({"message": "Which environment?", "requestedSchema": {"type": "object"}});
222
223        let accept = Yes(Answer::Accept(json!({"env": "staging"})));
224        let r = answer(&req("elicitation/create", p.clone()), caps, Some(&accept));
225        assert_eq!(r.result.as_ref().unwrap()["action"], "accept");
226        assert_eq!(r.result.unwrap()["content"]["env"], "staging");
227
228        // A refusal is a SUCCESSFUL response carrying `decline`, not an error.
229        let decline = Yes(Answer::Decline);
230        let r = answer(&req("elicitation/create", p.clone()), caps, Some(&decline));
231        assert!(r.error.is_none());
232        assert_eq!(r.result.unwrap()["action"], "decline");
233
234        // Nothing could ask ⇒ cancel.
235        let r = answer(&req("elicitation/create", p.clone()), caps, Some(&No));
236        assert_eq!(r.result.unwrap()["action"], "cancel");
237        let r = answer(&req("elicitation/create", p), caps, None);
238        assert_eq!(r.result.unwrap()["action"], "cancel");
239    }
240
241    #[test]
242    fn an_undeclared_capability_is_refused_not_half_answered() {
243        // A server may probe by calling; the honest answer is method-not-found.
244        let none = Capabilities::default();
245        let r = answer(&req("elicitation/create", json!({})), none, Some(&No));
246        assert_eq!(r.error.as_ref().unwrap().code, rpc::METHOD_NOT_FOUND);
247        let r = answer(&req("roots/list", json!({})), none, Some(&No));
248        assert_eq!(r.error.as_ref().unwrap().code, rpc::METHOD_NOT_FOUND);
249        // And anything we simply do not implement.
250        let r = answer(&req("sampling/createMessage", json!({})), none, None);
251        assert_eq!(r.error.unwrap().code, rpc::METHOD_NOT_FOUND);
252    }
253
254    #[test]
255    fn roots_are_listed_when_declared() {
256        let caps = Capabilities {
257            elicitation: false,
258            roots: true,
259        };
260        let h = Yes(Answer::Roots(vec![Root {
261            uri: "file:///work".into(),
262            name: Some("workspace".into()),
263        }]));
264        let r = answer(&req("roots/list", json!({})), caps, Some(&h));
265        let roots = &r.result.unwrap()["roots"];
266        assert_eq!(roots[0]["uri"], "file:///work");
267        assert_eq!(roots[0]["name"], "workspace");
268    }
269
270    #[test]
271    fn capabilities_serialize_to_what_we_can_actually_answer() {
272        assert_eq!(Capabilities::default().to_json(), json!({}));
273        assert!(Capabilities::default().is_empty());
274        let both = Capabilities {
275            elicitation: true,
276            roots: true,
277        };
278        assert_eq!(
279            both.to_json(),
280            json!({"elicitation": {}, "roots": {"listChanged": false}})
281        );
282        // `ping` is unconditional and never advertised as a capability.
283        assert!(both.to_json().get("ping").is_none());
284    }
285
286    #[test]
287    fn request_classification_separates_requests_from_notifications() {
288        assert!(as_request(&json!({"jsonrpc":"2.0","id":1,"method":"ping"})).is_some());
289        // No id ⇒ a notification, not ours to answer.
290        assert!(as_request(&json!({"jsonrpc":"2.0","method":"notifications/x"})).is_none());
291        // A response to something we sent.
292        assert!(as_request(&json!({"jsonrpc":"2.0","id":1,"result":{}})).is_none());
293    }
294}