1use std::fmt;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub enum Method {
20 ToolsCall,
22 ToolsList,
24 ResourcesRead,
26 ResourcesList,
28 ResourcesTemplatesList,
30 PromptsGet,
32 PromptsList,
34 ServerDiscover,
36 TasksGet,
38 TasksUpdate,
40 TasksCancel,
42 SubscriptionsListen,
44 Other(String),
46}
47
48impl Method {
49 #[must_use]
56 pub fn parse(raw: &str) -> Self {
57 match raw {
58 "tools/call" => Self::ToolsCall,
59 "tools/list" => Self::ToolsList,
60 "resources/read" => Self::ResourcesRead,
61 "resources/list" => Self::ResourcesList,
62 "resources/templates/list" => Self::ResourcesTemplatesList,
63 "prompts/get" => Self::PromptsGet,
64 "prompts/list" => Self::PromptsList,
65 "server/discover" => Self::ServerDiscover,
66 "tasks/get" => Self::TasksGet,
67 "tasks/update" => Self::TasksUpdate,
68 "tasks/cancel" => Self::TasksCancel,
69 "subscriptions/listen" => Self::SubscriptionsListen,
70 other => Self::Other(other.to_owned()),
71 }
72 }
73
74 #[must_use]
76 pub fn as_str(&self) -> &str {
77 match self {
78 Self::ToolsCall => "tools/call",
79 Self::ToolsList => "tools/list",
80 Self::ResourcesRead => "resources/read",
81 Self::ResourcesList => "resources/list",
82 Self::ResourcesTemplatesList => "resources/templates/list",
83 Self::PromptsGet => "prompts/get",
84 Self::PromptsList => "prompts/list",
85 Self::ServerDiscover => "server/discover",
86 Self::TasksGet => "tasks/get",
87 Self::TasksUpdate => "tasks/update",
88 Self::TasksCancel => "tasks/cancel",
89 Self::SubscriptionsListen => "subscriptions/listen",
90 Self::Other(s) => s,
91 }
92 }
93
94 #[must_use]
100 pub const fn carries_name(&self) -> bool {
101 matches!(
102 self,
103 Self::ToolsCall | Self::ResourcesRead | Self::PromptsGet
104 )
105 }
106
107 #[must_use]
117 pub const fn supports_mrtr(&self) -> bool {
118 matches!(
119 self,
120 Self::ToolsCall | Self::ResourcesRead | Self::PromptsGet
121 )
122 }
123
124 #[must_use]
130 pub const fn is_cacheable(&self) -> bool {
131 matches!(
132 self,
133 Self::ServerDiscover
134 | Self::ToolsList
135 | Self::PromptsList
136 | Self::ResourcesList
137 | Self::ResourcesTemplatesList
138 | Self::ResourcesRead
139 )
140 }
141
142 #[must_use]
148 pub const fn is_discovery(&self) -> bool {
149 matches!(
150 self,
151 Self::ServerDiscover
152 | Self::ToolsList
153 | Self::PromptsList
154 | Self::ResourcesList
155 | Self::ResourcesTemplatesList
156 )
157 }
158
159 #[must_use]
166 pub const fn is_task_drive(&self) -> bool {
167 matches!(self, Self::TasksGet | Self::TasksUpdate | Self::TasksCancel)
168 }
169}
170
171impl fmt::Display for Method {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 f.write_str(self.as_str())
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn parses_every_core_method() {
183 for wire in [
184 "tools/call",
185 "tools/list",
186 "resources/read",
187 "resources/list",
188 "resources/templates/list",
189 "prompts/get",
190 "prompts/list",
191 "server/discover",
192 "tasks/get",
193 "tasks/update",
194 "tasks/cancel",
195 "subscriptions/listen",
196 ] {
197 let m = Method::parse(wire);
198 assert!(
199 !matches!(m, Method::Other(_)),
200 "{wire} fell through to Other"
201 );
202 assert_eq!(m.as_str(), wire, "round trip failed for {wire}");
203 }
204 }
205
206 #[test]
207 fn unknown_methods_are_retained_verbatim() {
208 let m = Method::parse("io.example/frobnicate");
209 assert_eq!(m, Method::Other("io.example/frobnicate".to_owned()));
210 assert_eq!(m.as_str(), "io.example/frobnicate");
211 }
212
213 #[test]
214 fn method_values_are_case_sensitive() {
215 assert!(matches!(Method::parse("Tools/Call"), Method::Other(_)));
218 assert!(matches!(Method::parse("TOOLS/LIST"), Method::Other(_)));
219 }
220
221 #[test]
222 fn mrtr_set_is_exactly_the_three_the_spec_allows() {
223 let allowed: Vec<Method> = [
224 "tools/call",
225 "tools/list",
226 "resources/read",
227 "resources/list",
228 "resources/templates/list",
229 "prompts/get",
230 "prompts/list",
231 "server/discover",
232 "tasks/get",
233 "tasks/update",
234 "tasks/cancel",
235 "subscriptions/listen",
236 "io.example/other",
237 ]
238 .into_iter()
239 .map(Method::parse)
240 .filter(Method::supports_mrtr)
241 .collect();
242
243 assert_eq!(
244 allowed,
245 vec![Method::ToolsCall, Method::ResourcesRead, Method::PromptsGet]
246 );
247 }
248
249 #[test]
250 fn resources_read_is_both_cacheable_and_mrtr_capable() {
251 let m = Method::ResourcesRead;
253 assert!(m.is_cacheable());
254 assert!(m.supports_mrtr());
255 }
256
257 #[test]
258 fn discovery_is_cacheable_but_lists_are_not_mrtr_capable() {
259 for m in [
260 Method::ToolsList,
261 Method::PromptsList,
262 Method::ResourcesList,
263 Method::ResourcesTemplatesList,
264 Method::ServerDiscover,
265 ] {
266 assert!(m.is_cacheable(), "{m} should be cacheable");
267 assert!(m.is_discovery(), "{m} should be discovery");
268 assert!(
269 !m.supports_mrtr(),
270 "{m} must not accept InputRequiredResult"
271 );
272 }
273 }
274
275 #[test]
276 fn task_drive_covers_get_update_cancel_only() {
277 assert!(Method::TasksGet.is_task_drive());
278 assert!(Method::TasksUpdate.is_task_drive());
279 assert!(Method::TasksCancel.is_task_drive());
280 assert!(!Method::ToolsCall.is_task_drive());
281 assert!(matches!(Method::parse("tasks/list"), Method::Other(_)));
283 }
284
285 #[test]
286 fn name_bearing_methods_match_the_header_requirement() {
287 assert!(Method::ToolsCall.carries_name());
288 assert!(Method::ResourcesRead.carries_name());
289 assert!(Method::PromptsGet.carries_name());
290 assert!(!Method::ToolsList.carries_name());
291 assert!(!Method::TasksGet.carries_name());
292 }
293}