Skip to main content

turbomcp_types/
definitions.rs

1//! Definition types for MCP capabilities.
2//!
3//! This module defines the metadata types that describe MCP server capabilities:
4//! - `Tool` - Tool definitions with input schemas
5//! - `Resource` - Resource definitions with URI templates
6//! - `Prompt` - Prompt definitions with arguments
7//! - `ServerInfo` - Server identification and version
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12#[cfg(not(feature = "std"))]
13use alloc::{
14    collections::BTreeMap as HashMap,
15    string::{String, ToString},
16    vec::Vec,
17};
18#[cfg(feature = "std")]
19use std::collections::HashMap;
20
21/// Server information for MCP initialization.
22#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
23pub struct ServerInfo {
24    /// Server name (machine-readable identifier)
25    pub name: String,
26    /// Server version
27    pub version: String,
28    /// Human-readable title
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub title: Option<String>,
31    /// Server description
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub description: Option<String>,
34    /// Server icons
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub icons: Option<Vec<Icon>>,
37    /// Website URL for this implementation
38    #[serde(rename = "websiteUrl", skip_serializing_if = "Option::is_none")]
39    pub website_url: Option<String>,
40}
41
42impl ServerInfo {
43    /// Create server info with name and version.
44    #[must_use]
45    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
46        Self {
47            name: name.into(),
48            version: version.into(),
49            ..Default::default()
50        }
51    }
52
53    /// Set the title.
54    #[must_use]
55    pub fn with_title(mut self, title: impl Into<String>) -> Self {
56        self.title = Some(title.into());
57        self
58    }
59
60    /// Set the description.
61    #[must_use]
62    pub fn with_description(mut self, description: impl Into<String>) -> Self {
63        self.description = Some(description.into());
64        self
65    }
66
67    /// Add an icon.
68    #[must_use]
69    pub fn with_icon(mut self, icon: Icon) -> Self {
70        self.icons.get_or_insert_with(Vec::new).push(icon);
71        self
72    }
73
74    /// Set the website URL.
75    #[must_use]
76    pub fn with_website_url(mut self, url: impl Into<String>) -> Self {
77        self.website_url = Some(url.into());
78        self
79    }
80}
81
82/// Spec-aligned alias for [`ServerInfo`].
83///
84/// MCP 2025-11-25 calls this type `Implementation` for both server and client
85/// identity. The Rust name `ServerInfo` predates the spec; this alias makes
86/// both names available.
87pub type Implementation = ServerInfo;
88
89/// Icon for tools, resources, prompts, or servers.
90#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
91pub struct Icon {
92    /// URI of the icon (HTTP/HTTPS or data: URI)
93    pub src: String,
94    /// MIME type of the icon
95    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
96    pub mime_type: Option<String>,
97    /// Sized icons (e.g., "48x48", "96x96", "any")
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub sizes: Option<Vec<String>>,
100    /// Theme for which this icon is designed
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub theme: Option<IconTheme>,
103}
104
105/// Theme for an icon.
106#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
107#[serde(rename_all = "lowercase")]
108pub enum IconTheme {
109    /// Designed for light backgrounds
110    Light,
111    /// Designed for dark backgrounds
112    Dark,
113}
114
115impl core::fmt::Display for IconTheme {
116    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
117        match self {
118            Self::Light => f.write_str("light"),
119            Self::Dark => f.write_str("dark"),
120        }
121    }
122}
123
124impl Icon {
125    /// Create a new icon from a URI.
126    #[must_use]
127    pub fn new(src: impl Into<String>) -> Self {
128        Self {
129            src: src.into(),
130            ..Default::default()
131        }
132    }
133
134    /// Set the MIME type.
135    #[must_use]
136    pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
137        self.mime_type = Some(mime_type.into());
138        self
139    }
140
141    /// Set the sizes.
142    #[must_use]
143    pub fn with_sizes(mut self, sizes: Vec<impl Into<String>>) -> Self {
144        self.sizes = Some(sizes.into_iter().map(Into::into).collect());
145        self
146    }
147
148    /// Set the theme.
149    #[must_use]
150    pub fn with_theme(mut self, theme: IconTheme) -> Self {
151        self.theme = Some(theme);
152        self
153    }
154}
155
156/// Tool definition.
157///
158/// Describes a callable tool with its input schema and metadata.
159#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
160pub struct Tool {
161    /// Tool name (machine-readable identifier)
162    pub name: String,
163    /// Tool description
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub description: Option<String>,
166    /// JSON Schema for input parameters
167    #[serde(rename = "inputSchema")]
168    pub input_schema: ToolInputSchema,
169    /// Human-readable title
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub title: Option<String>,
172    /// Tool icons
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub icons: Option<Vec<Icon>>,
175    /// Tool annotations (hints about behavior)
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub annotations: Option<ToolAnnotations>,
178    /// Tool execution properties
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub execution: Option<ToolExecution>,
181    /// Output schema for structured results
182    #[serde(rename = "outputSchema", skip_serializing_if = "Option::is_none")]
183    pub output_schema: Option<ToolOutputSchema>,
184    /// Extension metadata (tags, version, etc.)
185    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
186    pub meta: Option<HashMap<String, Value>>,
187}
188
189impl Tool {
190    /// Create a new tool with name and description.
191    #[must_use]
192    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
193        Self {
194            name: name.into(),
195            description: Some(description.into()),
196            input_schema: ToolInputSchema::default(),
197            ..Default::default()
198        }
199    }
200
201    /// Set the input schema.
202    #[must_use]
203    pub fn with_schema(mut self, schema: ToolInputSchema) -> Self {
204        self.input_schema = schema;
205        self
206    }
207
208    /// Set the output schema.
209    #[must_use]
210    pub fn with_output_schema(mut self, schema: ToolOutputSchema) -> Self {
211        self.output_schema = Some(schema);
212        self
213    }
214
215    /// Set the annotations.
216    #[must_use]
217    pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
218        self.annotations = Some(annotations);
219        self
220    }
221
222    /// Add an icon.
223    #[must_use]
224    pub fn with_icon(mut self, icon: Icon) -> Self {
225        self.icons.get_or_insert_with(Vec::new).push(icon);
226        self
227    }
228
229    /// Set tool execution properties.
230    #[must_use]
231    pub fn with_execution(mut self, execution: ToolExecution) -> Self {
232        self.execution = Some(execution);
233        self
234    }
235
236    /// Mark as read-only (hint for clients).
237    #[must_use]
238    pub fn read_only(mut self) -> Self {
239        self.annotations = Some(self.annotations.unwrap_or_default().with_read_only(true));
240        self
241    }
242
243    /// Mark as destructive (hint for clients).
244    #[must_use]
245    pub fn destructive(mut self) -> Self {
246        self.annotations = Some(self.annotations.unwrap_or_default().with_destructive(true));
247        self
248    }
249}
250
251/// Execution properties for a tool.
252#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
253pub struct ToolExecution {
254    /// Support level for task-augmented execution
255    #[serde(rename = "taskSupport", skip_serializing_if = "Option::is_none")]
256    pub task_support: Option<TaskSupportLevel>,
257}
258
259/// Task support level for tools.
260#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
261#[serde(rename_all = "lowercase")]
262pub enum TaskSupportLevel {
263    /// Tool does not support task-augmented execution (default)
264    Forbidden,
265    /// Tool may support task-augmented execution
266    Optional,
267    /// Tool requires task-augmented execution
268    Required,
269}
270
271impl core::fmt::Display for TaskSupportLevel {
272    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
273        match self {
274            Self::Forbidden => f.write_str("forbidden"),
275            Self::Optional => f.write_str("optional"),
276            Self::Required => f.write_str("required"),
277        }
278    }
279}
280
281/// JSON Schema dialect URI defaulted by MCP 2025-11-25 (SEP-1613).
282///
283/// Spec language: "Establish JSON Schema 2020-12 as the default dialect for
284/// MCP schema definitions." Tools, resources, prompts, and elicitation
285/// schemas should advertise this `$schema` value unless they intentionally
286/// declare a different dialect.
287pub const JSON_SCHEMA_DIALECT_2020_12: &str = "https://json-schema.org/draft/2020-12/schema";
288
289/// Build the default `extra_keywords` map containing the SEP-1613 dialect.
290fn default_schema_extras() -> HashMap<String, Value> {
291    let mut m = HashMap::new();
292    m.insert(
293        "$schema".to_string(),
294        Value::String(JSON_SCHEMA_DIALECT_2020_12.to_string()),
295    );
296    m
297}
298
299/// JSON Schema for tool input parameters.
300///
301/// `properties` is stored as a raw `serde_json::Value` (typically an object) to
302/// keep the surface forward-compatible with arbitrary JSON Schema. Use
303/// [`ToolInputSchema::properties_as_object`] for map-style access.
304#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
305pub struct ToolInputSchema {
306    /// Schema type declaration. This may be a string or an array of strings.
307    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
308    pub schema_type: Option<Value>,
309    /// Property definitions (raw JSON Schema object).
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub properties: Option<Value>,
312    /// Required property names
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub required: Option<Vec<String>>,
315    /// Whether additional properties are allowed, or a schema constraining them.
316    #[serde(
317        rename = "additionalProperties",
318        skip_serializing_if = "Option::is_none"
319    )]
320    pub additional_properties: Option<Value>,
321    /// Additional JSON Schema keywords preserved losslessly.
322    #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
323    pub extra_keywords: HashMap<String, Value>,
324}
325
326impl Default for ToolInputSchema {
327    fn default() -> Self {
328        Self {
329            schema_type: Some(Value::String("object".into())),
330            properties: None,
331            required: None,
332            additional_properties: Some(Value::Bool(false)),
333            extra_keywords: default_schema_extras(),
334        }
335    }
336}
337
338impl ToolInputSchema {
339    /// Create an empty object schema.
340    #[must_use]
341    pub fn empty() -> Self {
342        Self::default()
343    }
344
345    /// Create from a JSON value (typically from schemars).
346    ///
347    /// Falls back to [`ToolInputSchema::default`] if the value cannot be
348    /// deserialized as a schema (e.g. not an object).
349    #[must_use]
350    pub fn from_value(value: Value) -> Self {
351        serde_json::from_value(value).unwrap_or_default()
352    }
353
354    /// Borrow `properties` as a JSON object map if present.
355    #[must_use]
356    pub fn properties_as_object(&self) -> Option<&serde_json::Map<String, Value>> {
357        self.properties.as_ref().and_then(|v| v.as_object())
358    }
359
360    /// Build a schema from an explicit property map.
361    #[must_use]
362    pub fn with_properties(properties: HashMap<String, Value>) -> Self {
363        let obj: serde_json::Map<String, Value> = properties.into_iter().collect();
364        Self {
365            schema_type: Some(Value::String("object".into())),
366            properties: Some(Value::Object(obj)),
367            required: None,
368            additional_properties: None,
369            extra_keywords: default_schema_extras(),
370        }
371    }
372
373    /// Build a schema from property map + `required` list.
374    #[must_use]
375    pub fn with_required_properties(
376        properties: HashMap<String, Value>,
377        required: Vec<String>,
378    ) -> Self {
379        let obj: serde_json::Map<String, Value> = properties.into_iter().collect();
380        Self {
381            schema_type: Some(Value::String("object".into())),
382            properties: Some(Value::Object(obj)),
383            required: Some(required),
384            additional_properties: Some(Value::Bool(false)),
385            extra_keywords: default_schema_extras(),
386        }
387    }
388
389    /// Add a property to the schema (builder style).
390    #[must_use]
391    pub fn add_property(mut self, name: impl Into<String>, property: Value) -> Self {
392        let obj = match self.properties.take() {
393            Some(Value::Object(m)) => m,
394            _ => serde_json::Map::new(),
395        };
396        let mut obj = obj;
397        obj.insert(name.into(), property);
398        self.properties = Some(Value::Object(obj));
399        self
400    }
401
402    /// Mark a property as required (builder style). No-op if already required.
403    #[must_use]
404    pub fn require_property(mut self, name: impl Into<String>) -> Self {
405        let name = name.into();
406        let required = self.required.get_or_insert_with(Vec::new);
407        if !required.contains(&name) {
408            required.push(name);
409        }
410        self
411    }
412}
413
414/// JSON Schema for a tool's structured output (`outputSchema` per MCP spec).
415///
416/// Has the same shape as [`ToolInputSchema`]; a separate struct preserves the
417/// distinction between input and output schemas at the Rust type level.
418#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
419pub struct ToolOutputSchema {
420    /// Schema type declaration. This may be a string or an array of strings.
421    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
422    pub schema_type: Option<Value>,
423    /// Property definitions (raw JSON Schema object).
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub properties: Option<Value>,
426    /// Required property names.
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub required: Option<Vec<String>>,
429    /// Whether additional properties are allowed, or a schema constraining them.
430    #[serde(
431        rename = "additionalProperties",
432        skip_serializing_if = "Option::is_none"
433    )]
434    pub additional_properties: Option<Value>,
435    /// Additional JSON Schema keywords preserved losslessly.
436    #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
437    pub extra_keywords: HashMap<String, Value>,
438}
439
440impl Default for ToolOutputSchema {
441    fn default() -> Self {
442        Self {
443            schema_type: Some(Value::String("object".into())),
444            properties: None,
445            required: None,
446            additional_properties: None,
447            extra_keywords: default_schema_extras(),
448        }
449    }
450}
451
452impl ToolOutputSchema {
453    /// Create an empty object output schema.
454    #[must_use]
455    pub fn empty() -> Self {
456        Self::default()
457    }
458
459    /// Create from a JSON value (e.g. a `schemars`-generated schema).
460    #[must_use]
461    pub fn from_value(value: Value) -> Self {
462        serde_json::from_value(value).unwrap_or_default()
463    }
464
465    /// Borrow `properties` as a JSON object map if present.
466    #[must_use]
467    pub fn properties_as_object(&self) -> Option<&serde_json::Map<String, Value>> {
468        self.properties.as_ref().and_then(|v| v.as_object())
469    }
470}
471
472/// Annotations for tools describing behavior hints.
473#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
474pub struct ToolAnnotations {
475    /// Hint that this tool is read-only
476    #[serde(rename = "readOnlyHint", skip_serializing_if = "Option::is_none")]
477    pub read_only_hint: Option<bool>,
478    /// Hint that this tool has destructive effects
479    #[serde(rename = "destructiveHint", skip_serializing_if = "Option::is_none")]
480    pub destructive_hint: Option<bool>,
481    /// Hint that this tool is idempotent
482    #[serde(rename = "idempotentHint", skip_serializing_if = "Option::is_none")]
483    pub idempotent_hint: Option<bool>,
484    /// Hint that this tool operates on an open world
485    #[serde(rename = "openWorldHint", skip_serializing_if = "Option::is_none")]
486    pub open_world_hint: Option<bool>,
487    /// Human-readable title
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub title: Option<String>,
490}
491
492impl ToolAnnotations {
493    /// Set the read-only hint.
494    #[must_use]
495    pub fn with_read_only(mut self, value: bool) -> Self {
496        self.read_only_hint = Some(value);
497        self
498    }
499
500    /// Set the destructive hint.
501    #[must_use]
502    pub fn with_destructive(mut self, value: bool) -> Self {
503        self.destructive_hint = Some(value);
504        self
505    }
506
507    /// Set the idempotent hint.
508    #[must_use]
509    pub fn with_idempotent(mut self, value: bool) -> Self {
510        self.idempotent_hint = Some(value);
511        self
512    }
513
514    /// Set the open world hint.
515    #[must_use]
516    pub fn with_open_world(mut self, value: bool) -> Self {
517        self.open_world_hint = Some(value);
518        self
519    }
520}
521
522/// Resource definition.
523///
524/// Describes a readable resource with its URI template and metadata.
525#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
526pub struct Resource {
527    /// Resource URI or URI template
528    pub uri: String,
529    /// Resource name (machine-readable identifier)
530    pub name: String,
531    /// Resource description
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub description: Option<String>,
534    /// Human-readable title
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub title: Option<String>,
537    /// Resource icons
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub icons: Option<Vec<Icon>>,
540    /// MIME type of the resource content
541    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
542    pub mime_type: Option<String>,
543    /// Resource annotations
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub annotations: Option<ResourceAnnotations>,
546    /// Size in bytes (if known)
547    #[serde(skip_serializing_if = "Option::is_none")]
548    pub size: Option<u64>,
549    /// Extension metadata (tags, version, etc.)
550    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
551    pub meta: Option<HashMap<String, Value>>,
552}
553
554impl Resource {
555    /// Create a new resource with URI and name.
556    #[must_use]
557    pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
558        Self {
559            uri: uri.into(),
560            name: name.into(),
561            ..Default::default()
562        }
563    }
564
565    /// Set the description.
566    #[must_use]
567    pub fn with_description(mut self, description: impl Into<String>) -> Self {
568        self.description = Some(description.into());
569        self
570    }
571
572    /// Set the MIME type.
573    #[must_use]
574    pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
575        self.mime_type = Some(mime_type.into());
576        self
577    }
578
579    /// Set the size.
580    #[must_use]
581    pub fn with_size(mut self, size: u64) -> Self {
582        self.size = Some(size);
583        self
584    }
585
586    /// Add an icon.
587    #[must_use]
588    pub fn with_icon(mut self, icon: Icon) -> Self {
589        self.icons.get_or_insert_with(Vec::new).push(icon);
590        self
591    }
592}
593
594/// Annotations for resources.
595///
596/// Same structure as content `Annotations` per MCP 2025-11-25.
597#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
598pub struct ResourceAnnotations {
599    /// Target audience
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub audience: Option<Vec<crate::Role>>,
602    /// Priority level (0.0 to 1.0)
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub priority: Option<f64>,
605    /// Last modified timestamp (ISO 8601)
606    #[serde(rename = "lastModified", skip_serializing_if = "Option::is_none")]
607    pub last_modified: Option<String>,
608}
609
610/// Resource template definition.
611///
612/// Describes a URI template for dynamic resources.
613#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
614pub struct ResourceTemplate {
615    /// URI template (RFC 6570)
616    #[serde(rename = "uriTemplate")]
617    pub uri_template: String,
618    /// Template name
619    pub name: String,
620    /// Template description
621    #[serde(skip_serializing_if = "Option::is_none")]
622    pub description: Option<String>,
623    /// Human-readable title
624    #[serde(skip_serializing_if = "Option::is_none")]
625    pub title: Option<String>,
626    /// Template icons
627    #[serde(skip_serializing_if = "Option::is_none")]
628    pub icons: Option<Vec<Icon>>,
629    /// MIME type of resources from this template
630    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
631    pub mime_type: Option<String>,
632    /// Template annotations
633    #[serde(skip_serializing_if = "Option::is_none")]
634    pub annotations: Option<ResourceAnnotations>,
635    /// Extension metadata
636    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
637    pub meta: Option<HashMap<String, Value>>,
638}
639
640impl ResourceTemplate {
641    /// Create a new resource template, without validation.
642    ///
643    /// Use [`ResourceTemplate::try_new`] for structural validation of the URI
644    /// template string against RFC 6570 brace balance.
645    #[must_use]
646    pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
647        Self {
648            uri_template: uri_template.into(),
649            name: name.into(),
650            ..Default::default()
651        }
652    }
653
654    /// Create a new resource template, validating the URI template against
655    /// the structural shape of RFC 6570 (matched `{` / `}` without nesting).
656    ///
657    /// This is a lightweight check that catches the common drift modes — typos
658    /// in expression names and missing closing braces — without attempting
659    /// full RFC 6570 expansion.
660    pub fn try_new(
661        uri_template: impl Into<String>,
662        name: impl Into<String>,
663    ) -> Result<Self, &'static str> {
664        let uri_template = uri_template.into();
665        validate_uri_template(&uri_template)?;
666        Ok(Self {
667            uri_template,
668            name: name.into(),
669            ..Default::default()
670        })
671    }
672
673    /// Set the description.
674    #[must_use]
675    pub fn with_description(mut self, description: impl Into<String>) -> Self {
676        self.description = Some(description.into());
677        self
678    }
679
680    /// Add an icon.
681    #[must_use]
682    pub fn with_icon(mut self, icon: Icon) -> Self {
683        self.icons.get_or_insert_with(Vec::new).push(icon);
684        self
685    }
686}
687
688/// Validate a string against the structural shape of an RFC 6570 URI Template.
689///
690/// Checks for balanced `{` / `}` braces without nesting. This does not attempt
691/// full RFC 6570 expansion — it catches typos and missing closing braces.
692pub fn validate_uri_template(s: &str) -> Result<(), &'static str> {
693    let mut depth = 0i32;
694    let mut current_expr_start: Option<usize> = None;
695    let bytes = s.as_bytes();
696    for (i, ch) in s.char_indices() {
697        match ch {
698            '{' => {
699                depth += 1;
700                if depth > 1 {
701                    return Err("URI template: nested '{' not allowed in RFC 6570");
702                }
703                current_expr_start = Some(i + 1);
704            }
705            '}' => {
706                depth -= 1;
707                if depth < 0 {
708                    return Err("URI template: unbalanced '}' (no matching '{')");
709                }
710                if let Some(start) = current_expr_start {
711                    let body = &bytes[start..i];
712                    if body.is_empty() {
713                        return Err("URI template: empty expression `{}`");
714                    }
715                    // Skip leading RFC 6570 operator if present (one of +#./;?&)
716                    let body_start =
717                        if matches!(body[0], b'+' | b'#' | b'.' | b'/' | b';' | b'?' | b'&') {
718                            1
719                        } else {
720                            0
721                        };
722                    let var_bytes = &body[body_start..];
723                    if var_bytes.is_empty() {
724                        return Err("URI template: operator without variable name");
725                    }
726                    let first = var_bytes[0];
727                    if !(first.is_ascii_alphabetic() || first == b'_') {
728                        return Err(
729                            "URI template: variable name must start with a letter or underscore",
730                        );
731                    }
732                    for &b in var_bytes {
733                        if !(b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b',') {
734                            return Err(
735                                "URI template: invalid character in variable name (allowed: ALPHA / DIGIT / '_' / '.' / ',')",
736                            );
737                        }
738                    }
739                }
740                current_expr_start = None;
741            }
742            _ => {}
743        }
744    }
745    if depth != 0 {
746        return Err("URI template: unbalanced '{' (missing closing '}')");
747    }
748    Ok(())
749}
750
751/// Prompt definition.
752///
753/// Describes a retrievable prompt with its arguments and metadata.
754#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
755pub struct Prompt {
756    /// Prompt name (machine-readable identifier)
757    pub name: String,
758    /// Prompt description
759    #[serde(skip_serializing_if = "Option::is_none")]
760    pub description: Option<String>,
761    /// Human-readable title
762    #[serde(skip_serializing_if = "Option::is_none")]
763    pub title: Option<String>,
764    /// Prompt icons
765    #[serde(skip_serializing_if = "Option::is_none")]
766    pub icons: Option<Vec<Icon>>,
767    /// Prompt arguments
768    #[serde(skip_serializing_if = "Option::is_none")]
769    pub arguments: Option<Vec<PromptArgument>>,
770    /// Extension metadata (tags, version, etc.)
771    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
772    pub meta: Option<HashMap<String, Value>>,
773}
774
775impl Prompt {
776    /// Create a new prompt with name and description.
777    #[must_use]
778    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
779        Self {
780            name: name.into(),
781            description: Some(description.into()),
782            ..Default::default()
783        }
784    }
785
786    /// Add an argument to the prompt.
787    #[must_use]
788    pub fn with_argument(mut self, arg: PromptArgument) -> Self {
789        self.arguments.get_or_insert_with(Vec::new).push(arg);
790        self
791    }
792
793    /// Add a required argument.
794    #[must_use]
795    pub fn with_required_arg(
796        self,
797        name: impl Into<String>,
798        description: impl Into<String>,
799    ) -> Self {
800        self.with_argument(PromptArgument::required(name, description))
801    }
802
803    /// Add an optional argument.
804    #[must_use]
805    pub fn with_optional_arg(
806        self,
807        name: impl Into<String>,
808        description: impl Into<String>,
809    ) -> Self {
810        self.with_argument(PromptArgument::optional(name, description))
811    }
812
813    /// Add an icon.
814    #[must_use]
815    pub fn with_icon(mut self, icon: Icon) -> Self {
816        self.icons.get_or_insert_with(Vec::new).push(icon);
817        self
818    }
819}
820
821/// Argument definition for prompts.
822///
823/// Extends `BaseMetadata` per MCP 2025-11-25 (`name` + optional `title`).
824#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
825pub struct PromptArgument {
826    /// Argument name (machine-readable identifier)
827    pub name: String,
828    /// Human-readable title
829    #[serde(skip_serializing_if = "Option::is_none")]
830    pub title: Option<String>,
831    /// Argument description
832    #[serde(skip_serializing_if = "Option::is_none")]
833    pub description: Option<String>,
834    /// Whether this argument is required
835    #[serde(skip_serializing_if = "Option::is_none")]
836    pub required: Option<bool>,
837}
838
839impl PromptArgument {
840    /// Create a required argument.
841    #[must_use]
842    pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
843        Self {
844            name: name.into(),
845            title: None,
846            description: Some(description.into()),
847            required: Some(true),
848        }
849    }
850
851    /// Create an optional argument.
852    #[must_use]
853    pub fn optional(name: impl Into<String>, description: impl Into<String>) -> Self {
854        Self {
855            name: name.into(),
856            title: None,
857            description: Some(description.into()),
858            required: Some(false),
859        }
860    }
861}
862
863#[cfg(test)]
864mod tests {
865    use super::*;
866
867    #[test]
868    fn test_server_info() {
869        let info = ServerInfo::new("my-server", "1.0.0")
870            .with_title("My Server")
871            .with_description("A test server")
872            .with_icon(Icon::new("https://example.com/icon.png"));
873
874        assert_eq!(info.name, "my-server");
875        assert_eq!(info.version, "1.0.0");
876        assert_eq!(info.title, Some("My Server".into()));
877        assert_eq!(info.icons.as_ref().unwrap().len(), 1);
878        assert_eq!(
879            info.icons.as_ref().unwrap()[0].src,
880            "https://example.com/icon.png"
881        );
882    }
883
884    #[test]
885    fn test_tool_builder() {
886        // Test with_annotations directly
887        let tool = Tool::new("add", "Add two numbers").with_annotations(
888            ToolAnnotations::default()
889                .with_read_only(true)
890                .with_idempotent(true),
891        );
892
893        assert_eq!(tool.name, "add");
894        assert!(tool.annotations.as_ref().unwrap().read_only_hint.unwrap());
895        assert!(tool.annotations.as_ref().unwrap().idempotent_hint.unwrap());
896    }
897
898    #[test]
899    fn test_tool_read_only() {
900        let tool = Tool::new("query", "Query data").read_only();
901        assert!(tool.annotations.as_ref().unwrap().read_only_hint.unwrap());
902    }
903
904    #[test]
905    fn test_tool_destructive() {
906        let tool = Tool::new("delete", "Delete data").destructive();
907        assert!(tool.annotations.as_ref().unwrap().destructive_hint.unwrap());
908    }
909
910    #[test]
911    fn test_resource_builder() {
912        let resource = Resource::new("file:///test.txt", "test")
913            .with_description("A test file")
914            .with_mime_type("text/plain");
915
916        assert_eq!(resource.uri, "file:///test.txt");
917        assert_eq!(resource.mime_type, Some("text/plain".into()));
918    }
919
920    #[test]
921    fn test_prompt_builder() {
922        let prompt = Prompt::new("greeting", "A greeting prompt")
923            .with_required_arg("name", "Name to greet")
924            .with_optional_arg("style", "Greeting style");
925
926        assert_eq!(prompt.name, "greeting");
927        assert_eq!(prompt.arguments.as_ref().unwrap().len(), 2);
928        assert!(prompt.arguments.as_ref().unwrap()[0].required.unwrap());
929        assert!(!prompt.arguments.as_ref().unwrap()[1].required.unwrap());
930    }
931
932    #[test]
933    fn test_tool_serde() {
934        let tool = Tool::new("test", "Test tool");
935        let json = serde_json::to_string(&tool).unwrap();
936        assert!(json.contains("\"name\":\"test\""));
937        assert!(json.contains("\"inputSchema\""));
938    }
939}