Skip to main content

greentic_mcp/
types.rs

1use std::path::PathBuf;
2use std::time::Duration;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use thiserror::Error;
7
8/// Reference to a tool stored in the [`ToolMapConfig`](ToolMapConfig).
9#[derive(Clone, Debug, Deserialize, Serialize)]
10pub struct ToolRef {
11    pub name: String,
12    pub component: String,
13    pub entry: String,
14    #[serde(default)]
15    pub timeout_ms: Option<u64>,
16    #[serde(default)]
17    pub max_retries: Option<u32>,
18    #[serde(default)]
19    pub retry_backoff_ms: Option<u64>,
20}
21
22impl ToolRef {
23    /// Resolve the component path to a [`PathBuf`], if it is a filesystem path.
24    pub fn component_path(&self) -> PathBuf {
25        PathBuf::from(&self.component)
26    }
27
28    /// Timeout duration requested for this tool.
29    pub fn timeout(&self) -> Option<Duration> {
30        self.timeout_ms.map(Duration::from_millis)
31    }
32
33    /// Maximum retry attempts for this tool.
34    pub fn max_retries(&self) -> u32 {
35        self.max_retries.unwrap_or(0)
36    }
37
38    /// Base retry backoff in milliseconds.
39    pub fn retry_backoff(&self) -> Duration {
40        Duration::from_millis(self.retry_backoff_ms.unwrap_or(200))
41    }
42}
43
44/// Tool map configuration file structure.
45#[derive(Clone, Debug, Deserialize, Serialize)]
46pub struct ToolMapConfig {
47    pub tools: Vec<ToolRef>,
48}
49
50/// Input payload for a tool invocation.
51#[derive(Clone, Debug, Deserialize, Serialize)]
52pub struct ToolInput {
53    pub payload: Value,
54}
55
56/// Output payload for a tool invocation.
57#[derive(Clone, Debug, Deserialize, Serialize)]
58pub struct ToolOutput {
59    pub payload: Value,
60    #[serde(
61        default,
62        skip_serializing_if = "Option::is_none",
63        rename = "structuredContent"
64    )]
65    pub structured_content: Option<Value>,
66}
67
68/// Errors surfaced by the MCP executor.
69#[derive(Debug, Error)]
70pub enum McpError {
71    #[error("tool `{0}` not found")]
72    ToolNotFound(String),
73    #[error("invalid input: {0}")]
74    InvalidInput(String),
75    #[error("execution failed: {0}")]
76    ExecutionFailed(String),
77    #[error("tool `{name}` timed out after {timeout:?}")]
78    Timeout { name: String, timeout: Duration },
79    #[error("transient failure invoking `{0}`: {1}")]
80    Transient(String, String),
81    #[error("internal error: {0}")]
82    Internal(String),
83    #[error(transparent)]
84    Io(#[from] std::io::Error),
85    #[error(transparent)]
86    Config(#[from] serde_yaml_bw::Error),
87    #[error(transparent)]
88    Json(#[from] serde_json::Error),
89}
90
91impl McpError {
92    pub fn tool_not_found(name: impl Into<String>) -> Self {
93        McpError::ToolNotFound(name.into())
94    }
95
96    pub fn timeout(name: impl Into<String>, timeout: Duration) -> Self {
97        McpError::Timeout {
98            name: name.into(),
99            timeout,
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use serde_json::json;
108
109    #[test]
110    fn captures_structured_content_in_output() {
111        let value = json!({
112            "payload": {"message": "ok"},
113            "structuredContent": {"result": "structured"}
114        });
115
116        let output: ToolOutput = serde_json::from_value(value).expect("deserialize");
117        assert_eq!(
118            output
119                .structured_content
120                .as_ref()
121                .and_then(|v| v.get("result"))
122                .and_then(Value::as_str),
123            Some("structured")
124        );
125        assert_eq!(
126            output.payload.get("message").and_then(Value::as_str),
127            Some("ok")
128        );
129    }
130}