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(
160 calls: &[PlannedToolCall],
161 tool_output_schemas: &[(String, Option<Value>)],
162 ) -> Result<(), Vec<PlanValidationError>> {
163 let mut errors = Vec::new();
164
165 for (tool_index, call) in calls.iter().enumerate() {
166 for template in Self::find_templates_in_value(&call.arguments) {
167 if let Some(err) =
168 Self::validate_template(tool_index, call, &template, tool_output_schemas)
169 {
170 errors.push(err);
171 }
172 }
173 }
174
175 if errors.is_empty() {
176 Ok(())
177 } else {
178 Err(errors)
179 }
180 }
181
182 fn validate_template(
184 tool_index: usize,
185 call: &PlannedToolCall,
186 template: &str,
187 tool_output_schemas: &[(String, Option<Value>)],
188 ) -> Option<PlanValidationError> {
189 let make_error = |error: ValidationErrorKind| PlanValidationError {
190 tool_index,
191 argument: Self::find_argument_for_template(&call.arguments, template),
192 template: template.to_owned(),
193 error,
194 };
195
196 let Some(template_ref) = TemplateRef::parse(template) else {
197 return Some(make_error(ValidationErrorKind::InvalidTemplateSyntax));
198 };
199
200 if template_ref.tool_index == tool_index {
201 return Some(make_error(ValidationErrorKind::SelfReference));
202 }
203 if template_ref.tool_index > tool_index {
204 return Some(make_error(ValidationErrorKind::ForwardReference {
205 referenced_index: template_ref.tool_index,
206 }));
207 }
208 if template_ref.tool_index >= tool_output_schemas.len() {
209 return Some(make_error(ValidationErrorKind::IndexOutOfBounds {
210 referenced_index: template_ref.tool_index,
211 max_valid_index: tool_output_schemas.len().saturating_sub(1),
212 }));
213 }
214
215 let (ref_tool_name, ref_output_schema) = &tool_output_schemas[template_ref.tool_index];
216
217 ref_output_schema.as_ref().map_or_else(
218 || {
219 Some(make_error(ValidationErrorKind::NoOutputSchema {
220 tool_name: ref_tool_name.clone(),
221 }))
222 },
223 |schema| {
224 Self::validate_field_access(&template_ref, schema, ref_tool_name).map(make_error)
225 },
226 )
227 }
228
229 fn validate_field_access(
230 template_ref: &TemplateRef,
231 schema: &Value,
233 tool_name: &str,
234 ) -> Option<ValidationErrorKind> {
235 let first_field = template_ref.field_path.first()?;
236 let available_fields = Self::get_schema_fields(schema);
237
238 if available_fields.contains(first_field) {
239 None
240 } else {
241 Some(ValidationErrorKind::FieldNotFound {
242 tool_name: tool_name.to_owned(),
243 field: first_field.clone(),
244 available_fields,
245 })
246 }
247 }
248
249 fn find_argument_for_template(value: &Value, template: &str) -> String {
251 if let Value::Object(obj) = value {
252 for (key, val) in obj {
253 if let Value::String(s) = val
254 && s == template
255 {
256 return key.clone();
257 }
258 let nested = Self::find_argument_for_template(val, template);
259 if !nested.is_empty() {
260 return format!("{key}.{nested}");
261 }
262 }
263 }
264 String::new()
265 }
266
267 fn get_schema_fields(schema: &Value) -> Vec<String> {
269 schema
270 .get("properties")
271 .and_then(|p| p.as_object())
272 .map_or_else(Vec::new, |obj| obj.keys().cloned().collect())
273 }
274}