ironcontext_core/
manifest.rs1use serde::{Deserialize, Serialize};
9
10use crate::SentinelError;
11
12#[derive(Debug, Clone, Serialize, Deserialize, Default)]
14pub struct Manifest {
15 #[serde(default)]
17 pub server: ServerInfo,
18 #[serde(default)]
20 pub tools: Vec<Tool>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, Default)]
24pub struct ServerInfo {
25 #[serde(default)]
26 pub name: String,
27 #[serde(default)]
28 pub version: String,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Tool {
34 pub name: String,
35 #[serde(default)]
36 pub description: String,
37 #[serde(rename = "inputSchema", default)]
39 pub input_schema: serde_json::Value,
40}
41
42impl Manifest {
43 pub fn from_slice(bytes: &[u8]) -> Result<Self, SentinelError> {
48 let v: serde_json::Value = serde_json::from_slice(bytes)?;
49 let root = match v.get("result") {
50 Some(r) => r.clone(),
51 None => v,
52 };
53
54 let server = root
55 .get("serverInfo")
56 .cloned()
57 .map(|s| serde_json::from_value::<ServerInfo>(s).unwrap_or_default())
58 .unwrap_or_default();
59
60 let tools_val = root
61 .get("tools")
62 .cloned()
63 .ok_or_else(|| SentinelError::InvalidManifest("missing `tools` array".into()))?;
64
65 let tools: Vec<Tool> = serde_json::from_value(tools_val)
66 .map_err(|e| SentinelError::InvalidManifest(format!("tools[] malformed: {e}")))?;
67
68 for t in &tools {
69 if t.name.trim().is_empty() {
70 return Err(SentinelError::InvalidManifest(
71 "tool has empty name".into(),
72 ));
73 }
74 }
75
76 Ok(Self { server, tools })
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn parses_tools_list_envelope() {
86 let raw = br#"{
87 "tools": [
88 {"name": "echo", "description": "Echoes input", "inputSchema": {"type":"object"}}
89 ]
90 }"#;
91 let m = Manifest::from_slice(raw).unwrap();
92 assert_eq!(m.tools.len(), 1);
93 assert_eq!(m.tools[0].name, "echo");
94 }
95
96 #[test]
97 fn parses_jsonrpc_envelope() {
98 let raw = br#"{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"t","description":"d"}]}}"#;
99 let m = Manifest::from_slice(raw).unwrap();
100 assert_eq!(m.tools[0].name, "t");
101 }
102
103 #[test]
104 fn parses_initialize_response() {
105 let raw = br#"{
106 "serverInfo": {"name":"acme","version":"1.0"},
107 "capabilities": {},
108 "tools": [{"name":"a","description":"","inputSchema":{}}]
109 }"#;
110 let m = Manifest::from_slice(raw).unwrap();
111 assert_eq!(m.server.name, "acme");
112 assert_eq!(m.tools.len(), 1);
113 }
114
115 #[test]
116 fn rejects_missing_tools() {
117 let raw = b"{}";
118 assert!(Manifest::from_slice(raw).is_err());
119 }
120
121 #[test]
122 fn rejects_empty_tool_name() {
123 let raw = br#"{"tools":[{"name":"","description":""}]}"#;
124 assert!(Manifest::from_slice(raw).is_err());
125 }
126}