Skip to main content

mcpr_core/protocol/
mcp.rs

1//! MCP message taxonomy (spec v2025-11-25).
2//!
3//! See `PIPELINE.md` §Types. Method identity
4//! is a cheap enum — one string match per message. Grouping by feature
5//! area (`Tools`, `Resources`, …) matches the spec table and lets
6//! middlewares pattern-match at the granularity they need.
7//!
8//! Every method enum has an `Unknown(String)` tail variant so non-spec
9//! methods forward unchanged instead of failing classification.
10
11use super::jsonrpc::JsonRpcEnvelope;
12
13/// Shallow envelope paired with its classification. Used inside
14/// `McpRequest` (client direction) and `Response::McpBuffered` (server
15/// direction).
16#[derive(Debug, Clone)]
17pub struct McpMessage {
18    pub envelope: JsonRpcEnvelope,
19    pub kind: MessageKind,
20}
21
22/// Direction discriminator for an `McpMessage`.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum MessageKind {
25    Client(ClientKind),
26    Server(ServerKind),
27}
28
29// ── Client → Server ──────────────────────────────────────────
30
31/// Kind of message the client is sending. Computed at intake from
32/// `method` + `id` + `result`/`error` presence.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum ClientKind {
35    Request(ClientMethod),
36    Notification(ClientNotifMethod),
37    Result,
38    Error,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum ClientMethod {
43    Ping,
44    Lifecycle(LifecycleMethod),
45    Tools(ToolsMethod),
46    Resources(ResourcesMethod),
47    Prompts(PromptsMethod),
48    Completion(CompletionMethod),
49    Logging(LoggingMethod),
50    Tasks(TasksMethod),
51    Unknown(String),
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum LifecycleMethod {
56    Initialize,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum ToolsMethod {
61    List,
62    Call,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum ResourcesMethod {
67    List,
68    TemplatesList,
69    Read,
70    Subscribe,
71    Unsubscribe,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum PromptsMethod {
76    List,
77    Get,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum CompletionMethod {
82    Complete,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum LoggingMethod {
87    SetLevel,
88}
89
90/// Task lifecycle methods. Used by both directions (client asks the
91/// server about tasks; server can also request task state from the
92/// client).
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum TasksMethod {
95    List,
96    Get,
97    Result,
98    Cancel,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum ClientNotifMethod {
103    Initialized,
104    Cancelled,
105    Progress,
106    RootsListChanged,
107    TaskStatus,
108    Unknown(String),
109}
110
111// ── Server → Client ──────────────────────────────────────────
112
113/// Kind of message the server is sending. Appears in response bodies
114/// (streamable-HTTP chunks or legacy SSE frames).
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum ServerKind {
117    Request(ServerMethod),
118    Notification(ServerNotifMethod),
119    Result,
120    Error,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum ServerMethod {
125    Ping,
126    Sampling,
127    Elicitation,
128    Roots,
129    Tasks(TasksMethod),
130    Unknown(String),
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum ServerNotifMethod {
135    Cancelled,
136    Progress,
137    LogMessage,
138    ResourcesListChanged,
139    ResourceUpdated,
140    ToolsListChanged,
141    PromptsListChanged,
142    ElicitationComplete,
143    TaskStatus,
144    Unknown(String),
145}
146
147// ── Method parsing ────────────────────────────────────────────
148
149impl ClientMethod {
150    pub fn parse(method: &str) -> Self {
151        match method {
152            "ping" => Self::Ping,
153            "initialize" => Self::Lifecycle(LifecycleMethod::Initialize),
154            "tools/list" => Self::Tools(ToolsMethod::List),
155            "tools/call" => Self::Tools(ToolsMethod::Call),
156            "resources/list" => Self::Resources(ResourcesMethod::List),
157            "resources/templates/list" => Self::Resources(ResourcesMethod::TemplatesList),
158            "resources/read" => Self::Resources(ResourcesMethod::Read),
159            "resources/subscribe" => Self::Resources(ResourcesMethod::Subscribe),
160            "resources/unsubscribe" => Self::Resources(ResourcesMethod::Unsubscribe),
161            "prompts/list" => Self::Prompts(PromptsMethod::List),
162            "prompts/get" => Self::Prompts(PromptsMethod::Get),
163            "completion/complete" => Self::Completion(CompletionMethod::Complete),
164            "logging/setLevel" => Self::Logging(LoggingMethod::SetLevel),
165            "tasks/list" => Self::Tasks(TasksMethod::List),
166            "tasks/get" => Self::Tasks(TasksMethod::Get),
167            "tasks/result" => Self::Tasks(TasksMethod::Result),
168            "tasks/cancel" => Self::Tasks(TasksMethod::Cancel),
169            other => Self::Unknown(other.to_owned()),
170        }
171    }
172
173    /// Inverse of [`ClientMethod::parse`]. Returns `None` for
174    /// `Self::Unknown(_)` — unknown methods don't have a canonical
175    /// spec string.
176    pub fn as_str(&self) -> Option<&'static str> {
177        Some(match self {
178            Self::Ping => "ping",
179            Self::Lifecycle(LifecycleMethod::Initialize) => "initialize",
180            Self::Tools(ToolsMethod::List) => "tools/list",
181            Self::Tools(ToolsMethod::Call) => "tools/call",
182            Self::Resources(ResourcesMethod::List) => "resources/list",
183            Self::Resources(ResourcesMethod::TemplatesList) => "resources/templates/list",
184            Self::Resources(ResourcesMethod::Read) => "resources/read",
185            Self::Resources(ResourcesMethod::Subscribe) => "resources/subscribe",
186            Self::Resources(ResourcesMethod::Unsubscribe) => "resources/unsubscribe",
187            Self::Prompts(PromptsMethod::List) => "prompts/list",
188            Self::Prompts(PromptsMethod::Get) => "prompts/get",
189            Self::Completion(CompletionMethod::Complete) => "completion/complete",
190            Self::Logging(LoggingMethod::SetLevel) => "logging/setLevel",
191            Self::Tasks(TasksMethod::List) => "tasks/list",
192            Self::Tasks(TasksMethod::Get) => "tasks/get",
193            Self::Tasks(TasksMethod::Result) => "tasks/result",
194            Self::Tasks(TasksMethod::Cancel) => "tasks/cancel",
195            Self::Unknown(_) => return None,
196        })
197    }
198}
199
200impl ClientNotifMethod {
201    pub fn parse(method: &str) -> Self {
202        match method {
203            "notifications/initialized" => Self::Initialized,
204            "notifications/cancelled" => Self::Cancelled,
205            "notifications/progress" => Self::Progress,
206            "notifications/roots/list_changed" => Self::RootsListChanged,
207            "notifications/tasks/status" => Self::TaskStatus,
208            other => Self::Unknown(other.to_owned()),
209        }
210    }
211}
212
213impl ServerMethod {
214    pub fn parse(method: &str) -> Self {
215        match method {
216            "ping" => Self::Ping,
217            "sampling/createMessage" => Self::Sampling,
218            "elicitation/create" => Self::Elicitation,
219            "roots/list" => Self::Roots,
220            "tasks/list" => Self::Tasks(TasksMethod::List),
221            "tasks/get" => Self::Tasks(TasksMethod::Get),
222            "tasks/result" => Self::Tasks(TasksMethod::Result),
223            "tasks/cancel" => Self::Tasks(TasksMethod::Cancel),
224            other => Self::Unknown(other.to_owned()),
225        }
226    }
227}
228
229impl ServerNotifMethod {
230    pub fn parse(method: &str) -> Self {
231        match method {
232            "notifications/cancelled" => Self::Cancelled,
233            "notifications/progress" => Self::Progress,
234            "notifications/message" => Self::LogMessage,
235            "notifications/resources/list_changed" => Self::ResourcesListChanged,
236            "notifications/resources/updated" => Self::ResourceUpdated,
237            "notifications/tools/list_changed" => Self::ToolsListChanged,
238            "notifications/prompts/list_changed" => Self::PromptsListChanged,
239            "notifications/elicitation/complete" => Self::ElicitationComplete,
240            "notifications/tasks/status" => Self::TaskStatus,
241            other => Self::Unknown(other.to_owned()),
242        }
243    }
244}
245
246// ── Classification ────────────────────────────────────────────
247
248/// Classify a client→server envelope. Assumes the envelope came from
249/// [`JsonRpcEnvelope::parse`], which already rejected malformed shapes.
250pub fn classify_client(env: &JsonRpcEnvelope) -> ClientKind {
251    match (
252        env.method.as_deref(),
253        env.id.is_some(),
254        env.result.is_some(),
255        env.error.is_some(),
256    ) {
257        (Some(m), true, false, false) => ClientKind::Request(ClientMethod::parse(m)),
258        (Some(m), false, false, false) => ClientKind::Notification(ClientNotifMethod::parse(m)),
259        (None, true, true, false) => ClientKind::Result,
260        (None, true, false, true) => ClientKind::Error,
261        _ => {
262            debug_assert!(
263                false,
264                "classify_client: envelope shape should have been rejected by parse",
265            );
266            ClientKind::Error
267        }
268    }
269}
270
271/// Classify a server→client envelope.
272pub fn classify_server(env: &JsonRpcEnvelope) -> ServerKind {
273    match (
274        env.method.as_deref(),
275        env.id.is_some(),
276        env.result.is_some(),
277        env.error.is_some(),
278    ) {
279        (Some(m), true, false, false) => ServerKind::Request(ServerMethod::parse(m)),
280        (Some(m), false, false, false) => ServerKind::Notification(ServerNotifMethod::parse(m)),
281        (None, true, true, false) => ServerKind::Result,
282        (None, true, false, true) => ServerKind::Error,
283        _ => {
284            debug_assert!(
285                false,
286                "classify_server: envelope shape should have been rejected by parse",
287            );
288            ServerKind::Error
289        }
290    }
291}
292
293#[cfg(test)]
294#[allow(non_snake_case)]
295mod tests {
296    use super::*;
297
298    fn parsed(bytes: &[u8]) -> JsonRpcEnvelope {
299        JsonRpcEnvelope::parse(bytes).unwrap()
300    }
301
302    // ── ClientMethod::parse coverage ─────────────────────────
303
304    #[test]
305    fn client_method__spec_coverage() {
306        let cases: &[(&str, ClientMethod)] = &[
307            ("ping", ClientMethod::Ping),
308            (
309                "initialize",
310                ClientMethod::Lifecycle(LifecycleMethod::Initialize),
311            ),
312            ("tools/list", ClientMethod::Tools(ToolsMethod::List)),
313            ("tools/call", ClientMethod::Tools(ToolsMethod::Call)),
314            (
315                "resources/list",
316                ClientMethod::Resources(ResourcesMethod::List),
317            ),
318            (
319                "resources/templates/list",
320                ClientMethod::Resources(ResourcesMethod::TemplatesList),
321            ),
322            (
323                "resources/read",
324                ClientMethod::Resources(ResourcesMethod::Read),
325            ),
326            (
327                "resources/subscribe",
328                ClientMethod::Resources(ResourcesMethod::Subscribe),
329            ),
330            (
331                "resources/unsubscribe",
332                ClientMethod::Resources(ResourcesMethod::Unsubscribe),
333            ),
334            ("prompts/list", ClientMethod::Prompts(PromptsMethod::List)),
335            ("prompts/get", ClientMethod::Prompts(PromptsMethod::Get)),
336            (
337                "completion/complete",
338                ClientMethod::Completion(CompletionMethod::Complete),
339            ),
340            (
341                "logging/setLevel",
342                ClientMethod::Logging(LoggingMethod::SetLevel),
343            ),
344            ("tasks/list", ClientMethod::Tasks(TasksMethod::List)),
345            ("tasks/get", ClientMethod::Tasks(TasksMethod::Get)),
346            ("tasks/result", ClientMethod::Tasks(TasksMethod::Result)),
347            ("tasks/cancel", ClientMethod::Tasks(TasksMethod::Cancel)),
348        ];
349        for (m, expected) in cases {
350            assert_eq!(ClientMethod::parse(m), *expected, "method = {m}");
351        }
352    }
353
354    #[test]
355    fn client_method__unknown_preserves_string() {
356        assert_eq!(
357            ClientMethod::parse("tools/future-method"),
358            ClientMethod::Unknown("tools/future-method".into()),
359        );
360    }
361
362    // ── ClientNotifMethod::parse coverage ────────────────────
363
364    #[test]
365    fn client_notif_method__spec_coverage() {
366        let cases: &[(&str, ClientNotifMethod)] = &[
367            ("notifications/initialized", ClientNotifMethod::Initialized),
368            ("notifications/cancelled", ClientNotifMethod::Cancelled),
369            ("notifications/progress", ClientNotifMethod::Progress),
370            (
371                "notifications/roots/list_changed",
372                ClientNotifMethod::RootsListChanged,
373            ),
374            ("notifications/tasks/status", ClientNotifMethod::TaskStatus),
375        ];
376        for (m, expected) in cases {
377            assert_eq!(ClientNotifMethod::parse(m), *expected, "method = {m}");
378        }
379    }
380
381    #[test]
382    fn client_notif_method__unknown_preserves_string() {
383        assert_eq!(
384            ClientNotifMethod::parse("notifications/something"),
385            ClientNotifMethod::Unknown("notifications/something".into()),
386        );
387    }
388
389    // ── ServerMethod::parse coverage ─────────────────────────
390
391    #[test]
392    fn server_method__spec_coverage() {
393        let cases: &[(&str, ServerMethod)] = &[
394            ("ping", ServerMethod::Ping),
395            ("sampling/createMessage", ServerMethod::Sampling),
396            ("elicitation/create", ServerMethod::Elicitation),
397            ("roots/list", ServerMethod::Roots),
398            ("tasks/list", ServerMethod::Tasks(TasksMethod::List)),
399            ("tasks/get", ServerMethod::Tasks(TasksMethod::Get)),
400            ("tasks/result", ServerMethod::Tasks(TasksMethod::Result)),
401            ("tasks/cancel", ServerMethod::Tasks(TasksMethod::Cancel)),
402        ];
403        for (m, expected) in cases {
404            assert_eq!(ServerMethod::parse(m), *expected, "method = {m}");
405        }
406    }
407
408    #[test]
409    fn server_method__unknown_preserves_string() {
410        assert_eq!(
411            ServerMethod::parse("custom/method"),
412            ServerMethod::Unknown("custom/method".into()),
413        );
414    }
415
416    // ── ServerNotifMethod::parse coverage ────────────────────
417
418    #[test]
419    fn server_notif_method__spec_coverage() {
420        let cases: &[(&str, ServerNotifMethod)] = &[
421            ("notifications/cancelled", ServerNotifMethod::Cancelled),
422            ("notifications/progress", ServerNotifMethod::Progress),
423            ("notifications/message", ServerNotifMethod::LogMessage),
424            (
425                "notifications/resources/list_changed",
426                ServerNotifMethod::ResourcesListChanged,
427            ),
428            (
429                "notifications/resources/updated",
430                ServerNotifMethod::ResourceUpdated,
431            ),
432            (
433                "notifications/tools/list_changed",
434                ServerNotifMethod::ToolsListChanged,
435            ),
436            (
437                "notifications/prompts/list_changed",
438                ServerNotifMethod::PromptsListChanged,
439            ),
440            (
441                "notifications/elicitation/complete",
442                ServerNotifMethod::ElicitationComplete,
443            ),
444            ("notifications/tasks/status", ServerNotifMethod::TaskStatus),
445        ];
446        for (m, expected) in cases {
447            assert_eq!(ServerNotifMethod::parse(m), *expected, "method = {m}");
448        }
449    }
450
451    #[test]
452    fn server_notif_method__unknown_preserves_string() {
453        assert_eq!(
454            ServerNotifMethod::parse("notifications/future"),
455            ServerNotifMethod::Unknown("notifications/future".into()),
456        );
457    }
458
459    // ── classify_client ──────────────────────────────────────
460
461    #[test]
462    fn classify_client__request() {
463        let e = parsed(br#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#);
464        assert_eq!(
465            classify_client(&e),
466            ClientKind::Request(ClientMethod::Tools(ToolsMethod::List)),
467        );
468    }
469
470    #[test]
471    fn classify_client__notification() {
472        let e = parsed(br#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#);
473        assert_eq!(
474            classify_client(&e),
475            ClientKind::Notification(ClientNotifMethod::Initialized),
476        );
477    }
478
479    #[test]
480    fn classify_client__result() {
481        let e = parsed(br#"{"jsonrpc":"2.0","id":1,"result":{}}"#);
482        assert_eq!(classify_client(&e), ClientKind::Result);
483    }
484
485    #[test]
486    fn classify_client__error() {
487        let e = parsed(br#"{"jsonrpc":"2.0","id":1,"error":{"code":-1,"message":"x"}}"#);
488        assert_eq!(classify_client(&e), ClientKind::Error);
489    }
490
491    #[test]
492    fn classify_client__unknown_method() {
493        let e = parsed(br#"{"jsonrpc":"2.0","id":1,"method":"custom/method"}"#);
494        assert_eq!(
495            classify_client(&e),
496            ClientKind::Request(ClientMethod::Unknown("custom/method".into())),
497        );
498    }
499
500    // ── classify_server ──────────────────────────────────────
501
502    #[test]
503    fn classify_server__request() {
504        let e = parsed(br#"{"jsonrpc":"2.0","id":1,"method":"sampling/createMessage"}"#);
505        assert_eq!(
506            classify_server(&e),
507            ServerKind::Request(ServerMethod::Sampling),
508        );
509    }
510
511    #[test]
512    fn classify_server__notification() {
513        let e = parsed(br#"{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}"#);
514        assert_eq!(
515            classify_server(&e),
516            ServerKind::Notification(ServerNotifMethod::ToolsListChanged),
517        );
518    }
519
520    #[test]
521    fn classify_server__result() {
522        let e = parsed(br#"{"jsonrpc":"2.0","id":1,"result":{}}"#);
523        assert_eq!(classify_server(&e), ServerKind::Result);
524    }
525
526    #[test]
527    fn classify_server__error() {
528        let e = parsed(br#"{"jsonrpc":"2.0","id":1,"error":{"code":-1,"message":"x"}}"#);
529        assert_eq!(classify_server(&e), ServerKind::Error);
530    }
531}