Skip to content

Commit 5a78773

Browse files
authored
fix: return tool errors for invalid arguments (modelcontextprotocol#894)
1 parent 53c6daa commit 5a78773

1 file changed

Lines changed: 71 additions & 2 deletions

File tree

  • crates/rmcp/src/handler/server/router

crates/rmcp/src/handler/server/router/tool.rs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,24 @@ use crate::{
137137
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext},
138138
tool_name_validation::validate_and_warn_tool_name,
139139
},
140-
model::{CallToolResult, Tool, ToolAnnotations},
140+
model::{CallToolResult, Content, ErrorCode, Tool, ToolAnnotations},
141141
service::{MaybeBoxFuture, MaybeSend},
142142
};
143143

144+
const TOOL_ARGUMENT_DESERIALIZATION_ERROR_PREFIX: &str = "failed to deserialize parameters:";
145+
146+
fn into_tool_argument_error(error: crate::ErrorData) -> Result<CallToolResult, crate::ErrorData> {
147+
if error.code == ErrorCode::INVALID_PARAMS
148+
&& error
149+
.message
150+
.starts_with(TOOL_ARGUMENT_DESERIALIZATION_ERROR_PREFIX)
151+
{
152+
return Ok(CallToolResult::error(vec![Content::text(error.message)]));
153+
}
154+
155+
Err(error)
156+
}
157+
144158
#[non_exhaustive]
145159
pub struct ToolRoute<S> {
146160
#[allow(clippy::type_complexity)]
@@ -555,7 +569,10 @@ where
555569
.get(name)
556570
.ok_or_else(|| crate::ErrorData::invalid_params("tool not found", None))?;
557571

558-
let result = (item.call)(context).await?;
572+
let result = match (item.call)(context).await {
573+
Ok(result) => result,
574+
Err(error) => return into_tool_argument_error(error),
575+
};
559576

560577
Ok(result)
561578
}
@@ -611,13 +628,65 @@ mod tests {
611628
use super::*;
612629
use crate::{
613630
RoleServer,
631+
handler::server::wrapper::Parameters,
614632
model::{CallToolRequestParams, ErrorCode, NumberOrString},
615633
service::{AtomicU32RequestIdProvider, Peer, RequestContext},
616634
};
617635

618636
struct DummyService;
619637
impl crate::handler::server::ServerHandler for DummyService {}
620638

639+
#[derive(serde::Deserialize, schemars::JsonSchema)]
640+
struct RequiredParams {
641+
project: String,
642+
}
643+
644+
fn requires_params(Parameters(params): Parameters<RequiredParams>) -> String {
645+
params.project
646+
}
647+
648+
#[tokio::test]
649+
async fn test_argument_deserialization_error_returns_tool_error_result() {
650+
let service = DummyService;
651+
let router = ToolRouter::new().with_route(ToolRoute::new(
652+
crate::model::Tool::new(
653+
"requires_params",
654+
"requires params",
655+
Arc::new(Default::default()),
656+
),
657+
requires_params,
658+
));
659+
660+
let id_provider: Arc<dyn crate::service::RequestIdProvider> =
661+
Arc::new(AtomicU32RequestIdProvider::default());
662+
let (peer, _rx) = Peer::<RoleServer>::new(id_provider, None);
663+
let ctx = crate::handler::server::tool::ToolCallContext::new(
664+
&service,
665+
CallToolRequestParams {
666+
meta: None,
667+
name: Cow::Borrowed("requires_params"),
668+
arguments: Some(Default::default()),
669+
task: None,
670+
},
671+
RequestContext::new(NumberOrString::Number(1), peer),
672+
);
673+
674+
let result = router
675+
.call(ctx)
676+
.await
677+
.expect("argument validation should be a tool result");
678+
assert_eq!(result.is_error, Some(true));
679+
680+
let text = result
681+
.content
682+
.first()
683+
.and_then(|content| content.raw.as_text())
684+
.map(|text| text.text.as_str())
685+
.expect("tool error result should include text");
686+
assert!(text.contains("failed to deserialize parameters"));
687+
assert!(text.contains("missing field `project`"));
688+
}
689+
621690
#[tokio::test]
622691
async fn test_call_disabled_tool_returns_error() {
623692
let service = DummyService;

0 commit comments

Comments
 (0)