Skip to main content

component_shape_mcp/
definitions.rs

1use super::*;
2
3/// Build and validate an MCP tool definition.
4///
5/// Output schemas use JSON Schema Draft 2020-12 unless `$schema` selects a
6/// supported draft (4, 6, 7, 2019-09, or 2020-12). Formats are annotations.
7/// References must resolve within the supplied schema; external retrieval is
8/// disabled. Successful output must use the advertised names, without input
9/// alias or `x-mcp*` normalization.
10///
11/// # Errors
12///
13/// Returns [`McpToolError`] when the tool metadata is invalid or when the input
14/// schema is not an object-shaped MCP tool schema, or the output schema is not
15/// a valid, self-contained JSON Schema object.
16pub 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
47/// Build and validate a typed MCP tool definition from its input type.
48///
49/// # Errors
50///
51/// Returns [`McpToolError`] when the tool metadata is invalid or when the input
52/// type's generated schema or output schema is not accepted.
53pub 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
72/// Build and validate an MCP tool definition with tool annotations.
73///
74/// # Errors
75///
76/// Returns [`McpToolError`] when the annotations, metadata, input schema, or
77/// output schema are invalid.
78pub 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
94/// Build and validate a typed MCP tool definition with tool annotations.
95///
96/// # Errors
97///
98/// Returns [`McpToolError`] when the annotations, metadata, input schema, or
99/// output schema are invalid.
100pub 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
121/// Build and validate an MCP tool definition from generated metadata.
122///
123/// # Errors
124///
125/// Returns [`McpToolError`] when the generated metadata, input schema, or output
126/// schema is invalid.
127pub 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
148/// Build and validate a typed MCP tool definition from generated metadata.
149///
150/// # Errors
151///
152/// Returns [`McpToolError`] when the generated metadata, typed input schema, or
153/// output schema is invalid.
154pub 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
166/// Build and validate a concrete MCP resource definition.
167///
168/// # Errors
169///
170/// Returns [`McpToolError`] when the resource URI, name, title, description, or
171/// MIME type is invalid.
172pub 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
187/// Build and validate an MCP resource template definition.
188///
189/// # Errors
190///
191/// Returns [`McpToolError`] when the template URI, name, title, description, or
192/// MIME type is invalid.
193pub 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
208/// Build a text resource result with an explicit MIME type.
209pub 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
219/// Encode a JSON value as a pretty-printed `application/json` resource result.
220///
221/// # Errors
222///
223/// Returns [`McpToolError`] when `value` cannot be serialized as JSON.
224pub 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/// Validated static JSON resource ready to register on an [`McpServer`].
235#[derive(Clone, Debug)]
236pub struct McpJsonResourceSpec {
237    uri: String,
238    definition: ResourceDefinition,
239    value: Arc<Value>,
240}
241
242impl McpJsonResourceSpec {
243    /// Build and validate an `application/json` resource backed by `value`.
244    ///
245    /// # Errors
246    ///
247    /// Returns [`McpToolError`] when the resource URI, name, title, description,
248    /// or MIME type is invalid.
249    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    /// Concrete resource URI.
272    pub fn uri(&self) -> &str {
273        &self.uri
274    }
275
276    /// Resource definition advertised by `resources/list`.
277    pub fn definition(&self) -> &ResourceDefinition {
278        &self.definition
279    }
280
281    /// Consume the spec and return its resource definition.
282    pub fn into_definition(self) -> ResourceDefinition {
283        self.definition
284    }
285}
286
287/// Return resource definitions for a distinct set of generated JSON resources.
288///
289/// # Errors
290///
291/// Returns [`McpToolError::DuplicateResource`] when `specs` contains the same
292/// URI more than once.
293pub 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
300/// Register generated JSON resources, failing if any URI is duplicated.
301///
302/// # Errors
303///
304/// Returns [`McpToolError`] when any spec URI is duplicated within the batch,
305/// is already registered on `server`, or cannot be registered by the server.
306pub 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
322/// Register generated JSON resources unless every URI is already present.
323///
324/// If only some resources are present, this fails with a duplicate-resource
325/// setup error instead of silently publishing a partial set.
326///
327/// # Errors
328///
329/// Returns [`McpToolError`] when the resource set is partially registered or
330/// contains duplicate URIs.
331pub 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
344/// Ensure generated JSON resource URIs are unique and not already registered.
345///
346/// # Errors
347///
348/// Returns [`McpToolError::DuplicateResource`] when a spec URI is duplicated
349/// within the batch or is already registered on `server`.
350pub 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
363/// Ensure generated JSON resource URIs are unique within one batch.
364///
365/// # Errors
366///
367/// Returns [`McpToolError::DuplicateResource`] when a spec URI appears more
368/// than once.
369pub 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
381/// Build and validate an MCP prompt definition.
382///
383/// # Errors
384///
385/// Returns [`McpToolError`] when the prompt name, title, description, or
386/// argument metadata is invalid.
387pub 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
399/// Build a prompt result containing one user text message.
400pub 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
406/// Validate an MCP tool name accepted by generated integrations.
407///
408/// # Errors
409///
410/// Returns [`McpToolError`] when `name` is outside the generated tool-name
411/// subset.
412pub 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
417/// Validate human-readable MCP tool metadata text.
418///
419/// # Errors
420///
421/// Returns [`McpToolError`] when `value` is empty or contains only whitespace.
422pub 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
427/// Validate MCP tool annotations accepted by generated integrations.
428///
429/// # Errors
430///
431/// Returns [`McpToolError`] when annotation hints conflict or annotation text is
432/// invalid.
433pub 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
441/// Validate an MCP tool definition accepted by this shared server.
442///
443/// # Errors
444///
445/// Returns [`McpToolError`] when the name, schemas, title, description,
446/// annotations, or icons are invalid.
447pub 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
477/// Validate a concrete MCP resource definition accepted by this shared server.
478///
479/// # Errors
480///
481/// Returns [`McpToolError`] when the URI, name, title, description, or MIME type
482/// is invalid.
483pub 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
498/// Validate an MCP resource template definition accepted by this shared server.
499///
500/// # Errors
501///
502/// Returns [`McpToolError`] when the URI template, name, title, description, or
503/// MIME type is invalid.
504pub 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
521/// Validate an MCP prompt definition accepted by this shared server.
522///
523/// # Errors
524///
525/// Returns [`McpToolError`] when the prompt name, title, description, or
526/// argument metadata is invalid.
527pub 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
584/// Converts an MCP schema wrapper into a JSON object.
585///
586/// # Errors
587///
588/// Returns [`McpToolError::InvalidSchema`] when `schema` is not a JSON object.
589pub 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}