Skip to main content

authy/mcp/
tools.rs

1//! MCP tool definitions and dispatch for the authy vault.
2
3use serde_json::Value;
4
5use crate::api::AuthyClient;
6
7/// Return JSON Schema definitions for all MCP tools.
8pub fn tool_definitions() -> Vec<Value> {
9    vec![
10        serde_json::json!({
11            "name": "get_secret",
12            "description": "Retrieve a secret value from the vault",
13            "inputSchema": {
14                "type": "object",
15                "properties": {
16                    "name": { "type": "string", "description": "Secret name" }
17                },
18                "required": ["name"]
19            }
20        }),
21        serde_json::json!({
22            "name": "list_secrets",
23            "description": "List all secret names, optionally filtered by a policy scope",
24            "inputSchema": {
25                "type": "object",
26                "properties": {
27                    "scope": { "type": "string", "description": "Policy scope to filter by (optional)" }
28                }
29            }
30        }),
31        serde_json::json!({
32            "name": "store_secret",
33            "description": "Store a secret in the vault",
34            "inputSchema": {
35                "type": "object",
36                "properties": {
37                    "name": { "type": "string", "description": "Secret name" },
38                    "value": { "type": "string", "description": "Secret value" },
39                    "force": { "type": "boolean", "description": "Overwrite if exists (default: false)" }
40                },
41                "required": ["name", "value"]
42            }
43        }),
44        serde_json::json!({
45            "name": "remove_secret",
46            "description": "Remove a secret from the vault",
47            "inputSchema": {
48                "type": "object",
49                "properties": {
50                    "name": { "type": "string", "description": "Secret name" }
51                },
52                "required": ["name"]
53            }
54        }),
55        serde_json::json!({
56            "name": "test_policy",
57            "description": "Test whether a policy allows access to a secret name",
58            "inputSchema": {
59                "type": "object",
60                "properties": {
61                    "scope": { "type": "string", "description": "Policy/scope name" },
62                    "secret_name": { "type": "string", "description": "Secret name to test" }
63                },
64                "required": ["scope", "secret_name"]
65            }
66        }),
67    ]
68}
69
70/// Dispatch a tool call to the appropriate handler.
71pub fn dispatch(client: &AuthyClient, tool_name: &str, args: &Value) -> Value {
72    match tool_name {
73        "get_secret" => handle_get_secret(client, args),
74        "list_secrets" => handle_list_secrets(client, args),
75        "store_secret" => handle_store_secret(client, args),
76        "remove_secret" => handle_remove_secret(client, args),
77        "test_policy" => handle_test_policy(client, args),
78        _ => error_result(&format!("Unknown tool: {}", tool_name)),
79    }
80}
81
82/// Build an MCP error result with `isError: true`.
83pub fn error_result(msg: &str) -> Value {
84    serde_json::json!({
85        "content": [{ "type": "text", "text": msg }],
86        "isError": true
87    })
88}
89
90/// Build an MCP success result.
91fn text_result(text: &str) -> Value {
92    serde_json::json!({
93        "content": [{ "type": "text", "text": text }]
94    })
95}
96
97fn handle_get_secret(client: &AuthyClient, args: &Value) -> Value {
98    let name = match args.get("name").and_then(|v| v.as_str()) {
99        Some(n) => n,
100        None => return error_result("Missing required parameter: name"),
101    };
102
103    match client.get_or_err(name) {
104        Ok(value) => text_result(&value),
105        Err(e) => error_result(&e.to_string()),
106    }
107}
108
109fn handle_list_secrets(client: &AuthyClient, args: &Value) -> Value {
110    let scope = args.get("scope").and_then(|v| v.as_str());
111
112    match client.list(scope) {
113        Ok(names) => {
114            let json = serde_json::to_string(&names).unwrap_or_else(|_| "[]".to_string());
115            text_result(&json)
116        }
117        Err(e) => error_result(&e.to_string()),
118    }
119}
120
121fn handle_store_secret(client: &AuthyClient, args: &Value) -> Value {
122    let name = match args.get("name").and_then(|v| v.as_str()) {
123        Some(n) => n,
124        None => return error_result("Missing required parameter: name"),
125    };
126    let value = match args.get("value").and_then(|v| v.as_str()) {
127        Some(v) => v,
128        None => return error_result("Missing required parameter: value"),
129    };
130    let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
131
132    match client.store(name, value, force) {
133        Ok(()) => text_result(&format!("Stored secret '{}'", name)),
134        Err(e) => error_result(&e.to_string()),
135    }
136}
137
138fn handle_remove_secret(client: &AuthyClient, args: &Value) -> Value {
139    let name = match args.get("name").and_then(|v| v.as_str()) {
140        Some(n) => n,
141        None => return error_result("Missing required parameter: name"),
142    };
143
144    match client.remove(name) {
145        Ok(true) => text_result(&format!("Removed secret '{}'", name)),
146        Ok(false) => text_result(&format!("Secret '{}' not found", name)),
147        Err(e) => error_result(&e.to_string()),
148    }
149}
150
151fn handle_test_policy(client: &AuthyClient, args: &Value) -> Value {
152    let scope = match args.get("scope").and_then(|v| v.as_str()) {
153        Some(s) => s,
154        None => return error_result("Missing required parameter: scope"),
155    };
156    let secret_name = match args.get("secret_name").and_then(|v| v.as_str()) {
157        Some(n) => n,
158        None => return error_result("Missing required parameter: secret_name"),
159    };
160
161    match client.test_policy(scope, secret_name) {
162        Ok(true) => text_result(&format!(
163            "allowed: scope '{}' can read '{}'",
164            scope, secret_name
165        )),
166        Ok(false) => text_result(&format!(
167            "denied: scope '{}' cannot read '{}'",
168            scope, secret_name
169        )),
170        Err(e) => error_result(&e.to_string()),
171    }
172}