1use super::*;
2
3pub fn tool_definition(
11 name: impl Into<String>,
12 title: Option<String>,
13 description: Option<String>,
14 input_schema: McpSchema,
15 output_schema: Option<McpSchema>,
16) -> Result<ToolDefinition, McpToolError> {
17 let name = name.into();
18 validate_tool_name(&name)?;
19 if let Some(title) = &title {
20 validate_tool_metadata_text("title", title)?;
21 }
22 if let Some(description) = &description {
23 validate_tool_metadata_text("description", description)?;
24 }
25
26 let mut tool = ToolDefinition::default();
27 tool.name = Cow::Owned(name);
28 tool.title = title;
29 tool.description = description.map(Cow::Owned);
30 tool.input_schema = Arc::new(input_schema_object("input_schema", input_schema)?);
31 tool.output_schema = output_schema
32 .map(|schema| schema_object("output_schema", schema))
33 .transpose()?
34 .map(Arc::new);
35 Ok(tool)
36}
37
38pub fn tool_definition_for_input<Input>(
45 name: impl Into<String>,
46 title: Option<String>,
47 description: Option<String>,
48 output_schema: Option<McpSchema>,
49) -> Result<McpTypedTool<Input>, McpToolError>
50where
51 Input: McpToolInput,
52{
53 tool_definition(
54 name,
55 title,
56 description,
57 Input::input_schema(),
58 output_schema,
59 )
60 .map(McpTypedTool::from_definition_unchecked)
61}
62
63pub fn tool_definition_with_annotations(
70 name: impl Into<String>,
71 title: Option<String>,
72 description: Option<String>,
73 input_schema: McpSchema,
74 output_schema: Option<McpSchema>,
75 annotations: Option<McpToolAnnotations>,
76) -> Result<ToolDefinition, McpToolError> {
77 if let Some(annotations) = annotations.as_ref() {
78 validate_tool_annotations(annotations)?;
79 }
80 let mut tool = tool_definition(name, title, description, input_schema, output_schema)?;
81 tool.annotations = annotations;
82 Ok(tool)
83}
84
85pub fn tool_definition_for_input_with_annotations<Input>(
92 name: impl Into<String>,
93 title: Option<String>,
94 description: Option<String>,
95 output_schema: Option<McpSchema>,
96 annotations: Option<McpToolAnnotations>,
97) -> Result<McpTypedTool<Input>, McpToolError>
98where
99 Input: McpToolInput,
100{
101 tool_definition_with_annotations(
102 name,
103 title,
104 description,
105 Input::input_schema(),
106 output_schema,
107 annotations,
108 )
109 .map(McpTypedTool::from_definition_unchecked)
110}
111
112pub fn tool_definition_with_metadata(
119 default_name: impl Into<String>,
120 metadata: McpToolMetadata,
121 input_schema: McpSchema,
122 output_schema: Option<McpSchema>,
123) -> Result<ToolDefinition, McpToolError> {
124 metadata.validate()?;
125 let default_name = default_name.into();
126 let name = metadata.name().map(str::to_string).unwrap_or(default_name);
127 let mut tool = tool_definition(
128 name,
129 metadata.title().map(str::to_string),
130 metadata.description().map(str::to_string),
131 input_schema,
132 output_schema,
133 )?;
134 tool.annotations = metadata.tool_annotations();
135 tool.icons = metadata.tool_icons();
136 Ok(tool)
137}
138
139pub fn tool_definition_for_input_with_metadata<Input>(
146 default_name: impl Into<String>,
147 metadata: McpToolMetadata,
148 output_schema: Option<McpSchema>,
149) -> Result<McpTypedTool<Input>, McpToolError>
150where
151 Input: McpToolInput,
152{
153 tool_definition_with_metadata(default_name, metadata, Input::input_schema(), output_schema)
154 .map(McpTypedTool::from_definition_unchecked)
155}
156
157pub fn resource_definition(
164 uri: impl Into<String>,
165 name: impl Into<String>,
166 title: Option<String>,
167 description: Option<String>,
168 mime_type: Option<String>,
169) -> Result<ResourceDefinition, McpToolError> {
170 let mut resource = Resource::new(uri, name);
171 resource.title = title;
172 resource.description = description;
173 resource.mime_type = mime_type;
174 validate_resource_definition(&resource)?;
175 Ok(resource)
176}
177
178pub fn resource_template_definition(
185 uri_template: impl Into<String>,
186 name: impl Into<String>,
187 title: Option<String>,
188 description: Option<String>,
189 mime_type: Option<String>,
190) -> Result<ResourceTemplateDefinition, McpToolError> {
191 let mut template = ResourceTemplate::new(uri_template, name);
192 template.title = title;
193 template.description = description;
194 template.mime_type = mime_type;
195 validate_resource_template(&template)?;
196 Ok(template)
197}
198
199pub fn text_resource_result(
201 uri: impl Into<String>,
202 text: impl Into<String>,
203 mime_type: impl Into<String>,
204) -> ReadResourceResult {
205 ReadResourceResult::new(vec![
206 ResourceContents::text(text, uri).with_mime_type(mime_type),
207 ])
208}
209
210pub fn json_resource_result(
216 uri: impl Into<String>,
217 value: &Value,
218) -> Result<ReadResourceResult, McpToolError> {
219 let text = serde_json::to_string_pretty(value).map_err(|error| {
220 McpToolError::conversion(format!("failed to encode JSON resource: {error}"))
221 })?;
222 Ok(text_resource_result(uri, text, "application/json"))
223}
224
225#[derive(Clone, Debug)]
227pub struct McpJsonResourceSpec {
228 uri: String,
229 definition: ResourceDefinition,
230 value: Arc<Value>,
231}
232
233impl McpJsonResourceSpec {
234 pub fn new(
241 uri: impl Into<String>,
242 name: impl Into<String>,
243 title: Option<String>,
244 description: Option<String>,
245 value: Value,
246 ) -> Result<Self, McpToolError> {
247 let uri = uri.into();
248 let definition = resource_definition(
249 uri.clone(),
250 name,
251 title,
252 description,
253 Some("application/json".to_string()),
254 )?;
255 Ok(Self {
256 uri,
257 definition,
258 value: Arc::new(value),
259 })
260 }
261
262 pub fn uri(&self) -> &str {
264 &self.uri
265 }
266
267 pub fn definition(&self) -> &ResourceDefinition {
269 &self.definition
270 }
271
272 pub fn into_definition(self) -> ResourceDefinition {
274 self.definition
275 }
276}
277
278pub fn json_resource_definitions(
285 specs: &[McpJsonResourceSpec],
286) -> Result<Vec<ResourceDefinition>, McpToolError> {
287 ensure_json_resource_specs_distinct(specs)?;
288 Ok(specs.iter().map(|spec| spec.definition.clone()).collect())
289}
290
291pub fn register_json_resource_specs(
298 server: &mut McpServer,
299 specs: Vec<McpJsonResourceSpec>,
300) -> Result<(), McpToolError> {
301 ensure_json_resource_specs_available(server, &specs)?;
302 for spec in specs {
303 let uri = spec.uri.clone();
304 let value = Arc::clone(&spec.value);
305 server.add_resource(spec.definition, move || {
306 json_resource_result(uri.clone(), value.as_ref())
307 .expect("generated JSON resource should encode")
308 })?;
309 }
310 Ok(())
311}
312
313pub fn register_json_resource_specs_if_missing(
323 server: &mut McpServer,
324 specs: Vec<McpJsonResourceSpec>,
325) -> Result<(), McpToolError> {
326 if specs
327 .iter()
328 .all(|spec| server.contains_resource(spec.uri()))
329 {
330 return Ok(());
331 }
332 register_json_resource_specs(server, specs)
333}
334
335pub fn ensure_json_resource_specs_available(
342 server: &McpServer,
343 specs: &[McpJsonResourceSpec],
344) -> Result<(), McpToolError> {
345 ensure_json_resource_specs_distinct(specs)?;
346 for spec in specs {
347 if server.contains_resource(spec.uri()) {
348 return Err(McpToolError::duplicate_resource(spec.uri().to_string()));
349 }
350 }
351 Ok(())
352}
353
354pub fn ensure_json_resource_specs_distinct(
361 specs: &[McpJsonResourceSpec],
362) -> Result<(), McpToolError> {
363 let mut seen = BTreeSet::new();
364 for spec in specs {
365 if !seen.insert(spec.uri()) {
366 return Err(McpToolError::duplicate_resource(spec.uri().to_string()));
367 }
368 }
369 Ok(())
370}
371
372pub fn prompt_definition(
379 name: impl Into<String>,
380 title: Option<String>,
381 description: Option<String>,
382 arguments: Option<Vec<McpPromptArgument>>,
383) -> Result<PromptDefinition, McpToolError> {
384 let mut prompt = Prompt::new(name, description, arguments);
385 prompt.title = title;
386 validate_prompt_definition(&prompt)?;
387 Ok(prompt)
388}
389
390pub fn text_prompt_result(description: Option<String>, text: impl Into<String>) -> GetPromptResult {
392 let mut result = GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]);
393 result.description = description;
394 result
395}
396
397pub fn validate_tool_name(name: &str) -> Result<(), McpToolError> {
404 component_shape::validate_mcp_tool_name(name)
405 .map_err(|error| McpToolError::validation(error.to_string()))
406}
407
408pub fn validate_tool_metadata_text(label: &str, value: &str) -> Result<(), McpToolError> {
414 component_shape::validate_mcp_tool_metadata_text(label, value)
415 .map_err(|error| McpToolError::validation(error.to_string()))
416}
417
418pub fn validate_tool_annotations(annotations: &McpToolAnnotations) -> Result<(), McpToolError> {
425 validate_tool_annotation_hints(annotations.read_only_hint, annotations.destructive_hint)?;
426 if let Some(title) = annotations.title.as_deref() {
427 validate_tool_metadata_text("annotation title", title)?;
428 }
429 Ok(())
430}
431
432pub fn validate_tool_definition(definition: &ToolDefinition) -> Result<(), McpToolError> {
439 validate_tool_name(definition.name.as_ref())?;
440 validate_tool_input_schema("input_schema", definition.input_schema.as_ref())?;
441 if let Some(title) = definition.title.as_deref() {
442 validate_tool_metadata_text("title", title)?;
443 }
444 if let Some(description) = definition.description.as_deref() {
445 validate_tool_metadata_text("description", description)?;
446 }
447 if let Some(annotations) = definition.annotations.as_ref() {
448 validate_tool_annotations(annotations)?;
449 }
450 if let Some(icons) = definition.icons.as_ref() {
451 for icon in icons {
452 validate_icon_definition(icon)?;
453 }
454 }
455 Ok(())
456}
457
458pub fn validate_resource_definition(definition: &ResourceDefinition) -> Result<(), McpToolError> {
465 validate_required_metadata_text("resource uri", &definition.uri)?;
466 validate_required_metadata_text("resource name", &definition.name)?;
467 if let Some(title) = definition.title.as_deref() {
468 validate_tool_metadata_text("resource title", title)?;
469 }
470 if let Some(description) = definition.description.as_deref() {
471 validate_tool_metadata_text("resource description", description)?;
472 }
473 if let Some(mime_type) = definition.mime_type.as_deref() {
474 validate_required_metadata_text("resource mime type", mime_type)?;
475 }
476 Ok(())
477}
478
479pub fn validate_resource_template(
486 definition: &ResourceTemplateDefinition,
487) -> Result<(), McpToolError> {
488 validate_required_metadata_text("resource uri template", &definition.uri_template)?;
489 validate_required_metadata_text("resource template name", &definition.name)?;
490 if let Some(title) = definition.title.as_deref() {
491 validate_tool_metadata_text("resource template title", title)?;
492 }
493 if let Some(description) = definition.description.as_deref() {
494 validate_tool_metadata_text("resource template description", description)?;
495 }
496 if let Some(mime_type) = definition.mime_type.as_deref() {
497 validate_required_metadata_text("resource template mime type", mime_type)?;
498 }
499 Ok(())
500}
501
502pub fn validate_prompt_definition(definition: &PromptDefinition) -> Result<(), McpToolError> {
509 validate_tool_name(&definition.name)?;
510 if let Some(title) = definition.title.as_deref() {
511 validate_tool_metadata_text("prompt title", title)?;
512 }
513 if let Some(description) = definition.description.as_deref() {
514 validate_tool_metadata_text("prompt description", description)?;
515 }
516 if let Some(arguments) = definition.arguments.as_deref() {
517 for argument in arguments {
518 validate_tool_name(&argument.name)?;
519 if let Some(title) = argument.title.as_deref() {
520 validate_tool_metadata_text("prompt argument title", title)?;
521 }
522 if let Some(description) = argument.description.as_deref() {
523 validate_tool_metadata_text("prompt argument description", description)?;
524 }
525 }
526 }
527 Ok(())
528}
529
530pub(crate) fn validate_required_metadata_text(
531 label: &str,
532 value: &str,
533) -> Result<(), McpToolError> {
534 if value.trim().is_empty() {
535 return Err(McpToolError::invalid_schema(label, "must not be empty"));
536 }
537 validate_tool_metadata_text(label, value)
538}
539
540fn validate_icon_definition(icon: &McpIcon) -> Result<(), McpToolError> {
541 validate_required_metadata_text("icon src", &icon.src)?;
542 if let Some(mime_type) = icon.mime_type.as_deref() {
543 validate_required_metadata_text("icon mime_type", mime_type)?;
544 }
545 if let Some(sizes) = icon.sizes.as_ref() {
546 for size in sizes {
547 validate_required_metadata_text("icon size", size)?;
548 }
549 }
550 Ok(())
551}
552
553pub(crate) fn validate_tool_annotation_hints(
554 read_only: Option<bool>,
555 destructive: Option<bool>,
556) -> Result<(), McpToolError> {
557 if read_only == Some(true) && destructive == Some(true) {
558 return Err(McpToolError::validation(
559 "MCP tool annotation hints cannot be both read-only and destructive",
560 ));
561 }
562 Ok(())
563}
564
565pub fn schema_object(
571 label: impl Into<String>,
572 schema: McpSchema,
573) -> Result<JsonObject, McpToolError> {
574 match schema.into_value() {
575 Value::Object(object) => Ok(object),
576 _ => Err(McpToolError::invalid_schema(
577 label,
578 "MCP tool schemas must be JSON objects",
579 )),
580 }
581}
582
583fn input_schema_object(
584 label: impl Into<String>,
585 schema: McpSchema,
586) -> Result<JsonObject, McpToolError> {
587 let label = label.into();
588 let object = schema_object(label.clone(), schema)?;
589 validate_tool_input_schema(&label, &object)?;
590 Ok(object)
591}
592
593fn validate_tool_input_schema(
594 label: impl Into<String>,
595 schema: &JsonObject,
596) -> Result<(), McpToolError> {
597 let label = label.into();
598 let type_value = schema.get("type").ok_or_else(|| {
599 McpToolError::invalid_schema(
600 label.clone(),
601 "MCP tool input schemas must declare `type: \"object\"`",
602 )
603 })?;
604
605 let object_type = match type_value {
606 Value::String(value) => value == "object",
607 Value::Array(values) => values
608 .iter()
609 .any(|value| matches!(value, Value::String(value) if value == "object")),
610 _ => false,
611 };
612
613 if object_type {
614 Ok(())
615 } else {
616 Err(McpToolError::invalid_schema(
617 label,
618 "MCP tool input schemas must declare `type: \"object\"`",
619 ))
620 }
621}