Skip to content

Commit edc4373

Browse files
m13vclaude
andcommitted
feat: Add nested schema support for workflow variables
- Add value_schema, properties, item_schema fields to VariableDefinition - Make label field Optional<String> to support nested schemas - Add recursive validate_variable_value() function for nested validation - Validate arrays with item_schema at any depth - Validate objects with properties (known structure) - Validate objects with value_schema (uniform value types) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3e99cb0 commit edc4373

File tree

2 files changed

+126
-65
lines changed

2 files changed

+126
-65
lines changed

terminator-mcp-agent/src/server_sequence.rs

Lines changed: 117 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::helpers::substitute_variables;
22
use crate::output_parser;
33
use crate::server::extract_content_json;
44
use crate::telemetry::{StepSpan, WorkflowSpan};
5-
use crate::utils::{DesktopWrapper, ExecuteSequenceArgs, SequenceItem, ToolCall, ToolGroup};
5+
use crate::utils::{DesktopWrapper, ExecuteSequenceArgs, SequenceItem, ToolCall, ToolGroup, VariableDefinition};
66
use rmcp::model::{CallToolResult, Content};
77
use rmcp::service::{Peer, RequestContext, RoleServer};
88
use rmcp::ErrorData as McpError;
@@ -11,6 +11,120 @@ use std::path::{Path, PathBuf};
1111
use std::time::Duration;
1212
use tracing::{debug, info, warn};
1313

