1use std::any::{TypeId, type_name};
7use std::borrow::Cow;
8use std::fmt;
9use std::future::Future;
10use std::pin::Pin;
11
12use schemars::JsonSchema;
13use serde::Serialize;
14use serde::de::DeserializeOwned;
15use serde_json::value::RawValue;
16use serde_json::{Map, Value};
17
18use crate::context::Ctx;
19use crate::error::McpError;
20use crate::spec::{self, CallToolResult, ContentBlock, TextContent};
21
22pub type ToolFuture = Pin<Box<dyn Future<Output = ToolResult> + Send + 'static>>;
24
25pub type ToolResult = Result<ToolOutput, ToolError>;
27
28pub type CallFn = fn(arguments: Option<&RawValue>, ctx: Ctx) -> Result<ToolFuture, ToolError>;
31
32pub trait Tool: 'static {
35 fn definition() -> ToolDef;
37}
38
39#[derive(Debug, Clone, Copy)]
41pub struct StateRequirement {
42 pub(crate) type_id: TypeId,
43 pub(crate) type_name: &'static str,
44}
45
46impl StateRequirement {
47 pub(crate) fn of<T: Send + Sync + 'static>() -> Self {
48 Self {
49 type_id: TypeId::of::<T>(),
50 type_name: type_name::<T>(),
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct ToolDef {
58 pub(crate) tool: spec::Tool,
59 pub(crate) call: CallFn,
60 pub(crate) required_state: Vec<StateRequirement>,
61 pub(crate) scopes: Vec<String>,
62 pub(crate) ui: Option<String>,
63 pub(crate) visibility: Option<Vec<crate::apps::Visibility>>,
64}
65
66impl ToolDef {
67 #[must_use]
69 pub fn new(tool: spec::Tool, call: CallFn) -> Self {
70 Self {
71 tool,
72 call,
73 required_state: Vec::new(),
74 scopes: Vec::new(),
75 ui: None,
76 visibility: None,
77 }
78 }
79
80 #[must_use]
85 pub fn ui(mut self, resource_uri: impl Into<String>) -> Self {
86 self.ui = Some(resource_uri.into());
87 self
88 }
89
90 #[must_use]
97 pub fn visibility(
98 mut self,
99 visibility: impl IntoIterator<Item = crate::apps::Visibility>,
100 ) -> Self {
101 self.visibility = Some(visibility.into_iter().collect());
102 self
103 }
104
105 #[must_use]
107 pub fn ui_resource(&self) -> Option<&str> {
108 self.ui.as_deref()
109 }
110
111 #[must_use]
115 pub fn scopes<I, S>(mut self, scopes: I) -> Self
116 where
117 I: IntoIterator<Item = S>,
118 S: Into<String>,
119 {
120 self.scopes = scopes.into_iter().map(Into::into).collect();
121 self
122 }
123
124 #[must_use]
126 pub fn required_scopes(&self) -> &[String] {
127 &self.scopes
128 }
129
130 #[must_use]
132 pub fn require_state<T: Send + Sync + 'static>(mut self) -> Self {
133 self.required_state.push(StateRequirement::of::<T>());
134 self
135 }
136
137 #[must_use]
139 pub fn tool(&self) -> &spec::Tool {
140 &self.tool
141 }
142}
143
144const TIMEOUT_HELP: &str = "raise ServerBuilder::tool_timeout(..) (default 30 s) or make the \
145 handler faster; long work should stop when ctx.is_cancelled() becomes true";
146
147const PANIC_HELP: &str = "the panic message is logged just above; return an error instead of \
148 panicking (the client got a generic error)";
149
150#[derive(Debug, Clone, PartialEq)]
152#[non_exhaustive]
153pub enum ToolOutput {
154 Complete(CallToolResult),
156}
157
158pub struct ToolError {
167 kind: ToolErrorKind,
168}
169
170enum ToolErrorKind {
171 User(Cow<'static, str>),
172 NotFound,
173 InputRequired,
175 Protocol(McpError),
176 InvalidArguments(String),
177 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
178 Timeout,
179 Panicked,
180}
181
182impl ToolError {
183 pub fn user(message: impl Into<Cow<'static, str>>) -> Self {
185 Self {
186 kind: ToolErrorKind::User(message.into()),
187 }
188 }
189
190 #[must_use]
194 pub fn not_found() -> Self {
195 Self {
196 kind: ToolErrorKind::NotFound,
197 }
198 }
199
200 pub fn invalid_argument(message: impl Into<String>) -> Self {
202 Self {
203 kind: ToolErrorKind::InvalidArguments(crate::error::sanitize(&message.into())),
204 }
205 }
206
207 #[must_use]
211 pub fn protocol(error: McpError) -> Self {
212 Self {
213 kind: ToolErrorKind::Protocol(error),
214 }
215 }
216
217 #[must_use]
223 pub fn missing_client_capability(required: &spec::ClientCapabilities) -> Self {
224 Self::protocol(McpError::missing_client_capability(required))
225 }
226
227 pub fn internal(error: impl Into<Box<dyn std::error::Error + Send + Sync + 'static>>) -> Self {
229 Self {
230 kind: ToolErrorKind::Internal(error.into()),
231 }
232 }
233
234 #[must_use]
237 pub fn invalid_arguments(error: &serde_json::Error) -> Self {
238 Self {
239 kind: ToolErrorKind::InvalidArguments(crate::error::sanitize(&error.to_string())),
240 }
241 }
242
243 pub(crate) fn input_required() -> Self {
244 Self {
245 kind: ToolErrorKind::InputRequired,
246 }
247 }
248
249 pub(crate) fn is_input_required(&self) -> bool {
250 matches!(self.kind, ToolErrorKind::InputRequired)
251 }
252
253 pub(crate) fn timeout() -> Self {
254 Self {
255 kind: ToolErrorKind::Timeout,
256 }
257 }
258
259 pub(crate) fn panicked() -> Self {
260 Self {
261 kind: ToolErrorKind::Panicked,
262 }
263 }
264
265 pub(crate) fn into_protocol(self) -> Result<McpError, Self> {
268 match self.kind {
269 ToolErrorKind::Protocol(error) => Ok(error),
270 kind => Err(Self { kind }),
271 }
272 }
273
274 pub(crate) fn into_call_result(self, tool: &str) -> CallToolResult {
276 let message: Cow<'static, str> = match self.kind {
277 ToolErrorKind::User(message) => message,
278 ToolErrorKind::NotFound => Cow::Borrowed("Not found."),
279 ToolErrorKind::InputRequired => {
280 tracing::error!(tool, "input required outside a multi round-trip request");
281 Cow::Borrowed("The tool failed because of an internal error.")
282 }
283 ToolErrorKind::Protocol(error) => Cow::Owned(error.message().to_owned()),
284 ToolErrorKind::InvalidArguments(details) => {
285 Cow::Owned(format!("Invalid arguments: {details}"))
286 }
287 ToolErrorKind::Internal(error) => {
288 tracing::error!(
289 "tool `{tool}` failed: {error}\n = help: the client saw only a generic \
290 message; return ToolError::user(\"...\") for errors the model should see"
291 );
292 Cow::Borrowed("The tool failed because of an internal error.")
293 }
294 ToolErrorKind::Timeout => {
295 tracing::warn!("tool `{tool}` timed out\n = help: {TIMEOUT_HELP}");
296 Cow::Borrowed("The tool did not finish within the time limit.")
297 }
298 ToolErrorKind::Panicked => {
299 tracing::error!("tool `{tool}` panicked\n = help: {PANIC_HELP}");
300 Cow::Borrowed("The tool failed because of an internal error.")
301 }
302 };
303 let mut result = CallToolResult::new(
304 vec![ContentBlock::from(TextContent::new(message.into_owned()))],
305 "complete".to_owned(),
306 );
307 result.is_error = Some(true);
308 result
309 }
310
311 pub(crate) fn into_rpc_error(self, handler: &str, uri: Option<&str>) -> McpError {
314 match self.kind {
315 ToolErrorKind::User(message) => McpError::request_failed(message),
316 ToolErrorKind::NotFound => match uri {
317 Some(uri) => McpError::resource_not_found(uri),
318 None => McpError::request_failed("Not found"),
319 },
320 ToolErrorKind::Protocol(error) => error,
321 ToolErrorKind::InputRequired => {
322 tracing::error!(handler, "input required outside a multi round-trip request");
323 McpError::internal()
324 }
325 ToolErrorKind::InvalidArguments(details) => {
326 McpError::request_failed(format!("Invalid arguments: {details}"))
327 }
328 ToolErrorKind::Internal(error) => {
329 tracing::error!(
330 "`{handler}` failed: {error}\n = help: the client saw only a generic \
331 message; return ToolError::user(\"...\") for errors it should see"
332 );
333 McpError::internal()
334 }
335 ToolErrorKind::Timeout => {
336 tracing::warn!("`{handler}` timed out\n = help: {TIMEOUT_HELP}");
337 McpError::internal_with_message("The request did not finish within the time limit.")
338 }
339 ToolErrorKind::Panicked => {
340 tracing::error!("`{handler}` panicked\n = help: {PANIC_HELP}");
341 McpError::internal()
342 }
343 }
344 }
345}
346
347impl fmt::Debug for ToolError {
348 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 match &self.kind {
350 ToolErrorKind::User(m) => f.debug_tuple("User").field(m).finish(),
351 ToolErrorKind::NotFound => f.write_str("NotFound"),
352 ToolErrorKind::InputRequired => f.write_str("InputRequired"),
353 ToolErrorKind::Protocol(e) => f.debug_tuple("Protocol").field(e).finish(),
354 ToolErrorKind::InvalidArguments(m) => {
355 f.debug_tuple("InvalidArguments").field(m).finish()
356 }
357 ToolErrorKind::Internal(e) => f.debug_tuple("Internal").field(e).finish(),
358 ToolErrorKind::Timeout => f.write_str("Timeout"),
359 ToolErrorKind::Panicked => f.write_str("Panicked"),
360 }
361 }
362}
363
364impl fmt::Display for ToolError {
365 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366 match &self.kind {
367 ToolErrorKind::User(m) => f.write_str(m),
368 ToolErrorKind::NotFound => f.write_str("not found"),
369 ToolErrorKind::InputRequired => f.write_str("input required"),
370 ToolErrorKind::Protocol(e) => write!(f, "protocol error: {e}"),
371 ToolErrorKind::InvalidArguments(m) => write!(f, "invalid arguments: {m}"),
372 ToolErrorKind::Internal(e) => write!(f, "internal error: {e}"),
373 ToolErrorKind::Timeout => f.write_str("timed out"),
374 ToolErrorKind::Panicked => f.write_str("panicked"),
375 }
376 }
377}
378
379pub trait IntoToolError {
382 fn into_tool_error(self) -> ToolError;
384}
385
386impl IntoToolError for ToolError {
387 fn into_tool_error(self) -> ToolError {
388 self
389 }
390}
391
392impl<E: std::error::Error + Send + Sync + 'static> IntoToolError for E {
393 fn into_tool_error(self) -> ToolError {
394 ToolError::internal(self)
395 }
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
402pub struct Json<T>(pub T);
403
404pub trait IntoToolResult {
406 fn into_tool_result(self) -> ToolResult;
408
409 fn output_schema() -> Option<Map<String, Value>> {
411 None
412 }
413}
414
415fn text_result(text: String) -> ToolResult {
416 Ok(ToolOutput::Complete(CallToolResult::new(
417 vec![ContentBlock::from(TextContent::new(text))],
418 "complete".to_owned(),
419 )))
420}
421
422impl IntoToolResult for String {
423 fn into_tool_result(self) -> ToolResult {
424 text_result(self)
425 }
426}
427
428impl IntoToolResult for &'static str {
429 fn into_tool_result(self) -> ToolResult {
430 text_result(self.to_owned())
431 }
432}
433
434impl IntoToolResult for Cow<'static, str> {
435 fn into_tool_result(self) -> ToolResult {
436 text_result(self.into_owned())
437 }
438}
439
440macro_rules! display_results {
441 ($($t:ty),* $(,)?) => {
442 $(impl IntoToolResult for $t {
443 fn into_tool_result(self) -> ToolResult {
444 text_result(self.to_string())
445 }
446 })*
447 };
448}
449
450display_results!(
451 bool, char, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64
452);
453
454impl IntoToolResult for () {
455 fn into_tool_result(self) -> ToolResult {
456 Ok(ToolOutput::Complete(CallToolResult::new(
457 Vec::new(),
458 "complete".to_owned(),
459 )))
460 }
461}
462
463impl IntoToolResult for CallToolResult {
464 fn into_tool_result(self) -> ToolResult {
465 Ok(ToolOutput::Complete(self))
466 }
467}
468
469impl IntoToolResult for ContentBlock {
470 fn into_tool_result(self) -> ToolResult {
471 Ok(ToolOutput::Complete(CallToolResult::new(
472 vec![self],
473 "complete".to_owned(),
474 )))
475 }
476}
477
478impl IntoToolResult for Vec<ContentBlock> {
479 fn into_tool_result(self) -> ToolResult {
480 Ok(ToolOutput::Complete(CallToolResult::new(
481 self,
482 "complete".to_owned(),
483 )))
484 }
485}
486
487impl<T: Serialize + JsonSchema> IntoToolResult for Json<T> {
488 fn into_tool_result(self) -> ToolResult {
489 let value = serde_json::to_value(&self.0).map_err(ToolError::internal)?;
490 let text = serde_json::to_string(&value).map_err(ToolError::internal)?;
491 let mut result = CallToolResult::new(
492 vec![ContentBlock::from(TextContent::new(text))],
493 "complete".to_owned(),
494 );
495 result.structured_content = Some(value);
496 Ok(ToolOutput::Complete(result))
497 }
498
499 fn output_schema() -> Option<Map<String, Value>> {
500 Some(schema_for::<T>())
501 }
502}
503
504impl<T: IntoToolResult, E: IntoToolError> IntoToolResult for Result<T, E> {
505 fn into_tool_result(self) -> ToolResult {
506 match self {
507 Ok(value) => value.into_tool_result(),
508 Err(error) => Err(error.into_tool_error()),
509 }
510 }
511
512 fn output_schema() -> Option<Map<String, Value>> {
513 T::output_schema()
514 }
515}
516
517#[must_use]
521pub fn schema_for<T: JsonSchema>() -> Map<String, Value> {
522 let generator = schemars::generate::SchemaSettings::draft2020_12().into_generator();
523 let schema = generator.into_root_schema_for::<T>();
524 let mut map = match schema.to_value() {
525 Value::Object(map) => map,
526 Value::Bool(true) => Map::new(),
528 other => {
529 let mut map = Map::new();
530 map.insert("not".to_owned(), Value::Object(Map::new()));
531 tracing::warn!(schema = %other, "unexpected schema shape");
532 map
533 }
534 };
535 map.remove("$schema");
536 map.remove("title");
537 for value in map.values_mut() {
538 strip_nonstandard_formats(value);
539 }
540 map
541}
542
543const NONSTANDARD_FORMATS: &[&str] = &[
547 "int", "int8", "int16", "int32", "int64", "int128", "uint", "uint8", "uint16", "uint32",
548 "uint64", "uint128", "float", "double",
549];
550
551fn strip_nonstandard_formats(value: &mut Value) {
552 match value {
553 Value::Object(map) => {
554 if map
555 .get("format")
556 .and_then(Value::as_str)
557 .is_some_and(|f| NONSTANDARD_FORMATS.contains(&f))
558 {
559 map.remove("format");
560 }
561 map.values_mut().for_each(strip_nonstandard_formats);
562 }
563 Value::Array(items) => items.iter_mut().for_each(strip_nonstandard_formats),
564 _ => {}
565 }
566}
567
568#[must_use]
571pub fn input_schema_for<T: JsonSchema>() -> spec::ToolInputSchema {
572 let mut map = schema_for::<T>();
573 map.remove("type");
575 spec::ToolInputSchema {
576 schema: None,
577 r#type: spec::ObjectType::Object,
578 extra: map,
579 }
580}
581
582#[must_use]
584pub fn output_schema_from(schema: Map<String, Value>) -> spec::ToolOutputSchema {
585 spec::ToolOutputSchema {
586 schema: None,
587 extra: schema,
588 }
589}
590
591pub fn parse_arguments<T: DeserializeOwned>(arguments: Option<&RawValue>) -> Result<T, ToolError> {
598 let raw = arguments.map_or("{}", RawValue::get);
599 serde_json::from_str(raw).map_err(|e| ToolError::invalid_arguments(&e))
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[derive(Serialize, JsonSchema)]
607 struct Point {
608 x: i32,
609 y: i32,
610 }
611
612 fn complete(r: ToolResult) -> CallToolResult {
613 match r {
614 Ok(ToolOutput::Complete(c)) => c,
615 Err(e) => panic!("unexpected error {e:?}"),
616 }
617 }
618
619 #[test]
620 fn json_output_has_structured_content_and_schema() {
621 let result = complete(Json(Point { x: 1, y: 2 }).into_tool_result());
622 assert_eq!(
623 result.structured_content,
624 Some(serde_json::json!({"x": 1, "y": 2}))
625 );
626 let schema = <Json<Point> as IntoToolResult>::output_schema().unwrap();
627 assert_eq!(schema.get("type"), Some(&Value::from("object")));
628 assert!(!schema.contains_key("title"));
629 assert!(!schema.contains_key("$schema"));
630 assert_eq!(
632 schema["properties"]["x"],
633 serde_json::json!({ "type": "integer" })
634 );
635 }
636
637 #[test]
638 fn errors_hide_internal_details() {
639 let io = std::io::Error::other("secret path /etc/shadow");
640 let result = Err::<String, _>(io)
641 .into_tool_result()
642 .unwrap_err()
643 .into_call_result("t");
644 assert_eq!(result.is_error, Some(true));
645 let text = serde_json::to_string(&result).unwrap();
646 assert!(!text.contains("shadow"));
647
648 let user = ToolError::user("b must not be zero").into_call_result("t");
649 assert!(
650 serde_json::to_string(&user)
651 .unwrap()
652 .contains("b must not be zero")
653 );
654 }
655
656 #[test]
657 fn missing_arguments_are_an_empty_object() {
658 #[derive(serde::Deserialize)]
659 struct NoArgs {}
660 assert!(parse_arguments::<NoArgs>(None).is_ok());
661 #[derive(Debug, serde::Deserialize)]
662 #[allow(dead_code)]
663 struct NeedsA {
664 a: i32,
665 }
666 let err = parse_arguments::<NeedsA>(None).unwrap_err();
667 assert!(err.to_string().contains("missing field `a`"));
668 }
669}