use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Method {
ToolsCall,
ToolsList,
ResourcesRead,
ResourcesList,
ResourcesTemplatesList,
PromptsGet,
PromptsList,
ServerDiscover,
TasksGet,
TasksUpdate,
TasksCancel,
SubscriptionsListen,
Other(String),
}
impl Method {
#[must_use]
pub fn parse(raw: &str) -> Self {
match raw {
"tools/call" => Self::ToolsCall,
"tools/list" => Self::ToolsList,
"resources/read" => Self::ResourcesRead,
"resources/list" => Self::ResourcesList,
"resources/templates/list" => Self::ResourcesTemplatesList,
"prompts/get" => Self::PromptsGet,
"prompts/list" => Self::PromptsList,
"server/discover" => Self::ServerDiscover,
"tasks/get" => Self::TasksGet,
"tasks/update" => Self::TasksUpdate,
"tasks/cancel" => Self::TasksCancel,
"subscriptions/listen" => Self::SubscriptionsListen,
other => Self::Other(other.to_owned()),
}
}
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::ToolsCall => "tools/call",
Self::ToolsList => "tools/list",
Self::ResourcesRead => "resources/read",
Self::ResourcesList => "resources/list",
Self::ResourcesTemplatesList => "resources/templates/list",
Self::PromptsGet => "prompts/get",
Self::PromptsList => "prompts/list",
Self::ServerDiscover => "server/discover",
Self::TasksGet => "tasks/get",
Self::TasksUpdate => "tasks/update",
Self::TasksCancel => "tasks/cancel",
Self::SubscriptionsListen => "subscriptions/listen",
Self::Other(s) => s,
}
}
#[must_use]
pub const fn carries_name(&self) -> bool {
matches!(
self,
Self::ToolsCall | Self::ResourcesRead | Self::PromptsGet
)
}
#[must_use]
pub const fn supports_mrtr(&self) -> bool {
matches!(
self,
Self::ToolsCall | Self::ResourcesRead | Self::PromptsGet
)
}
#[must_use]
pub const fn is_cacheable(&self) -> bool {
matches!(
self,
Self::ServerDiscover
| Self::ToolsList
| Self::PromptsList
| Self::ResourcesList
| Self::ResourcesTemplatesList
| Self::ResourcesRead
)
}
#[must_use]
pub const fn is_discovery(&self) -> bool {
matches!(
self,
Self::ServerDiscover
| Self::ToolsList
| Self::PromptsList
| Self::ResourcesList
| Self::ResourcesTemplatesList
)
}
#[must_use]
pub const fn is_task_drive(&self) -> bool {
matches!(self, Self::TasksGet | Self::TasksUpdate | Self::TasksCancel)
}
}
impl fmt::Display for Method {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_every_core_method() {
for wire in [
"tools/call",
"tools/list",
"resources/read",
"resources/list",
"resources/templates/list",
"prompts/get",
"prompts/list",
"server/discover",
"tasks/get",
"tasks/update",
"tasks/cancel",
"subscriptions/listen",
] {
let m = Method::parse(wire);
assert!(
!matches!(m, Method::Other(_)),
"{wire} fell through to Other"
);
assert_eq!(m.as_str(), wire, "round trip failed for {wire}");
}
}
#[test]
fn unknown_methods_are_retained_verbatim() {
let m = Method::parse("io.example/frobnicate");
assert_eq!(m, Method::Other("io.example/frobnicate".to_owned()));
assert_eq!(m.as_str(), "io.example/frobnicate");
}
#[test]
fn method_values_are_case_sensitive() {
assert!(matches!(Method::parse("Tools/Call"), Method::Other(_)));
assert!(matches!(Method::parse("TOOLS/LIST"), Method::Other(_)));
}
#[test]
fn mrtr_set_is_exactly_the_three_the_spec_allows() {
let allowed: Vec<Method> = [
"tools/call",
"tools/list",
"resources/read",
"resources/list",
"resources/templates/list",
"prompts/get",
"prompts/list",
"server/discover",
"tasks/get",
"tasks/update",
"tasks/cancel",
"subscriptions/listen",
"io.example/other",
]
.into_iter()
.map(Method::parse)
.filter(Method::supports_mrtr)
.collect();
assert_eq!(
allowed,
vec![Method::ToolsCall, Method::ResourcesRead, Method::PromptsGet]
);
}
#[test]
fn resources_read_is_both_cacheable_and_mrtr_capable() {
let m = Method::ResourcesRead;
assert!(m.is_cacheable());
assert!(m.supports_mrtr());
}
#[test]
fn discovery_is_cacheable_but_lists_are_not_mrtr_capable() {
for m in [
Method::ToolsList,
Method::PromptsList,
Method::ResourcesList,
Method::ResourcesTemplatesList,
Method::ServerDiscover,
] {
assert!(m.is_cacheable(), "{m} should be cacheable");
assert!(m.is_discovery(), "{m} should be discovery");
assert!(
!m.supports_mrtr(),
"{m} must not accept InputRequiredResult"
);
}
}
#[test]
fn task_drive_covers_get_update_cancel_only() {
assert!(Method::TasksGet.is_task_drive());
assert!(Method::TasksUpdate.is_task_drive());
assert!(Method::TasksCancel.is_task_drive());
assert!(!Method::ToolsCall.is_task_drive());
assert!(matches!(Method::parse("tasks/list"), Method::Other(_)));
}
#[test]
fn name_bearing_methods_match_the_header_requirement() {
assert!(Method::ToolsCall.carries_name());
assert!(Method::ResourcesRead.carries_name());
assert!(Method::PromptsGet.carries_name());
assert!(!Method::ToolsList.carries_name());
assert!(!Method::TasksGet.carries_name());
}
}