Skip to main content

agentshield/parser/
json_schema.rs

1use crate::ir::tool_surface::{DeclaredPermission, PermissionType, ToolSurface};
2
3/// Extract tool definitions from an MCP-style JSON tool list.
4pub fn parse_tools_from_json(value: &serde_json::Value) -> Vec<ToolSurface> {
5    let mut tools = Vec::new();
6
7    let items = if let Some(arr) = value.as_array() {
8        arr.clone()
9    } else if let Some(arr) = value.get("tools").and_then(|v| v.as_array()) {
10        arr.clone()
11    } else {
12        return tools;
13    };
14
15    for item in &items {
16        let name = item
17            .get("name")
18            .and_then(|v| v.as_str())
19            .unwrap_or("unknown")
20            .to_string();
21        let description = item
22            .get("description")
23            .and_then(|v| v.as_str())
24            .map(|s| s.to_string());
25        let input_schema = item
26            .get("inputSchema")
27            .or(item.get("input_schema"))
28            .cloned();
29
30        // Infer permissions from description text
31        let desc_text = description.as_deref().unwrap_or("");
32        let permissions = infer_permissions_from_description(desc_text);
33
34        tools.push(ToolSurface {
35            name,
36            description,
37            input_schema,
38            output_schema: None,
39            declared_permissions: permissions,
40            defined_at: None,
41            declared_capabilities: Default::default(),
42            capability_declarations: Vec::new(),
43            observed_capabilities: Default::default(),
44            capability_observation_complete: false,
45            capability_evidence: Vec::new(),
46        });
47    }
48
49    tools
50}
51
52fn infer_permissions_from_description(desc: &str) -> Vec<DeclaredPermission> {
53    let lower = desc.to_lowercase();
54    let mut perms = Vec::new();
55
56    if lower.contains("file") || lower.contains("read") || lower.contains("directory") {
57        perms.push(DeclaredPermission {
58            permission_type: PermissionType::FileRead,
59            target: None,
60            description: Some("Inferred from description".into()),
61        });
62    }
63    if lower.contains("write") || lower.contains("save") || lower.contains("create file") {
64        perms.push(DeclaredPermission {
65            permission_type: PermissionType::FileWrite,
66            target: None,
67            description: Some("Inferred from description".into()),
68        });
69    }
70    if lower.contains("http")
71        || lower.contains("url")
72        || lower.contains("fetch")
73        || lower.contains("request")
74        || lower.contains("network")
75    {
76        perms.push(DeclaredPermission {
77            permission_type: PermissionType::NetworkAccess,
78            target: None,
79            description: Some("Inferred from description".into()),
80        });
81    }
82    if lower.contains("exec")
83        || lower.contains("run")
84        || lower.contains("command")
85        || lower.contains("shell")
86        || lower.contains("subprocess")
87    {
88        perms.push(DeclaredPermission {
89            permission_type: PermissionType::ProcessExec,
90            target: None,
91            description: Some("Inferred from description".into()),
92        });
93    }
94
95    perms
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn parses_mcp_tools_list() {
104        let json = serde_json::json!({
105            "tools": [
106                {
107                    "name": "calculator_add",
108                    "description": "Add two numbers",
109                    "inputSchema": {
110                        "type": "object",
111                        "properties": {
112                            "a": {"type": "number"},
113                            "b": {"type": "number"}
114                        }
115                    }
116                },
117                {
118                    "name": "fetch_url",
119                    "description": "Fetch content from a URL",
120                    "inputSchema": {
121                        "type": "object",
122                        "properties": {
123                            "url": {"type": "string"}
124                        }
125                    }
126                }
127            ]
128        });
129        let tools = parse_tools_from_json(&json);
130        assert_eq!(tools.len(), 2);
131        assert_eq!(tools[0].name, "calculator_add");
132        assert!(tools[0].declared_permissions.is_empty());
133        assert_eq!(tools[1].name, "fetch_url");
134        assert!(!tools[1].declared_permissions.is_empty());
135    }
136}