Skip to main content

component_shape_mcp/
definitions.rs

1use super::*;
2
3/// Build and validate an MCP tool definition.
4///
5/// # Errors
6///
7/// Returns [`McpToolError`] when the tool metadata is invalid or when the input
8/// schema is not an object-shaped MCP tool schema, or the output schema is not
9/// a JSON object.
10pub 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
38/// Build and validate a typed MCP tool definition from its input type.
39///
40/// # Errors
41///
42/// Returns [`McpToolError`] when the tool metadata is invalid or when the input
43/// type's generated schema or output schema is not accepted.
44pub 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
63/// Build and validate an MCP tool definition with tool annotations.
64///
65/// # Errors
66///
67/// Returns [`McpToolError`] when the annotations, metadata, input schema, or
68/// output schema are invalid.
69pub 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
85/// Build and validate a typed MCP tool definition with tool annotations.
86///
87/// # Errors
88///
89/// Returns [`McpToolError`] when the annotations, metadata, input schema, or
90/// output schema are invalid.
91pub 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
112/// Build and validate an MCP tool definition from generated metadata.
113///
114/// # Errors
115///
116/// Returns [`McpToolError`] when the generated metadata, input schema, or output
117/// schema is invalid.
118pub 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
139/// Build and validate a typed MCP tool definition from generated metadata.
140///
141/// # Errors
142///
143/// Returns [`McpToolError`] when the generated metadata, typed input schema, or
144/// output schema is invalid.
145pub 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
157/// Build and validate a concrete MCP resource definition.
158///
159/// # Errors
160///
161/// Returns [`McpToolError`] when the resource URI, name, title, description, or
162/// MIME type is invalid.
163pub 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
178/// Build and validate an MCP resource template definition.
179///
180/// # Errors
181///
182/// Returns [`McpToolError`] when the template URI, name, title, description, or
183/// MIME type is invalid.
184pub 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
199/// Build a text resource result with an explicit MIME type.
200pub 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
210/// Encode a JSON value as a pretty-printed `application/json` resource result.
211///
212/// # Errors
213///
214/// Returns [`McpToolError`] when `value` cannot be serialized as JSON.
215pub 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/// Validated static JSON resource ready to register on an [`McpServer`].
226#[derive(Clone, Debug)]
227pub struct McpJsonResourceSpec {
228    uri: String,
229    definition: ResourceDefinition,
230    value: Arc<Value>,
231}
232
233impl McpJsonResourceSpec {
234    /// Build and validate an `application/json` resource backed by `value`.
235    ///
236    /// # Errors
237    ///
238    /// Returns [`McpToolError`] when the resource URI, name, title, description,
239    /// or MIME type is invalid.
240    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    /// Concrete resource URI.
263    pub fn uri(&self) -> &str {
264        &self.uri
265    }
266
267    /// Resource definition advertised by `resources/list`.
268    pub fn definition(&self) -> &ResourceDefinition {
269        &self.definition
270    }
271
272    /// Consume the spec and return its resource definition.
273    pub fn into_definition(self) -> ResourceDefinition {
274        self.definition
275    }
276}
277
278/// Return resource definitions for a distinct set of generated JSON resources.
279///
280/// # Errors
281///
282/// Returns [`McpToolError::DuplicateResource`] when `specs` contains the same
283/// URI more than once.
284pub 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
291/// Register generated JSON resources, failing if any URI is duplicated.
292///
293/// # Errors
294///
295/// Returns [`McpToolError`] when any spec URI is duplicated within the batch,
296/// is already registered on `server`, or cannot be registered by the server.
297pub 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
313/// Register generated JSON resources unless every URI is already present.
314///
315/// If only some resources are present, this fails with a duplicate-resource
316/// setup error instead of silently publishing a partial set.
317///
318/// # Errors
319///
320/// Returns [`McpToolError`] when the resource set is partially registered or
321/// contains duplicate URIs.
322pub 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
335/// Ensure generated JSON resource URIs are unique and not already registered.
336///
337/// # Errors
338///
339/// Returns [`McpToolError::DuplicateResource`] when a spec URI is duplicated
340/// within the batch or is already registered on `server`.
341pub 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
354/// Ensure generated JSON resource URIs are unique within one batch.
355///
356/// # Errors
357///
358/// Returns [`McpToolError::DuplicateResource`] when a spec URI appears more
359/// than once.
360pub 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
372/// Build and validate an MCP prompt definition.
373///
374/// # Errors
375///
376/// Returns [`McpToolError`] when the prompt name, title, description, or
377/// argument metadata is invalid.
378pub 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
390/// Build a prompt result containing one user text message.
391pub 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
397/// Validate an MCP tool name accepted by generated integrations.
398///
399/// # Errors
400///
401/// Returns [`McpToolError`] when `name` is outside the generated tool-name
402/// subset.
403pub 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
408/// Validate human-readable MCP tool metadata text.
409///
410/// # Errors
411///
412/// Returns [`McpToolError`] when `value` is empty or contains only whitespace.
413pub 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
418/// Validate MCP tool annotations accepted by generated integrations.
419///
420/// # Errors
421///
422/// Returns [`McpToolError`] when annotation hints conflict or annotation text is
423/// invalid.
424pub 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
432/// Validate an MCP tool definition accepted by this shared server.
433///
434/// # Errors
435///
436/// Returns [`McpToolError`] when the name, schemas, title, description,
437/// annotations, or icons are invalid.
438pub 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
458/// Validate a concrete MCP resource definition accepted by this shared server.
459///
460/// # Errors
461///
462/// Returns [`McpToolError`] when the URI, name, title, description, or MIME type
463/// is invalid.
464pub 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
479/// Validate an MCP resource template definition accepted by this shared server.
480///
481/// # Errors
482///
483/// Returns [`McpToolError`] when the URI template, name, title, description, or
484/// MIME type is invalid.
485pub 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
502/// Validate an MCP prompt definition accepted by this shared server.
503///
504/// # Errors
505///
506/// Returns [`McpToolError`] when the prompt name, title, description, or
507/// argument metadata is invalid.
508pub 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
565/// Converts an MCP schema wrapper into a JSON object.
566///
567/// # Errors
568///
569/// Returns [`McpToolError::InvalidSchema`] when `schema` is not a JSON object.
570pub 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}