Skip to main content

rskit_tool/
definition.rs

1//! Tool definition types — MCP-aligned metadata.
2
3use serde::{Deserialize, Serialize};
4
5use crate::envelope::Envelope;
6use crate::io::ToolSchema;
7
8/// How the frontend should handle the tool result.
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
10#[serde(rename_all = "lowercase")]
11#[non_exhaustive]
12pub enum ExecutionHint {
13    /// Tool executes a real operation; result is authoritative.
14    #[default]
15    Backend,
16    /// Tool only validates/extracts params; frontend drives the action.
17    Ui,
18    /// Tool executes backend AND frontend should refresh/navigate.
19    Hybrid,
20    /// Unknown hint from a newer protocol version; normalizes to Backend.
21    #[serde(other)]
22    Unknown,
23}
24
25impl Serialize for ExecutionHint {
26    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
27        match self.effective() {
28            Self::Backend | Self::Unknown => serializer.serialize_str("backend"),
29            Self::Ui => serializer.serialize_str("ui"),
30            Self::Hybrid => serializer.serialize_str("hybrid"),
31        }
32    }
33}
34
35impl ExecutionHint {
36    /// Return the effective hint, mapping Unknown → Backend.
37    #[must_use]
38    pub const fn effective(self) -> Self {
39        match self {
40            Self::Unknown => Self::Backend,
41            other => other,
42        }
43    }
44}
45
46/// Optional hints about tool behavior (MCP-aligned).
47#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
48pub struct Annotations {
49    /// Human-readable title.
50    #[serde(skip_serializing_if = "String::is_empty", default)]
51    pub title: String,
52    /// Grouping category for UI.
53    #[serde(skip_serializing_if = "String::is_empty", default)]
54    pub category: String,
55    /// Freeform tags for filtering.
56    #[serde(skip_serializing_if = "Vec::is_empty", default)]
57    pub tags: Vec<String>,
58    /// True if repeated calls produce the same result.
59    #[serde(skip_serializing_if = "Option::is_none", default)]
60    pub idempotent_hint: Option<bool>,
61    /// Tells the frontend how to handle the tool result.
62    #[serde(default)]
63    pub execution_hint: ExecutionHint,
64}
65
66/// Describes a tool — MCP-aligned metadata.
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub struct Definition {
69    /// Unique tool identifier.
70    pub name: String,
71    /// Human-readable description.
72    pub description: String,
73    /// JSON Schema for the tool's input parameters.
74    pub input_schema: ToolSchema,
75    /// Optional JSON Schema for the tool's output.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub output_schema: Option<ToolSchema>,
78    /// Behavioral hints that are orthogonal to the executable permission envelope.
79    #[serde(default)]
80    pub annotations: Annotations,
81    /// Executable permission envelope — the single source of truth for what the tool may do at runtime.
82    /// It carries scopes, network/filesystem/ subprocess rules, safety classification,
83    /// sensitive-invocation predicates, and data-classification hints. Defaults deny network, filesystem,
84    /// and subprocess access.
85    #[serde(default)]
86    pub envelope: Envelope,
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn unknown_execution_hint_effectively_serializes_as_backend() {
95        let hint: ExecutionHint = serde_json::from_str(r#""future""#).expect("unknown maps");
96
97        assert_eq!(hint.effective(), ExecutionHint::Backend);
98        assert_eq!(
99            serde_json::to_value(hint).expect("hint serialises"),
100            serde_json::json!("backend")
101        );
102    }
103}