Skip to main content

subcog/mcp/
dispatch.rs

1//! MCP method dispatch using command pattern.
2//!
3//! This module implements a command pattern for MCP method dispatch,
4//! replacing string matching with type-safe enum variants.
5//!
6//! # Architecture
7//!
8//! ```text
9//! McpMethod (enum)
10//!   ├── Initialize
11//!   ├── ListTools
12//!   ├── CallTool
13//!   ├── ListResources
14//!   ├── ReadResource
15//!   ├── ListPrompts
16//!   ├── GetPrompt
17//!   ├── Ping
18//!   └── Unknown(String)
19//! ```
20//!
21//! # Open/Closed Principle
22//!
23//! To add a new method:
24//! 1. Add a variant to [`McpMethod`]
25//! 2. Update [`McpMethod::from_str`] parsing
26//! 3. Add handler in [`McpServer::dispatch_method`]
27//!
28//! The dispatch logic is centralized and type-safe.
29
30use std::fmt;
31
32/// MCP method identifier.
33///
34/// Represents all supported MCP protocol methods with type-safe variants.
35/// Unknown methods are captured for proper error reporting.
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub enum McpMethod {
38    /// Initialize the MCP session.
39    Initialize,
40    /// List available tools.
41    ListTools,
42    /// Call a specific tool.
43    CallTool,
44    /// List available resources.
45    ListResources,
46    /// Read a specific resource.
47    ReadResource,
48    /// List available prompts.
49    ListPrompts,
50    /// Get a specific prompt.
51    GetPrompt,
52    /// Ping the server (health check).
53    Ping,
54    /// Unknown method (for error handling).
55    Unknown(String),
56}
57
58impl McpMethod {
59    /// Returns the MCP protocol method name.
60    #[must_use]
61    pub const fn as_str(&self) -> &str {
62        match self {
63            Self::Initialize => "initialize",
64            Self::ListTools => "tools/list",
65            Self::CallTool => "tools/call",
66            Self::ListResources => "resources/list",
67            Self::ReadResource => "resources/read",
68            Self::ListPrompts => "prompts/list",
69            Self::GetPrompt => "prompts/get",
70            Self::Ping => "ping",
71            Self::Unknown(s) => s.as_str(),
72        }
73    }
74
75    /// Returns true if this is a known method.
76    #[must_use]
77    #[allow(dead_code)] // Useful for introspection and testing
78    pub const fn is_known(&self) -> bool {
79        !matches!(self, Self::Unknown(_))
80    }
81
82    /// Returns all known methods.
83    #[must_use]
84    #[allow(dead_code)] // Useful for introspection and testing
85    pub const fn known_methods() -> &'static [Self] {
86        &[
87            Self::Initialize,
88            Self::ListTools,
89            Self::CallTool,
90            Self::ListResources,
91            Self::ReadResource,
92            Self::ListPrompts,
93            Self::GetPrompt,
94            Self::Ping,
95        ]
96    }
97}
98
99impl From<&str> for McpMethod {
100    fn from(s: &str) -> Self {
101        match s {
102            "initialize" => Self::Initialize,
103            "tools/list" => Self::ListTools,
104            "tools/call" => Self::CallTool,
105            "resources/list" => Self::ListResources,
106            "resources/read" => Self::ReadResource,
107            "prompts/list" => Self::ListPrompts,
108            "prompts/get" => Self::GetPrompt,
109            "ping" => Self::Ping,
110            unknown => Self::Unknown(unknown.to_string()),
111        }
112    }
113}
114
115impl fmt::Display for McpMethod {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        write!(f, "{}", self.as_str())
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_method_from_str() {
127        assert_eq!(McpMethod::from("initialize"), McpMethod::Initialize);
128        assert_eq!(McpMethod::from("tools/list"), McpMethod::ListTools);
129        assert_eq!(McpMethod::from("tools/call"), McpMethod::CallTool);
130        assert_eq!(McpMethod::from("resources/list"), McpMethod::ListResources);
131        assert_eq!(McpMethod::from("resources/read"), McpMethod::ReadResource);
132        assert_eq!(McpMethod::from("prompts/list"), McpMethod::ListPrompts);
133        assert_eq!(McpMethod::from("prompts/get"), McpMethod::GetPrompt);
134        assert_eq!(McpMethod::from("ping"), McpMethod::Ping);
135    }
136
137    #[test]
138    fn test_unknown_method() {
139        let method = McpMethod::from("unknown/method");
140        assert!(!method.is_known());
141        assert_eq!(method.as_str(), "unknown/method");
142    }
143
144    #[test]
145    fn test_method_as_str_roundtrip() {
146        for method in McpMethod::known_methods() {
147            let s = method.as_str();
148            let parsed = McpMethod::from(s);
149            assert_eq!(&parsed, method, "Roundtrip failed for {method}");
150        }
151    }
152
153    #[test]
154    fn test_method_display() {
155        assert_eq!(format!("{}", McpMethod::Initialize), "initialize");
156        assert_eq!(format!("{}", McpMethod::ListTools), "tools/list");
157        assert_eq!(format!("{}", McpMethod::Unknown("foo".to_string())), "foo");
158    }
159
160    #[test]
161    fn test_known_methods_count() {
162        // Ensure we have all 8 known methods
163        assert_eq!(McpMethod::known_methods().len(), 8);
164    }
165
166    #[test]
167    fn test_all_known_methods_are_known() {
168        for method in McpMethod::known_methods() {
169            assert!(method.is_known(), "{method} should be known");
170        }
171    }
172}