1use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15use super::execution_plan::{PlannedToolCall, TemplateRef};
16use super::tools::McpTool;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PlanValidationError {
20 pub tool_index: usize,
21 pub argument: String,
22 pub template: String,
23 pub error: ValidationErrorKind,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(tag = "type", rename_all = "snake_case")]
28pub enum ValidationErrorKind {
29 InvalidTemplateSyntax,
30 IndexOutOfBounds {
31 referenced_index: usize,
32 max_valid_index: usize,
33 },
34 SelfReference,
35 ForwardReference {
36 referenced_index: usize,
37 },
38 FieldNotFound {
39 tool_name: String,
40 field: String,
41 available_fields: Vec<String>,
42 },
43 NoOutputSchema {
44 tool_name: String,
45 },
46}
47
48impl std::fmt::Display for PlanValidationError {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match &self.error {
51 ValidationErrorKind::InvalidTemplateSyntax => {
52 write!(
53 f,
54 "Tool {}: Invalid template syntax '{}' for argument '{}'",
55 self.tool_index, self.template, self.argument
56 )
57 },
58 ValidationErrorKind::IndexOutOfBounds {
59 referenced_index,
60 max_valid_index,
61 } => {
62 write!(
63 f,
64 "Tool {}: Template '{}' references tool {} but only tools 0-{} are available",
65 self.tool_index, self.template, referenced_index, max_valid_index
66 )
67 },
68 ValidationErrorKind::SelfReference => {
69 write!(
70 f,
71 "Tool {}: Template '{}' cannot reference itself",
72 self.tool_index, self.template
73 )
74 },
75 ValidationErrorKind::ForwardReference { referenced_index } => {
76 write!(
77 f,
78 "Tool {}: Template '{}' references tool {} which hasn't executed yet",
79 self.tool_index, self.template, referenced_index
80 )
81 },
82 ValidationErrorKind::FieldNotFound {
83 tool_name,
84 field,
85 available_fields,
86 } => {
87 write!(
88 f,
89 "Tool {}: Template '{}' references field '{}' but tool '{}' outputs: [{}]",
90 self.tool_index,
91 self.template,
92 field,
93 tool_name,
94 available_fields.join(", ")
95 )
96 },
97 ValidationErrorKind::NoOutputSchema { tool_name } => {
98 write!(
99 f,
100 "Tool {}: Template '{}' references '{}' which has no output schema",
101 self.tool_index, self.template, tool_name
102 )
103 },
104 }
105 }
106}
107
108impl std::error::Error for PlanValidationError {}
109
110#[derive(Debug, Clone, Copy)]
111pub struct TemplateValidator;
112
113impl TemplateValidator {
114 pub fn get_tool_output_schemas(
115 calls: &[PlannedToolCall],
116 tools: &[McpTool],
117 ) -> Vec<(String, Option<Value>)> {
119 calls
120 .iter()
121 .map(|call| {
122 let output_schema = tools
123 .iter()
124 .find(|t| t.name == call.tool_name)
125 .and_then(|t| t.output_schema.clone());
126 (call.tool_name.clone(), output_schema)
127 })
128 .collect()
129 }
130
131 pub fn find_templates_in_value(value: &Value) -> Vec<String> {
133 let mut templates = Vec::new();
134 Self::collect_templates(value, &mut templates);
135 templates
136 }
137
138 fn collect_templates(value: &Value, templates: &mut Vec<String>) {
140 match value {
141 Value::String(s) if s.starts_with('$') && s.contains(".output.") => {
142 templates.push(s.clone());
143 },
144 Value::Array(arr) => {
145 for v in arr {
146 Self::collect_templates(v, templates);
147 }
148 },
149 Value::Object(obj) => {
150 for v in obj.values() {
151 Self::collect_templates(v, templates);
152 }
153 },
154 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {},
155 }
156 }
157
158 pub fn validate_plan(
159 calls: &[PlannedToolCall],
160 tool_output_schemas: &[(String, Option<Value>)],
161 ) -> Result<(), Vec<PlanValidationError>> {
162 let mut errors = Vec::new();
163
164 for (tool_index, call) in calls.iter().enumerate() {
165 for template in Self::find_templates_in_value(&call.arguments) {
166 if let Some(err) =
167 Self::validate_template(tool_index, call, &template, tool_output_schemas)
168 {
169 errors.push(err);
170 }
171 }
172 }
173
174 if errors.is_empty() {
175 Ok(())
176 } else {
177 Err(errors)
178 }
179 }
180
181 fn validate_template(
182 tool_index: usize,
183 call: &PlannedToolCall,
184 template: &str,
185 tool_output_schemas: &[(String, Option<Value>)],
186 ) -> Option<PlanValidationError> {
187 let make_error = |error: ValidationErrorKind| PlanValidationError {
188 tool_index,
189 argument: Self::find_argument_for_template(&call.arguments, template),
190 template: template.to_owned(),
191 error,
192 };
193
194 let Some(template_ref) = TemplateRef::parse(template) else {
195 return Some(make_error(ValidationErrorKind::InvalidTemplateSyntax));
196 };
197
198 if template_ref.tool_index == tool_index {
199 return Some(make_error(ValidationErrorKind::SelfReference));
200 }
201 if template_ref.tool_index > tool_index {
202 return Some(make_error(ValidationErrorKind::ForwardReference {
203 referenced_index: template_ref.tool_index,
204 }));
205 }
206 if template_ref.tool_index >= tool_output_schemas.len() {
207 return Some(make_error(ValidationErrorKind::IndexOutOfBounds {
208 referenced_index: template_ref.tool_index,
209 max_valid_index: tool_output_schemas.len().saturating_sub(1),
210 }));
211 }
212
213 let (ref_tool_name, ref_output_schema) = &tool_output_schemas[template_ref.tool_index];
214
215 ref_output_schema.as_ref().map_or_else(
216 || {
217 Some(make_error(ValidationErrorKind::NoOutputSchema {
218 tool_name: ref_tool_name.clone(),
219 }))
220 },
221 |schema| {
222 Self::validate_field_access(&template_ref, schema, ref_tool_name).map(make_error)
223 },
224 )
225 }
226
227 fn validate_field_access(
228 template_ref: &TemplateRef,
229 schema: &Value,
231 tool_name: &str,
232 ) -> Option<ValidationErrorKind> {
233 let first_field = template_ref.field_path.first()?;
234 let available_fields = Self::get_schema_fields(schema);
235
236 if available_fields.contains(first_field) {
237 None
238 } else {
239 Some(ValidationErrorKind::FieldNotFound {
240 tool_name: tool_name.to_owned(),
241 field: first_field.clone(),
242 available_fields,
243 })
244 }
245 }
246
247 fn find_argument_for_template(value: &Value, template: &str) -> String {
249 if let Value::Object(obj) = value {
250 for (key, val) in obj {
251 if let Value::String(s) = val
252 && s == template
253 {
254 return key.clone();
255 }
256 let nested = Self::find_argument_for_template(val, template);
257 if !nested.is_empty() {
258 return format!("{key}.{nested}");
259 }
260 }
261 }
262 String::new()
263 }
264
265 fn get_schema_fields(schema: &Value) -> Vec<String> {
267 schema
268 .get("properties")
269 .and_then(|p| p.as_object())
270 .map_or_else(Vec::new, |obj| obj.keys().cloned().collect())
271 }
272}