1use super::*;
2
3pub fn tool_definition(
17 name: impl Into<String>,
18 title: Option<String>,
19 description: Option<String>,
20 input_schema: McpSchema,
21 output_schema: Option<McpSchema>,
22) -> Result<ToolDefinition, McpToolError> {
23 let name = name.into();
24 validate_tool_name(&name)?;
25 if let Some(title) = &title {
26 validate_tool_metadata_text("title", title)?;
27 }
28 if let Some(description) = &description {
29 validate_tool_metadata_text("description", description)?;
30 }
31
32 let mut tool = ToolDefinition::default();
33 tool.name = Cow::Owned(name);
34 tool.title = title;
35 tool.description = description.map(Cow::Owned);
36 tool.input_schema = Arc::new(input_schema_object("input_schema", input_schema)?);
37 tool.output_schema = output_schema
38 .map(|schema| schema_object("output_schema", schema))
39 .transpose()?
40 .map(Arc::new);
41 if let Some(schema) = tool.output_schema.as_deref() {
42 output_schema::compile_output_schema(schema)?;
43 }
44 Ok(tool)
45}
46
47pub fn tool_definition_for_input<Input>(
54 name: impl Into<String>,
55 title: Option<String>,
56 description: Option<String>,
57 output_schema: Option<McpSchema>,
58) -> Result<McpTypedTool<Input>, McpToolError>
59where
60 Input: McpToolInput,
61{
62 tool_definition(
63 name,
64 title,
65 description,
66 Input::input_schema(),
67 output_schema,
68 )
69 .map(McpTypedTool::from_definition_unchecked)
70}
71
72pub fn tool_definition_with_annotations(
79 name: impl Into<String>,
80 title: Option<String>,
81 description: Option<String>,
82 input_schema: McpSchema,
83 output_schema: Option<McpSchema>,
84 annotations: Option<McpToolAnnotations>,
85) -> Result<ToolDefinition, McpToolError> {
86 if let Some(annotations) = annotations.as_ref() {
87 validate_tool_annotations(annotations)?;
88 }
89 let mut tool = tool_definition(name, title, description, input_schema, output_schema)?;
90 tool.annotations = annotations;
91 Ok(tool)
92}
93
94pub fn tool_definition_for_input_with_annotations<Input>(
101 name: impl Into<String>,
102 title: Option<String>,
103 description: Option<String>,
104 output_schema: Option<McpSchema>,
105 annotations: Option<McpToolAnnotations>,
106) -> Result<McpTypedTool<Input>, McpToolError>
107where
108 Input: McpToolInput,
109{
110 tool_definition_with_annotations(
111 name,
112 title,
113 description,
114 Input::input_schema(),
115 output_schema,
116 annotations,
117 )
118 .map(McpTypedTool::from_definition_unchecked)
119}
120
121pub fn tool_definition_with_metadata(
128 default_name: impl Into<String>,
129 metadata: McpToolMetadata,
130 input_schema: McpSchema,
131 output_schema: Option<McpSchema>,
132) -> Result<ToolDefinition, McpToolError> {
133 metadata.validate()?;
134 let default_name = default_name.into();
135 let name = metadata.name().map(str::to_string).unwrap_or(default_name);
136 let mut tool = tool_definition(
137 name,
138 metadata.title().map(str::to_string),
139 metadata.description().map(str::to_string),
140 input_schema,
141 output_schema,
142 )?;
143 tool.annotations = metadata.tool_annotations();
144 tool.icons = metadata.tool_icons();
145 Ok(tool)
146}
147
148pub fn tool_definition_for_input_with_metadata<Input>(
155 default_name: impl Into<String>,
156 metadata: McpToolMetadata,
157 output_schema: Option<McpSchema>,
158) -> Result<McpTypedTool<Input>, McpToolError>
159where
160 Input: McpToolInput,
161{
162 tool_definition_with_metadata(default_name, metadata, Input::input_schema(), output_schema)
163 .map(McpTypedTool::from_definition_unchecked)
164}
165
166pub fn resource_definition(
173 uri: impl Into<String>,
174 name: impl Into<String>,
175 title: Option<String>,
176 description: Option<String>,
177 mime_type: Option<String>,
178) -> Result<ResourceDefinition, McpToolError> {
179 let mut resource = Resource::new(uri, name);
180 resource.title = title;
181 resource.description = description;
182 resource.mime_type = mime_type;
183 validate_resource_definition(&resource)?;
184 Ok(resource)
185}
186
187pub fn resource_template_definition(
194 uri_template: impl Into<String>,
195 name: impl Into<String>,
196 title: Option<String>,
197 description: Option<String>,
198 mime_type: Option<String>,
199) -> Result<ResourceTemplateDefinition, McpToolError> {
200 let mut template = ResourceTemplate::new(uri_template, name);
201 template.title = title;
202 template.description = description;
203 template.mime_type = mime_type;
204 validate_resource_template(&template)?;
205 Ok(template)
206}
207
208pub fn text_resource_result(
210 uri: impl Into<String>,
211 text: impl Into<String>,
212 mime_type: impl Into<String>,
213) -> ReadResourceResult {
214 ReadResourceResult::new(vec![
215 ResourceContents::text(text, uri).with_mime_type(mime_type),
216 ])
217}
218
219pub fn json_resource_result(
225 uri: impl Into<String>,
226 value: &Value,
227) -> Result<ReadResourceResult, McpToolError> {
228 let text = serde_json::to_string_pretty(value).map_err(|error| {
229 McpToolError::conversion(format!("failed to encode JSON resource: {error}"))
230 })?;
231 Ok(text_resource_result(uri, text, "application/json"))
232}
233
234#[derive(Clone, Debug)]
236pub struct McpJsonResourceSpec {
237 uri: String,
238 definition: ResourceDefinition,
239 value: Arc<Value>,
240}
241
242impl McpJsonResourceSpec {
243 pub fn new(
250 uri: impl Into<String>,
251 name: impl Into<String>,
252 title: Option<String>,
253 description: Option<String>,
254 value: Value,
255 ) -> Result<Self, McpToolError> {
256 let uri = uri.into();
257 let definition = resource_definition(
258 uri.clone(),
259 name,
260 title,
261 description,
262 Some("application/json".to_string()),
263 )?;
264 Ok(Self {
265 uri,
266 definition,
267 value: Arc::new(value),
268 })
269 }
270
271 pub fn uri(&self) -> &str {
273 &self.uri
274 }
275
276 pub fn definition(&self) -> &ResourceDefinition {
278 &self.definition
279 }
280
281 pub fn into_definition(self) -> ResourceDefinition {
283 self.definition
284 }
285}
286
287pub fn json_resource_definitions(
294 specs: &[McpJsonResourceSpec],
295) -> Result<Vec<ResourceDefinition>, McpToolError> {
296 ensure_json_resource_specs_distinct(specs)?;
297 Ok(specs.iter().map(|spec| spec.definition.clone()).collect())
298}
299
300pub fn register_json_resource_specs(
307 server: &mut McpServer,
308 specs: Vec<McpJsonResourceSpec>,
309) -> Result<(), McpToolError> {
310 ensure_json_resource_specs_available(server, &specs)?;
311 for spec in specs {
312 let uri = spec.uri.clone();
313 let value = Arc::clone(&spec.value);
314 server.add_resource(spec.definition, move || {
315 json_resource_result(uri.clone(), value.as_ref())
316 .expect("generated JSON resource should encode")
317 })?;
318 }
319 Ok(())
320}
321
322pub fn register_json_resource_specs_if_missing(
332 server: &mut McpServer,
333 specs: Vec<McpJsonResourceSpec>,
334) -> Result<(), McpToolError> {
335 if specs
336 .iter()
337 .all(|spec| server.contains_resource(spec.uri()))
338 {
339 return Ok(());
340 }
341 register_json_resource_specs(server, specs)
342}
343
344pub fn ensure_json_resource_specs_available(
351 server: &McpServer,
352 specs: &[McpJsonResourceSpec],
353) -> Result<(), McpToolError> {
354 ensure_json_resource_specs_distinct(specs)?;
355 for spec in specs {
356 if server.contains_resource(spec.uri()) {
357 return Err(McpToolError::duplicate_resource(spec.uri().to_string()));
358 }
359 }
360 Ok(())
361}
362
363pub fn ensure_json_resource_specs_distinct(
370 specs: &[McpJsonResourceSpec],
371) -> Result<(), McpToolError> {
372 let mut seen = BTreeSet::new();
373 for spec in specs {
374 if !seen.insert(spec.uri()) {
375 return Err(McpToolError::duplicate_resource(spec.uri().to_string()));
376 }
377 }
378 Ok(())
379}
380
381pub fn prompt_definition(
388 name: impl Into<String>,
389 title: Option<String>,
390 description: Option<String>,
391 arguments: Option<Vec<McpPromptArgument>>,
392) -> Result<PromptDefinition, McpToolError> {
393 let mut prompt = Prompt::new(name, description, arguments);
394 prompt.title = title;
395 validate_prompt_definition(&prompt)?;
396 Ok(prompt)
397}
398
399pub fn text_prompt_result(description: Option<String>, text: impl Into<String>) -> GetPromptResult {
401 let mut result = GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]);
402 result.description = description;
403 result
404}
405
406pub fn validate_tool_name(name: &str) -> Result<(), McpToolError> {
413 component_shape::validate_mcp_tool_name(name)
414 .map_err(|error| McpToolError::validation(error.to_string()))
415}
416
417pub fn validate_tool_metadata_text(label: &str, value: &str) -> Result<(), McpToolError> {
423 component_shape::validate_mcp_tool_metadata_text(label, value)
424 .map_err(|error| McpToolError::validation(error.to_string()))
425}
426
427pub fn validate_tool_annotations(annotations: &McpToolAnnotations) -> Result<(), McpToolError> {
434 validate_tool_annotation_hints(annotations.read_only_hint, annotations.destructive_hint)?;
435 if let Some(title) = annotations.title.as_deref() {
436 validate_tool_metadata_text("annotation title", title)?;
437 }
438 Ok(())
439}
440
441pub fn validate_tool_definition(definition: &ToolDefinition) -> Result<(), McpToolError> {
448 validate_tool_definition_with_output(definition).map(|_| ())
449}
450
451pub(crate) fn validate_tool_definition_with_output(
452 definition: &ToolDefinition,
453) -> Result<Option<jsonschema::Validator>, McpToolError> {
454 validate_tool_name(definition.name.as_ref())?;
455 validate_tool_input_schema("input_schema", definition.input_schema.as_ref())?;
456 if let Some(title) = definition.title.as_deref() {
457 validate_tool_metadata_text("title", title)?;
458 }
459 if let Some(description) = definition.description.as_deref() {
460 validate_tool_metadata_text("description", description)?;
461 }
462 if let Some(annotations) = definition.annotations.as_ref() {
463 validate_tool_annotations(annotations)?;
464 }
465 if let Some(icons) = definition.icons.as_ref() {
466 for icon in icons {
467 validate_icon_definition(icon)?;
468 }
469 }
470 definition
471 .output_schema
472 .as_deref()
473 .map(output_schema::compile_output_schema)
474 .transpose()
475}
476
477pub fn validate_resource_definition(definition: &ResourceDefinition) -> Result<(), McpToolError> {
484 validate_required_metadata_text("resource uri", &definition.uri)?;
485 validate_required_metadata_text("resource name", &definition.name)?;
486 if let Some(title) = definition.title.as_deref() {
487 validate_tool_metadata_text("resource title", title)?;
488 }
489 if let Some(description) = definition.description.as_deref() {
490 validate_tool_metadata_text("resource description", description)?;
491 }
492 if let Some(mime_type) = definition.mime_type.as_deref() {
493 validate_required_metadata_text("resource mime type", mime_type)?;
494 }
495 Ok(())
496}
497
498pub fn validate_resource_template(
505 definition: &ResourceTemplateDefinition,
506) -> Result<(), McpToolError> {
507 validate_required_metadata_text("resource uri template", &definition.uri_template)?;
508 validate_required_metadata_text("resource template name", &definition.name)?;
509 if let Some(title) = definition.title.as_deref() {
510 validate_tool_metadata_text("resource template title", title)?;
511 }
512 if let Some(description) = definition.description.as_deref() {
513 validate_tool_metadata_text("resource template description", description)?;
514 }
515 if let Some(mime_type) = definition.mime_type.as_deref() {
516 validate_required_metadata_text("resource template mime type", mime_type)?;
517 }
518 Ok(())
519}
520
521pub fn validate_prompt_definition(definition: &PromptDefinition) -> Result<(), McpToolError> {
528 validate_tool_name(&definition.name)?;
529 if let Some(title) = definition.title.as_deref() {
530 validate_tool_metadata_text("prompt title", title)?;
531 }
532 if let Some(description) = definition.description.as_deref() {
533 validate_tool_metadata_text("prompt description", description)?;
534 }
535 if let Some(arguments) = definition.arguments.as_deref() {
536 for argument in arguments {
537 validate_tool_name(&argument.name)?;
538 if let Some(title) = argument.title.as_deref() {
539 validate_tool_metadata_text("prompt argument title", title)?;
540 }
541 if let Some(description) = argument.description.as_deref() {
542 validate_tool_metadata_text("prompt argument description", description)?;
543 }
544 }
545 }
546 Ok(())
547}
548
549pub(crate) fn validate_required_metadata_text(
550 label: &str,
551 value: &str,
552) -> Result<(), McpToolError> {
553 if value.trim().is_empty() {
554 return Err(McpToolError::invalid_schema(label, "must not be empty"));
555 }
556 validate_tool_metadata_text(label, value)
557}
558
559fn validate_icon_definition(icon: &McpIcon) -> Result<(), McpToolError> {
560 validate_required_metadata_text("icon src", &icon.src)?;
561 if let Some(mime_type) = icon.mime_type.as_deref() {
562 validate_required_metadata_text("icon mime_type", mime_type)?;
563 }
564 if let Some(sizes) = icon.sizes.as_ref() {
565 for size in sizes {
566 validate_required_metadata_text("icon size", size)?;
567 }
568 }
569 Ok(())
570}
571
572pub(crate) fn validate_tool_annotation_hints(
573 read_only: Option<bool>,
574 destructive: Option<bool>,
575) -> Result<(), McpToolError> {
576 if read_only == Some(true) && destructive == Some(true) {
577 return Err(McpToolError::validation(
578 "MCP tool annotation hints cannot be both read-only and destructive",
579 ));
580 }
581 Ok(())
582}
583
584pub fn schema_object(
590 label: impl Into<String>,
591 schema: McpSchema,
592) -> Result<JsonObject, McpToolError> {
593 match schema.into_value() {
594 Value::Object(object) => Ok(object),
595 _ => Err(McpToolError::invalid_schema(
596 label,
597 "MCP tool schemas must be JSON objects",
598 )),
599 }
600}
601
602fn input_schema_object(
603 label: impl Into<String>,
604 schema: McpSchema,
605) -> Result<JsonObject, McpToolError> {
606 let label = label.into();
607 let object = schema_object(label.clone(), schema)?;
608 validate_tool_input_schema(&label, &object)?;
609 Ok(object)
610}
611
612fn validate_tool_input_schema(
613 label: impl Into<String>,
614 schema: &JsonObject,
615) -> Result<(), McpToolError> {
616 let label = label.into();
617 let type_value = schema.get("type").ok_or_else(|| {
618 McpToolError::invalid_schema(
619 label.clone(),
620 "MCP tool input schemas must declare `type: \"object\"`",
621 )
622 })?;
623
624 let object_type = match type_value {
625 Value::String(value) => value == "object",
626 Value::Array(values) => values
627 .iter()
628 .any(|value| matches!(value, Value::String(value) if value == "object")),
629 _ => false,
630 };
631
632 if object_type {
633 Ok(())
634 } else {
635 Err(McpToolError::invalid_schema(
636 label,
637 "MCP tool input schemas must declare `type: \"object\"`",
638 ))
639 }
640}