use std::borrow::Cow;
use rmcp::ErrorData as McpError;
use rmcp::model::{
CallToolRequestMethod, CallToolRequestParams, ConstString as _, CustomRequest,
DiscoverRequestMethod, DiscoverRequestParams, ErrorCode, InitializeRequestParams,
InitializeResultMethod, JsonObject, ListResourceTemplatesRequestMethod,
ListResourcesRequestMethod, ListToolsRequestMethod, PaginatedRequestParams, PingRequestMethod,
ReadResourceRequestMethod, ReadResourceRequestParams, SubscribeRequestMethod,
SubscribeRequestParams, SubscriptionsListenRequestMethod, SubscriptionsListenRequestParams,
UnsubscribeRequestMethod, UnsubscribeRequestParams,
};
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::error::spelled_enum;
use crate::util::{MAX_ECHOED_NAME_BYTES, MAX_ERROR_MESSAGE_CALLER_BYTES, escape_bounded};
spelled_enum! {
Served {
Initialize => InitializeResultMethod::VALUE,
Ping => PingRequestMethod::VALUE,
Discover => DiscoverRequestMethod::VALUE,
ToolsList => ListToolsRequestMethod::VALUE,
ToolsCall => CallToolRequestMethod::VALUE,
ResourcesList => ListResourcesRequestMethod::VALUE,
ResourcesTemplatesList => ListResourceTemplatesRequestMethod::VALUE,
ResourcesRead => ReadResourceRequestMethod::VALUE,
ResourcesSubscribe => SubscribeRequestMethod::VALUE,
ResourcesUnsubscribe => UnsubscribeRequestMethod::VALUE,
SubscriptionsListen => SubscriptionsListenRequestMethod::VALUE,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SessionKind {
Legacy,
Modern,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Lifecycle {
Any,
LegacyOnly,
ModernOnly,
}
impl Served {
fn of(method: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|served| served.as_str() == method)
}
const fn lifecycle(self) -> Lifecycle {
match self {
Self::Ping | Self::ResourcesSubscribe | Self::ResourcesUnsubscribe => {
Lifecycle::LegacyOnly
}
Self::SubscriptionsListen => Lifecycle::ModernOnly,
Self::Initialize
| Self::Discover
| Self::ToolsList
| Self::ToolsCall
| Self::ResourcesList
| Self::ResourcesTemplatesList
| Self::ResourcesRead => Lifecycle::Any,
}
}
const fn available(self, kind: SessionKind) -> bool {
match (self.lifecycle(), kind) {
(Lifecycle::Any, _)
| (Lifecycle::LegacyOnly, SessionKind::Legacy)
| (Lifecycle::ModernOnly, SessionKind::Modern) => true,
(Lifecycle::LegacyOnly, SessionKind::Modern)
| (Lifecycle::ModernOnly, SessionKind::Legacy) => false,
}
}
fn decode(self, params: Option<&Value>) -> Result<(), serde_json::Error> {
fn check<P: DeserializeOwned>(params: Option<&Value>) -> Result<(), serde_json::Error> {
P::deserialize(params.unwrap_or(&Value::Null)).map(drop)
}
match self {
Self::Initialize => check::<InitializeRequestParams>(params),
Self::Ping => check::<Option<JsonObject>>(params),
Self::Discover => check::<DiscoverRequestParams>(params),
Self::ToolsList | Self::ResourcesList | Self::ResourcesTemplatesList => {
check::<Option<PaginatedRequestParams>>(params)
}
Self::ToolsCall => check::<CallToolRequestParams>(params),
Self::ResourcesRead => check::<ReadResourceRequestParams>(params),
Self::ResourcesSubscribe => check::<SubscribeRequestParams>(params),
Self::ResourcesUnsubscribe => check::<UnsubscribeRequestParams>(params),
Self::SubscriptionsListen => check::<SubscriptionsListenRequestParams>(params),
}
}
}
fn method_not_found(method: &str) -> McpError {
McpError::new(
ErrorCode::METHOD_NOT_FOUND,
Cow::Owned(escape_bounded(method, MAX_ECHOED_NAME_BYTES)),
None,
)
}
pub(super) fn answer(request: &CustomRequest, kind: SessionKind) -> McpError {
let Some(served) = Served::of(&request.method) else {
return method_not_found(&request.method);
};
if !served.available(kind) {
return method_not_found(&request.method);
}
let name = served.as_str();
match served.decode(request.params.as_ref()) {
Err(error) => McpError::invalid_params(
format!(
"invalid params for `{name}`: {}",
escape_bounded(&error.to_string(), MAX_ERROR_MESSAGE_CALLER_BYTES)
),
None,
),
Ok(()) => McpError::invalid_params(
format!("invalid params for `{name}`: the params are malformed (duplicate field)"),
None,
),
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::util::TRUNCATION_MARKER;
fn request(method: &str, params: Option<Value>) -> CustomRequest {
CustomRequest::new(method, params)
}
fn code(error: &McpError) -> ErrorCode {
error.code
}
#[test]
fn malformed_params_of_a_served_method_are_invalid_params() {
for (method, params) in [
("tools/call", Some(json!([]))),
("tools/call", Some(json!({"name": 5}))),
("tools/call", Some(json!({}))),
("tools/call", None),
("resources/read", Some(json!({}))),
("resources/read", Some(json!({"uri": 5}))),
("resources/subscribe", Some(json!({}))),
("tools/list", Some(json!(5))),
("resources/list", Some(json!("x"))),
("resources/templates/list", Some(json!(5))),
("ping", Some(json!(5))),
("initialize", Some(json!({}))),
] {
let error = answer(&request(method, params), SessionKind::Legacy);
assert_eq!(code(&error), ErrorCode::INVALID_PARAMS, "{method}");
assert!(
error
.message
.starts_with(&format!("invalid params for `{method}`: ")),
"{}",
error.message
);
}
}
#[test]
fn a_method_mcpls_does_not_serve_is_not_found() {
let error = answer(&request("foo/bar", None), SessionKind::Legacy);
assert_eq!(code(&error), ErrorCode::METHOD_NOT_FOUND);
assert_eq!(error.message, "foo/bar");
}
#[test]
fn lifecycle_gated_methods_are_not_found_on_the_other_kind_of_session() {
let subscribe = request("resources/subscribe", Some(json!({})));
assert_eq!(
code(&answer(&subscribe, SessionKind::Legacy)),
ErrorCode::INVALID_PARAMS
);
assert_eq!(
code(&answer(&subscribe, SessionKind::Modern)),
ErrorCode::METHOD_NOT_FOUND
);
let listen = request("subscriptions/listen", Some(json!({})));
assert_eq!(
code(&answer(&listen, SessionKind::Legacy)),
ErrorCode::METHOD_NOT_FOUND
);
assert_eq!(
code(&answer(&listen, SessionKind::Modern)),
ErrorCode::INVALID_PARAMS
);
}
#[test]
fn params_that_decode_get_a_fixed_text() {
let initialize = json!({
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {"name": "c", "version": "1"},
});
for (method, params) in [
("tools/list", None),
("initialize", Some(initialize)),
("tools/call", Some(json!({"name": "get_hover"}))),
] {
let error = answer(&request(method, params), SessionKind::Legacy);
assert_eq!(code(&error), ErrorCode::INVALID_PARAMS, "{method}");
assert!(
error.message.ends_with("(duplicate field)"),
"{}",
error.message
);
}
}
#[test]
fn methods_the_sdk_answers_but_mcpls_does_not_advertise_stay_not_found() {
for method in ["completion/complete", "prompts/list"] {
let error = answer(&request(method, Some(json!({}))), SessionKind::Legacy);
assert_eq!(code(&error), ErrorCode::METHOD_NOT_FOUND, "{method}");
}
}
#[test]
fn ping_is_served_on_legacy_sessions_only() {
let ping = request("ping", Some(json!(5)));
assert_eq!(
code(&answer(&ping, SessionKind::Legacy)),
ErrorCode::INVALID_PARAMS
);
assert_eq!(
code(&answer(&ping, SessionKind::Modern)),
ErrorCode::METHOD_NOT_FOUND
);
}
#[test]
fn echoed_text_is_bounded() {
let long = "Z".repeat(1 << 20);
let unknown = answer(&request(&format!("x/{long}"), None), SessionKind::Legacy);
assert_eq!(code(&unknown), ErrorCode::METHOD_NOT_FOUND);
assert!(unknown.message.len() <= MAX_ECHOED_NAME_BYTES + TRUNCATION_MARKER.len());
assert!(unknown.message.ends_with(TRUNCATION_MARKER));
let invalid = answer(
&request("tools/call", Some(json!({"name": long}))),
SessionKind::Legacy,
);
assert!(
invalid.message.len() <= 2 * MAX_ERROR_MESSAGE_CALLER_BYTES,
"{}",
invalid.message.len()
);
let invalid = answer(
&request("tools/call", Some(json!(long))),
SessionKind::Legacy,
);
assert_eq!(code(&invalid), ErrorCode::INVALID_PARAMS);
assert!(
invalid.message.len() <= 2 * MAX_ERROR_MESSAGE_CALLER_BYTES,
"{}",
invalid.message.len()
);
}
}