14+
/// Helper function to recursively validate a value against a variable definition
15+
fn validate_variable_value(
16+
variable_name: &str,
17+
value: &Value,
18+
def: &VariableDefinition,
19+
) -> Result<(), McpError> {
20+
match def.r#type {
21+
crate::utils::VariableType::String => {
22+
if !value.is_string() {
23+
return Err(McpError::invalid_params(
24+
format!("Variable '{variable_name}' must be a string."),
25+
Some(json!({"value": value})),
26+
));
27+
}
28+
}
29+
crate::utils::VariableType::Number => {
30+
if !value.is_number() {
31+
return Err(McpError::invalid_params(
32+
format!("Variable '{variable_name}' must be a number."),
33+
Some(json!({"value": value})),
34+
));
35+
}
36+
}
37+
crate::utils::VariableType::Boolean => {
38+
if !value.is_boolean() {
39+
return Err(McpError::invalid_params(
40+
format!("Variable '{variable_name}' must be a boolean."),
41+
Some(json!({"value": value})),
42+
));
43+
}
44+
}
45+
crate::utils::VariableType::Enum => {
46+
let val_str = value.as_str().ok_or_else(|| {
47+
McpError::invalid_params(
48+
format!("Enum variable '{variable_name}' must be a string."),
49+
Some(json!({"value": value})),
50+
)
51+
})?;
52+
if let Some(options) = &def.options {
53+
if !options.contains(&val_str.to_string()) {
54+
return Err(McpError::invalid_params(
55+
format!("Variable '{variable_name}' has an invalid value."),
56+
Some(json!({
57+
"value": val_str,
58+
"allowed_options": options
59+
})),
60+
));
61+
}
62+
}
63+
}
64+
crate::utils::VariableType::Array => {
65+
if !value.is_array() {
66+
return Err(McpError::invalid_params(
67+
format!("Variable '{variable_name}' must be an array."),
68+
Some(json!({"value": value})),
69+
));
70+
}
71+
// Validate each array item against item_schema if provided
72+
if let Some(item_schema) = &def.item_schema {
73+
if let Some(array) = value.as_array() {
74+
for (index, item) in array.iter().enumerate() {
75+
validate_variable_value(
76+
&format!("{variable_name}[{index}]"),
77+
item,
78+
item_schema,
79+
)?;
80+
}
81+
}
82+
}
83+
}
84+
crate::utils::VariableType::Object => {
85+
if !value.is_object() {
86+
return Err(McpError::invalid_params(
87+
format!("Variable '{variable_name}' must be an object."),
88+
Some(json!({"value": value})),
89+
));
90+
}
91+
92+
let obj = value.as_object().unwrap();
93+
94+
// Validate against properties if defined (for objects with known structure)
95+
if let Some(properties) = &def.properties {
96+
for (prop_key, prop_def) in properties {
97+
if let Some(prop_value) = obj.get(prop_key) {
98+
validate_variable_value(
99+
&format!("{variable_name}.{prop_key}"),
100+
prop_value,
101+
prop_def,
102+
)?;
103+
} else if prop_def.required.unwrap_or(true) {
104+
return Err(McpError::invalid_params(
105+
format!("Required property '{variable_name}.{prop_key}' is missing."),
106+
None,
107+
));
108+
}
109+
}
110+
}
111+
112+
// Validate against value_schema if defined (for flat key-value objects)
113+
if let Some(value_schema) = &def.value_schema {
114+
for (key, val) in obj {
115+
validate_variable_value(
116+
&format!("{variable_name}.{key}"),
117+
val,
118+
value_schema,
119+
)?;
120+
}
121+
}
122+
}
123+
}
124+
125+
Ok(())
126+
}
127+
14128
impl DesktopWrapper {
15129
// Get the state file path for a workflow
16130
async fn get_state_file_path(workflow_url: &str) -> Option<PathBuf> {
@@ -413,68 +527,8 @@ impl DesktopWrapper {
413527

414528
match value {
415529
Some(val) => {
416-
// Validate the value against the definition
417-
match def.r#type {
418-
crate::utils::VariableType::String => {
419-
if !val.is_string() {
420-
return Err(McpError::invalid_params(
421-
format!("Variable '{key}' must be a string."),
422-
Some(json!({"value": val})),
423-
));
424-
}
425-
}
426-
crate::utils::VariableType::Number => {
427-
if !val.is_number() {
428-
return Err(McpError::invalid_params(
429-
format!("Variable '{key}' must be a number."),
430-
Some(json!({"value": val})),
431-
));
432-
}
433-
}
434-
crate::utils::VariableType::Boolean => {
435-
if !val.is_boolean() {
436-
return Err(McpError::invalid_params(
437-
format!("Variable '{key}' must be a boolean."),
438-
Some(json!({"value": val})),
439-
));
440-
}
441-
}
442-
crate::utils::VariableType::Enum => {
443-
let val_str = val.as_str().ok_or_else(|| {
444-
McpError::invalid_params(
445-
format!("Enum variable '{key}' must be a string."),
446-
Some(json!({"value": val})),
447-
)
448-
})?;
449-
if let Some(options) = &def.options {
450-
if !options.contains(&val_str.to_string()) {
451-
return Err(McpError::invalid_params(
452-
format!("Variable '{key}' has an invalid value."),
453-
Some(json!({
454-
"value": val_str,
455-
"allowed_options": options
456-
})),
457-
));
458-
}
459-
}
460-
}
461-
crate::utils::VariableType::Array => {
462-
if !val.is_array() {
463-
return Err(McpError::invalid_params(
464-
format!("Variable '{key}' must be an array."),
465-
Some(json!({"value": val})),
466-
));
467-
}
468-
}
469-
crate::utils::VariableType::Object => {
470-
if !val.is_object() {
471-
return Err(McpError::invalid_params(
472-
format!("Variable '{key}' must be an object."),
473-
Some(json!({"value": val})),
474-
));
475-
}
476-
}
477-
}
530+
// Use the recursive validation helper function
531+
validate_variable_value(key, val, def)?;
478532
}
479533
None => {
480534
if def.required.unwrap_or(true) {

terminator-mcp-agent/src/utils.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,8 +880,9 @@ pub enum VariableType {
880880
pub struct VariableDefinition {
881881
#[schemars(description = "The data type of the variable.")]
882882
pub r#type: VariableType,
883-
#[schemars(description = "A user-friendly label for the variable, for UI generation.")]
884-
pub label: String,
883+
#[schemars(description = "A user-friendly label for the variable, for UI generation. Optional for nested schemas (value_schema, properties, item_schema).")]
884+
#[serde(default)]
885+
pub label: Option<String>,
885886
#[schemars(description = "A detailed description of what the variable is for.")]
886887
pub description: Option<String>,
887888
#[schemars(description = "The default value for the variable if not provided in the inputs.")]
@@ -892,6 +893,12 @@ pub struct VariableDefinition {
892893
pub options: Option<Vec<String>>,
893894
#[schemars(description = "Whether this variable is required. Defaults to true.")]
894895
pub required: Option<bool>,
896+
#[schemars(description = "For object types with flat key-value structure, defines the schema for all values (e.g., all values must be enum with specific options).")]
897+
pub value_schema: Option<Box<VariableDefinition>>,
898+
#[schemars(description = "For object types with known properties, defines the schema for each named property.")]
899+
pub properties: Option<HashMap<String, Box<VariableDefinition>>>,
900+
#[schemars(description = "For array types, defines the schema for array items.")]
901+
pub item_schema: Option<Box<VariableDefinition>>,
895902
}
896903

897904
// Keep the old structures for internal use

0 commit comments

Comments
 (0)