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>)> {
118 calls
119 .iter()
120 .map(|call| {
121 let output_schema = tools
122 .iter()
123 .find(|t| t.name == call.tool_name)
124 .and_then(|t| t.output_schema.clone());
125 (call.tool_name.clone(), output_schema)
126 })
127 .collect()
128 }
129
130 pub fn find_templates_in_value(value: &Value) -> Vec<String> {
131 let mut templates = Vec::new();
132 Self::collect_templates(value, &mut templates);
133 templates
134 }
135
136 fn collect_templates(value: &Value, templates: &mut Vec<String>) {
137 match value {
138 Value::String(s) if s.starts_with('$') && s.contains(".output.") => {
139 templates.push(s.clone());
140 },
141 Value::Array(arr) => {
142 for v in arr {
143 Self::collect_templates(v, templates);
144 }
145 },
146 Value::Object(obj) => {
147 for v in obj.values() {
148 Self::collect_templates(v, templates);
149 }
150 },
151 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {},
152 }
153 }
154
155 pub fn validate_plan(
156 calls: &[PlannedToolCall],
157 tool_output_schemas: &[(String, Option<Value>)],
158 ) -> Result<(), Vec<PlanValidationError>> {
159 let mut errors = Vec::new();
160
161 for (tool_index, call) in calls.iter().enumerate() {
162 for template in Self::find_templates_in_value(&call.arguments) {
163 if let Some(err) =
164 Self::validate_template(tool_index, call, &template, tool_output_schemas)
165 {
166 errors.push(err);
167 }
168 }
169 }
170
171 if errors.is_empty() {
172 Ok(())
173 } else {
174 Err(errors)
175 }
176 }
177
178 fn validate_template(
179 tool_index: usize,
180 call: &PlannedToolCall,
181 template: &str,
182 tool_output_schemas: &[(String, Option<Value>)],
183 ) -> Option<PlanValidationError> {
184 let make_error = |error: ValidationErrorKind| PlanValidationError {
185 tool_index,
186 argument: Self::find_argument_for_template(&call.arguments, template),
187 template: template.to_owned(),
188 error,
189 };
190
191 let Some(template_ref) = TemplateRef::parse(template) else {
192 return Some(make_error(ValidationErrorKind::InvalidTemplateSyntax));
193 };
194
195 if template_ref.tool_index == tool_index {
196 return Some(make_error(ValidationErrorKind::SelfReference));
197 }
198 if template_ref.tool_index > tool_index {
199 return Some(make_error(ValidationErrorKind::ForwardReference {
200 referenced_index: template_ref.tool_index,
201 }));
202 }
203 if template_ref.tool_index >= tool_output_schemas.len() {
204 return Some(make_error(ValidationErrorKind::IndexOutOfBounds {
205 referenced_index: template_ref.tool_index,
206 max_valid_index: tool_output_schemas.len().saturating_sub(1),
207 }));
208 }
209
210 let (ref_tool_name, ref_output_schema) = &tool_output_schemas[template_ref.tool_index];
211
212 ref_output_schema.as_ref().map_or_else(
213 || {
214 Some(make_error(ValidationErrorKind::NoOutputSchema {
215 tool_name: ref_tool_name.clone(),
216 }))
217 },
218 |schema| {
219 Self::validate_field_access(&template_ref, schema, ref_tool_name).map(make_error)
220 },
221 )
222 }
223
224 fn validate_field_access(
225 template_ref: &TemplateRef,
226 schema: &Value,
227 tool_name: &str,
228 ) -> Option<ValidationErrorKind> {
229 let first_field = template_ref.field_path.first()?;
230 let available_fields = Self::get_schema_fields(schema);
231
232 if available_fields.contains(first_field) {
233 None
234 } else {
235 Some(ValidationErrorKind::FieldNotFound {
236 tool_name: tool_name.to_owned(),
237 field: first_field.clone(),
238 available_fields,
239 })
240 }
241 }
242
243 fn find_argument_for_template(value: &Value, template: &str) -> String {
244 if let Value::Object(obj) = value {
245 for (key, val) in obj {
246 if let Value::String(s) = val
247 && s == template
248 {
249 return key.clone();
250 }
251 let nested = Self::find_argument_for_template(val, template);
252 if !nested.is_empty() {
253 return format!("{key}.{nested}");
254 }
255 }
256 }
257 String::new()
258 }
259
260 fn get_schema_fields(schema: &Value) -> Vec<String> {
261 schema
262 .get("properties")
263 .and_then(|p| p.as_object())
264 .map_or_else(Vec::new, |obj| obj.keys().cloned().collect())
265 }
266}