Skip to main content

protocol_droid/domain/mcp/
client_methods.rs

1//! Which client-to-server request methods each revision defines.
2//!
3//! # Why a table and not the feature matrix
4//!
5//! The feature matrix answers "may this session use behaviour X", and a feature
6//! that switched off is still a feature: `ping` has a row because something can
7//! depend on it being refused. Three of the methods `2026-07-28` drops are not
8//! like that. `logging/setLevel` and `resources/subscribe` were *replaced* —
9//! by the per-request `io.modelcontextprotocol/logLevel` key and by
10//! `subscriptions/listen` — so the behaviour they carried survives under
11//! another name, and a feature row would say the capability is still there.
12//! What changed is the method set, and the method set is a schema fact:
13//! membership of each revision's `ClientRequest` union.
14//!
15//! A method a revision does not define is answered `-32601`, and on the
16//! `2026-07-28` HTTP transport with `404 Not Found`. Answering it as though it
17//! were merely ungated would tell a client the method exists and refused, when
18//! the revision it chose has no such method at all.
19//!
20//! # Sources
21//!
22//! Each list is the `ClientRequest` union of the vendored `schema.json` for
23//! that revision, with one correction: the `2025-03-26` union omits
24//! `resources/templates/list` although the same schema defines
25//! `ListResourceTemplatesRequest` and its result. The omission is an upstream
26//! slip, not a removal — the method exists on both sides of that revision — so
27//! the table follows the defined types rather than the union.
28//!
29//! - <https://modelcontextprotocol.io/specification/2024-11-05/changelog>
30//! - <https://modelcontextprotocol.io/specification/2025-11-25/changelog> —
31//!   SEP-1686 adds `tasks/get`, `tasks/result`, `tasks/cancel`, `tasks/list`.
32//! - <https://modelcontextprotocol.io/specification/2026-07-28/changelog> —
33//!   SEP-2575 removes `initialize`, `ping`, `logging/setLevel`,
34//!   `resources/subscribe` and `resources/unsubscribe`, and adds
35//!   `server/discover` and `subscriptions/listen`; SEP-2663 moves `tasks/*` to
36//!   the `io.modelcontextprotocol/tasks` extension.
37
38use crate::domain::ProtocolVersion;
39
40/// The methods every revision through `2025-11-25` shares.
41const STATEFUL: &[&str] = &[
42    "completion/complete",
43    "initialize",
44    "logging/setLevel",
45    "ping",
46    "prompts/get",
47    "prompts/list",
48    "resources/list",
49    "resources/read",
50    "resources/subscribe",
51    "resources/templates/list",
52    "resources/unsubscribe",
53    "tools/call",
54    "tools/list",
55];
56
57/// SEP-1686's task methods, core only in `2025-11-25`.
58const TASKS: &[&str] = &["tasks/cancel", "tasks/get", "tasks/list", "tasks/result"];
59
60/// The `2026-07-28` method set, and every later draft's.
61const STATELESS: &[&str] = &[
62    "completion/complete",
63    "prompts/get",
64    "prompts/list",
65    "resources/list",
66    "resources/read",
67    "resources/templates/list",
68    "server/discover",
69    "subscriptions/listen",
70    "tools/call",
71    "tools/list",
72];
73
74/// Whether `method` is a client-to-server request `version` defines.
75///
76/// A revision newer than this build answers with the `2026-07-28` set, for the
77/// reason [`discover_defined_in`](crate::domain::discover_defined_in) gives:
78/// SEP-2575 is not something a later draft would take back.
79///
80/// # Examples
81///
82/// ```
83/// use protocol_droid::domain::{ProtocolVersion, client_method_defined_in};
84///
85/// assert!(client_method_defined_in(ProtocolVersion::V2025_11_25, "ping"));
86/// // SEP-2575 removed `ping` and replaced `logging/setLevel`.
87/// assert!(!client_method_defined_in(ProtocolVersion::V2026_07_28, "ping"));
88/// assert!(!client_method_defined_in(ProtocolVersion::V2026_07_28, "logging/setLevel"));
89/// assert!(client_method_defined_in(ProtocolVersion::V2026_07_28, "subscriptions/listen"));
90/// ```
91#[must_use]
92pub fn client_method_defined_in(version: ProtocolVersion, method: &str) -> bool {
93    match version {
94        ProtocolVersion::V2024_11_05
95        | ProtocolVersion::V2025_03_26
96        | ProtocolVersion::V2025_06_18 => STATEFUL.contains(&method),
97        ProtocolVersion::V2025_11_25 => STATEFUL.contains(&method) || TASKS.contains(&method),
98        ProtocolVersion::V2026_07_28 | ProtocolVersion::Unknown(_) => STATELESS.contains(&method),
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::client_method_defined_in;
105    use crate::domain::ProtocolVersion;
106
107    #[test]
108    fn the_handshake_exists_exactly_where_the_negotiation_says_it_does() {
109        for version in [
110            ProtocolVersion::V2024_11_05,
111            ProtocolVersion::V2025_03_26,
112            ProtocolVersion::V2025_06_18,
113            ProtocolVersion::V2025_11_25,
114            ProtocolVersion::V2026_07_28,
115        ] {
116            assert_eq!(
117                client_method_defined_in(version, "initialize"),
118                crate::domain::handshake_defined_in(version),
119                "initialize at {version}"
120            );
121            assert_eq!(
122                client_method_defined_in(version, "server/discover"),
123                crate::domain::discover_defined_in(version),
124                "server/discover at {version}"
125            );
126        }
127    }
128
129    #[test]
130    fn tasks_are_core_only_in_2025_11_25() {
131        assert!(client_method_defined_in(
132            ProtocolVersion::V2025_11_25,
133            "tasks/get"
134        ));
135        assert!(!client_method_defined_in(
136            ProtocolVersion::V2025_06_18,
137            "tasks/get"
138        ));
139        assert!(!client_method_defined_in(
140            ProtocolVersion::V2026_07_28,
141            "tasks/get"
142        ));
143    }
144
145    #[test]
146    fn the_replaced_methods_are_gone_in_2026_07_28() {
147        for method in [
148            "ping",
149            "logging/setLevel",
150            "resources/subscribe",
151            "resources/unsubscribe",
152        ] {
153            assert!(client_method_defined_in(
154                ProtocolVersion::V2025_11_25,
155                method
156            ));
157            assert!(
158                !client_method_defined_in(ProtocolVersion::V2026_07_28, method),
159                "{method}"
160            );
161        }
162    }
163
164    #[test]
165    fn an_unknown_method_is_defined_nowhere() {
166        assert!(!client_method_defined_in(
167            ProtocolVersion::V2025_11_25,
168            "unknown/method"
169        ));
170        assert!(!client_method_defined_in(
171            ProtocolVersion::V2026_07_28,
172            "unknown/method"
173        ));
174    }
175}