1use schemars::JsonSchema;
4use serde::Deserialize;
5use serde_json::Value;
6
7use super::{parse_payload, ToolError};
8use crate::config::{PolicyConfig, SupernodeConfig};
9use crate::state::SharedState;
10
11#[derive(Debug, Deserialize, JsonSchema)]
13pub struct NameArgs {
14 pub name: String,
15}
16
17#[derive(Debug, Deserialize, JsonSchema)]
19pub struct ValidatePolicyArgs {
20 #[serde(default, alias = "definition")]
23 pub policy: Option<Value>,
24 #[serde(default)]
26 pub name: Option<String>,
27}
28
29#[derive(Debug, Deserialize, JsonSchema)]
31pub struct ValidateSupernodeArgs {
32 pub definition: Value,
34}
35
36fn json<T: serde::Serialize>(v: &T) -> Result<Value, ToolError> {
37 serde_json::to_value(v).map_err(|e| ToolError::internal(e.to_string()))
38}
39
40pub async fn list_routes(state: &SharedState) -> Result<Value, ToolError> {
41 let gw = state.gateway.read().await;
42 Ok(serde_json::json!({ "routes": json(&gw.routes)? }))
43}
44
45pub async fn get_route(state: &SharedState, a: NameArgs) -> Result<Value, ToolError> {
46 let gw = state.gateway.read().await;
47 let r = gw
48 .routes
49 .iter()
50 .find(|r| r.name == a.name)
51 .ok_or_else(|| ToolError::not_found("route", &a.name))?;
52 json(r)
53}
54
55pub async fn list_policies(state: &SharedState) -> Result<Value, ToolError> {
56 let gw = state.gateway.read().await;
57 Ok(serde_json::json!({ "policies": json(&gw.policies)? }))
58}
59
60pub async fn get_policy(state: &SharedState, a: NameArgs) -> Result<Value, ToolError> {
61 let gw = state.gateway.read().await;
62 let p = gw
63 .policies
64 .iter()
65 .find(|p| p.name == a.name)
66 .ok_or_else(|| ToolError::not_found("policy", &a.name))?;
67 let referenced_by: Vec<&str> = gw
68 .routes
69 .iter()
70 .filter(|r| r.policy == a.name)
71 .map(|r| r.name.as_str())
72 .collect();
73 Ok(serde_json::json!({ "policy": json(p)?, "referenced_by_routes": referenced_by }))
74}
75
76pub async fn list_supernodes(state: &SharedState) -> Result<Value, ToolError> {
77 let gw = state.gateway.read().await;
78 Ok(serde_json::json!({ "supernodes": json(&gw.supernodes)? }))
79}
80
81pub async fn get_supernode(state: &SharedState, a: NameArgs) -> Result<Value, ToolError> {
82 let gw = state.gateway.read().await;
83 let s = gw
84 .supernodes
85 .iter()
86 .find(|s| s.name == a.name)
87 .ok_or_else(|| ToolError::not_found("supernode", &a.name))?;
88 let used_by: Vec<&str> = gw
89 .policies
90 .iter()
91 .filter(|p| {
92 p.nodes.iter().any(|n| {
93 n.node_type == "supernode"
94 && n.config.get("name").and_then(Value::as_str) == Some(a.name.as_str())
95 })
96 })
97 .map(|p| p.name.as_str())
98 .collect();
99 Ok(serde_json::json!({ "supernode": json(s)?, "used_by_policies": used_by }))
100}
101
102pub async fn list_plugin_configs(state: &SharedState) -> Result<Value, ToolError> {
103 let gw = state.gateway.read().await;
104 Ok(serde_json::json!({ "plugin_configs": json(&gw.plugin_configs)? }))
105}
106
107pub async fn get_plugin_config(state: &SharedState, a: NameArgs) -> Result<Value, ToolError> {
108 let gw = state.gateway.read().await;
109 let pc = gw
110 .plugin_configs
111 .iter()
112 .find(|p| p.name == a.name)
113 .ok_or_else(|| ToolError::not_found("plugin config", &a.name))?;
114 json(pc)
115}
116
117pub async fn list_stores(state: &SharedState) -> Result<Value, ToolError> {
118 let gw = state.gateway.read().await;
119 Ok(serde_json::json!({ "stores": json(&gw.stores)? }))
120}
121
122pub async fn list_consumers(state: &SharedState) -> Result<Value, ToolError> {
123 let gw = state.gateway.read().await;
124 let masked: Vec<_> = gw
125 .consumers
126 .iter()
127 .map(crate::consumers::mask_credentials)
128 .collect();
129 Ok(serde_json::json!({ "consumers": json(&masked)?, "note": "credential secrets are masked" }))
130}
131
132pub async fn get_consumer(state: &SharedState, a: NameArgs) -> Result<Value, ToolError> {
133 let gw = state.gateway.read().await;
134 let c = gw
135 .consumers
136 .iter()
137 .find(|c| c.name == a.name)
138 .ok_or_else(|| ToolError::not_found("consumer", &a.name))?;
139 json(&crate::consumers::mask_credentials(c))
140}
141
142pub async fn validate_policy(
152 state: &SharedState,
153 a: ValidatePolicyArgs,
154) -> Result<Value, ToolError> {
155 let mut raw = a.policy.ok_or_else(|| {
159 ToolError::invalid_payload("validate_policy needs `policy` (or `definition`): the policy as a JSON object or YAML string")
160 })?;
161 if let Value::String(yaml) = &raw {
162 raw = serde_yaml::from_str(yaml)
163 .map_err(|e| ToolError::invalid_payload(format!("policy: YAML did not parse: {e}")))?;
164 }
165 if let Some(obj) = raw.as_object_mut() {
166 if !obj.contains_key("name") {
167 let name = a
168 .name
169 .clone()
170 .unwrap_or_else(|| "unsaved-policy".to_string());
171 obj.insert("name".to_string(), Value::String(name));
172 }
173 }
174 let policy: PolicyConfig = parse_payload(raw, "policy")?;
175 let (supernodes, plugin_configs) = {
176 let gw = state.gateway.read().await;
177 (gw.supernodes.clone(), gw.plugin_configs.clone())
178 };
179 let compiled = crate::graph::prepare_policy(policy, &supernodes, &plugin_configs)
180 .and_then(|p| crate::graph::compile_policy(&p, state.resources.clone()));
181 let cache_pairs: Value = match &compiled {
182 Ok(graph) => serde_json::to_value(graph.cache_pair_warnings())
183 .expect("CachePairWarning always serializes"),
184 Err(_) => serde_json::json!([]),
185 };
186 let (errors, buffering): (Vec<String>, Value) = match compiled {
187 Ok(graph) => (
188 Vec::new(),
189 serde_json::to_value(graph.buffering_reasons())
190 .expect("BufferingReason always serializes"),
191 ),
192 Err(e) => (
193 e.split("; ").map(str::to_string).collect(),
194 serde_json::json!([]),
195 ),
196 };
197 Ok(serde_json::json!({
198 "valid": errors.is_empty(),
199 "errors": errors,
200 "buffering": buffering,
201 "cache_pairs": cache_pairs
202 }))
203}
204
205pub async fn validate_supernode(a: ValidateSupernodeArgs) -> Result<Value, ToolError> {
207 let def: SupernodeConfig = parse_payload(a.definition, "definition")?;
208 let errors = crate::graph::validate_supernode(&def)
209 .err()
210 .unwrap_or_default();
211 Ok(serde_json::json!({ "valid": errors.is_empty(), "errors": errors }))
212}
213
214#[cfg(test)]
215mod tests {
216 use crate::mcp::tools::call;
217 use crate::mcp::tools::test_support::{obj, state, ECHO_GATEWAY};
218
219 #[tokio::test]
220 async fn validate_policy_accepts_agent_shapes() {
221 let s = state("{}", ECHO_GATEWAY);
222 let nameless = serde_json::json!({
223 "nodes": [{"id": "l", "type": "listener"}, {"id": "e", "type": "echo", "config": {"body": "hi"}}, {"id": "c", "type": "client"}],
224 "edges": [{"from": "l.out", "to": "e.in"}, {"from": "e.out", "to": "c.in"}]
225 });
226 let v = call(
228 &s,
229 "validate_policy",
230 obj(serde_json::json!({"policy": nameless})),
231 )
232 .await
233 .unwrap();
234 assert_eq!(v["valid"], true, "{v}");
235 let yaml = serde_yaml::to_string(&nameless).unwrap();
237 let v = call(
238 &s,
239 "validate_policy",
240 obj(serde_json::json!({"definition": yaml})),
241 )
242 .await
243 .unwrap();
244 assert_eq!(v["valid"], true, "{v}");
245 let err = call(&s, "validate_policy", obj(serde_json::json!({"nodes": []})))
247 .await
248 .unwrap_err();
249 assert_eq!(err.code, "invalid_input");
250 assert!(
251 err.hint.as_deref().unwrap_or("").contains("definition"),
252 "{err:?}"
253 );
254 }
255
256 #[tokio::test]
257 async fn reads_mirror_config_and_report_not_found() {
258 let s = state("{}", ECHO_GATEWAY);
259 let v = call(&s, "list_routes", obj(serde_json::json!({})))
260 .await
261 .unwrap();
262 assert_eq!(v["routes"][0]["name"], "hello");
263 assert_eq!(v["routes"][0]["match"]["path"], "/hello");
264 let v = call(
265 &s,
266 "get_policy",
267 obj(serde_json::json!({"name": "echo-policy"})),
268 )
269 .await
270 .unwrap();
271 assert_eq!(v["referenced_by_routes"][0], "hello");
272 assert_eq!(v["policy"]["nodes"].as_array().unwrap().len(), 3);
273 let err = call(&s, "get_route", obj(serde_json::json!({"name": "nope"})))
274 .await
275 .unwrap_err();
276 assert_eq!(err.code, "not_found");
277 assert!(err.message.contains("route 'nope'"));
278 }
279
280 #[tokio::test]
281 async fn consumers_are_masked() {
282 let gw = format!(
283 "{ECHO_GATEWAY}\nconsumers:\n - name: alice\n credentials:\n key-auth: {{ key: topsecret }}\n"
284 );
285 let s = state("{}", &gw);
286 let v = call(&s, "list_consumers", obj(serde_json::json!({})))
287 .await
288 .unwrap();
289 assert_eq!(
290 v["consumers"][0]["credentials"]["key-auth"]["key"],
291 "<masked>"
292 );
293 let v = call(
294 &s,
295 "get_consumer",
296 obj(serde_json::json!({"name": "alice"})),
297 )
298 .await
299 .unwrap();
300 assert_eq!(v["credentials"]["key-auth"]["key"], "<masked>");
301 }
302
303 #[tokio::test]
304 async fn validate_policy_reports_unwired_port_and_accepts_yaml() {
305 let s = state("{}", ECHO_GATEWAY);
306 let bad = "name: p\nnodes:\n - {id: l, type: listener}\n - {id: k, type: key-auth, config: {keys: [k1]}}\n - {id: c, type: client}\nedges:\n - {from: l.out, to: k.in}\n - {from: k.out, to: c.in}\n";
307 let v = call(
308 &s,
309 "validate_policy",
310 obj(serde_json::json!({"policy": bad})),
311 )
312 .await
313 .unwrap();
314 assert_eq!(v["valid"], false);
315 let errors = v["errors"].as_array().unwrap();
316 assert!(
317 errors
318 .iter()
319 .any(|e| e.as_str().unwrap().contains("denied")),
320 "{errors:?}"
321 );
322
323 let good = serde_json::json!({"name": "p", "nodes": [
324 {"id": "l", "type": "listener"}, {"id": "e", "type": "echo", "config": {"body": "hi"}}, {"id": "c", "type": "client"}],
325 "edges": [{"from": "l.out", "to": "e.in"}, {"from": "e.out", "to": "c.in"}]});
326 let v = call(
327 &s,
328 "validate_policy",
329 obj(serde_json::json!({"policy": good})),
330 )
331 .await
332 .unwrap();
333 assert_eq!(v["valid"], true);
334 }
335
336 #[tokio::test]
340 async fn validate_policy_reports_forced_buffering() {
341 let s = state("{}", ECHO_GATEWAY);
342 let policy = serde_json::json!({
343 "nodes": [
344 { "id": "listener", "type": "listener", "config": {} },
345 { "id": "up", "type": "upstream",
346 "config": { "targets": [{ "host": "h", "port": 80 }] } },
347 { "id": "rw", "type": "response-rewrite",
348 "config": { "filters": [{ "regex": "a", "replace": "b" }] } },
349 { "id": "client", "type": "client", "config": {} }
350 ],
351 "edges": [
352 { "from": "listener.out", "to": "up.in" },
353 { "from": "up.success", "to": "rw.in" },
354 { "from": "rw.success", "to": "client.in" }
355 ]
356 });
357 let v = call(
358 &s,
359 "validate_policy",
360 obj(serde_json::json!({"policy": policy})),
361 )
362 .await
363 .unwrap();
364 assert_eq!(v["valid"], true, "{v}");
365 assert_eq!(v["buffering"][0]["upstream"], "up");
366 assert_eq!(v["buffering"][0]["blocked_by"], "rw");
367 assert_eq!(v["buffering"][0]["node_type"], "response-rewrite");
368 }
369
370 #[tokio::test]
371 async fn validate_supernode_structural() {
372 let s = state("{}", "{}");
373 let def = "name: sn\nnodes:\n - {id: input, type: input}\n - {id: e, type: echo, config: {body: hi}}\nedges:\n - {from: input.out, to: e.in}\n";
374 let v = call(
375 &s,
376 "validate_supernode",
377 obj(serde_json::json!({"definition": def})),
378 )
379 .await
380 .unwrap();
381 assert_eq!(v["valid"], false, "{v}");
382 assert!(!v["errors"].as_array().unwrap().is_empty());
383 }
384}