fix(flows): make updateFlow body path optional so AI can update flows (#10176)

* fix(mcp): default a body field to its same-named path param so updateFlow works

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: trim mcp path-param fallback helper comment

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(mcp): keep path params un-mangled so update tools take plain `path`

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(flows): default update_flow body path from URL via EditFlow

Harmonizes updateFlow with the EditVariable/EditResource/EditApp convention: the
flow to update is identified by the URL, so the body path is optional and only
needed to rename. Fixes the 422 at the API layer for every client (MCP, the
in-app AI chat, raw HTTP), not just the MCP tool schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(mcp): drop redundant body-path fallback now that the server defaults it

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: fix stale generator comment after removing mcp body-path fallback

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(flows): mark updateFlow body path optional in the openapi contract

Adds an `EditFlow` schema (path optional) for the update route so the public
contract matches the server; createFlow keeps `OpenFlowWPath` (path required).
Also trims two test comments to record constraints rather than history.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-07-17 19:22:31 +02:00
committed by GitHub
parent 2ff5a918d5
commit 7d2c5ceb0f
9 changed files with 213 additions and 161 deletions
@@ -165,13 +165,14 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
conflicts = (path_keys & query_keys) | (path_keys & body_keys) | (query_keys & body_keys)
path_field_renames = {}
query_field_renames = {}
body_field_renames = {}
for field in conflicts:
# The path parameter keeps the plain name: it identifies the item, is what a
# caller reaches for first, and matches the non-colliding endpoints (`getFlowByPath`
# takes `path`). Only the other locations carry a suffix.
schemas_and_renames = [
(path_params_schema, path_keys, '__path', path_field_renames),
(query_params_schema, query_keys, '__query', query_field_renames),
(body_schema, body_keys, '__body', body_field_renames),
]
@@ -197,11 +198,26 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
# Store the reverse mapping: renamed -> original
renames_map[new_name] = field
# A body field colliding with a same-named path parameter (`path` on the update
# endpoints) holds the new value and differs only when moving the item, so it must
# stay optional; the server defaults it from the URL path when the caller omits it.
if field in path_keys and field in body_keys and body_schema:
body_name = field + '__body'
if 'required' in body_schema:
body_schema['required'] = [r for r in body_schema['required'] if r != body_name]
prop = body_schema['properties'].get(body_name)
if isinstance(prop, dict):
existing_desc = prop.get('description', '').rstrip('. ')
prop['description'] = (
f"{existing_desc}. Defaults to `{field}` when omitted; "
f"set it only to change the {field}."
).lstrip('. ')
# Return None for empty schemas
path_params_schema = path_params_schema if path_params_schema and path_params_schema.get('properties') else None
query_params_schema = query_params_schema if query_params_schema and query_params_schema.get('properties') else None
return (path_params_schema, query_params_schema, body_schema, path_field_renames, query_field_renames, body_field_renames)
return (path_params_schema, query_params_schema, body_schema, query_field_renames, body_field_renames)
# Cache for loaded external files
_external_file_cache: Dict[str, Dict[str, Any]] = {}
@@ -453,7 +469,7 @@ export const mcpEndpointTools: EndpointTool[] = [];
method = tool['method'].upper()
# Generate separate schemas
path_params_schema, query_params_schema, body_schema, path_field_renames, query_field_renames, body_field_renames = extract_separate_schemas(
path_params_schema, query_params_schema, body_schema, query_field_renames, body_field_renames = extract_separate_schemas(
tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path,
tool.get('include_fields'), tool.get('opaque_fields'), tool.get('include_query_params')
)
@@ -462,7 +478,6 @@ export const mcpEndpointTools: EndpointTool[] = [];
path_params_ts = json.dumps(path_params_schema, indent=8) if path_params_schema else "undefined"
query_params_ts = json.dumps(query_params_schema, indent=8) if query_params_schema else "undefined"
body_schema_ts = json.dumps(body_schema, indent=8) if body_schema else "undefined"
path_field_renames_ts = json.dumps(path_field_renames, indent=8) if path_field_renames else "undefined"
query_field_renames_ts = json.dumps(query_field_renames, indent=8) if query_field_renames else "undefined"
body_field_renames_ts = json.dumps(body_field_renames, indent=8) if body_field_renames else "undefined"
@@ -476,7 +491,6 @@ export const mcpEndpointTools: EndpointTool[] = [];
pathParamsSchema: {path_params_ts},
queryParamsSchema: {query_params_ts},
bodySchema: {body_schema_ts},
pathFieldRenames: {path_field_renames_ts},
queryFieldRenames: {query_field_renames_ts},
bodyFieldRenames: {body_field_renames_ts}
}}"""
@@ -497,7 +511,6 @@ export interface EndpointTool {{
pathParamsSchema?: object;
queryParamsSchema?: object;
bodySchema?: object;
pathFieldRenames?: Record<string, string>;
queryFieldRenames?: Record<string, string>;
bodyFieldRenames?: Record<string, string>;
}}
@@ -529,7 +542,7 @@ pub fn all_tools() -> Vec<EndpointTool> {{
method = tool['method'].upper()
# Generate separate schemas
path_params_schema, query_params_schema, body_schema, path_field_renames, query_field_renames, body_field_renames = extract_separate_schemas(
path_params_schema, query_params_schema, body_schema, query_field_renames, body_field_renames = extract_separate_schemas(
tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path,
tool.get('include_fields'), tool.get('opaque_fields'), tool.get('include_query_params')
)
@@ -537,7 +550,6 @@ pub fn all_tools() -> Vec<EndpointTool> {{
path_params_rust = schema_to_rust_value(path_params_schema)
query_params_rust = schema_to_rust_value(query_params_schema)
body_schema_rust = schema_to_rust_value(body_schema)
path_field_renames_rust = schema_to_rust_value(path_field_renames if path_field_renames else None)
query_field_renames_rust = schema_to_rust_value(query_field_renames if query_field_renames else None)
body_field_renames_rust = schema_to_rust_value(body_field_renames if body_field_renames else None)
@@ -551,7 +563,6 @@ pub fn all_tools() -> Vec<EndpointTool> {{
path_params_schema: {path_params_rust},
query_params_schema: {query_params_rust},
body_schema: {body_schema_rust},
path_field_renames: {path_field_renames_rust},
query_field_renames: {query_field_renames_rust},
body_field_renames: {body_field_renames_rust},
}}"""
+4 -2
View File
@@ -47,7 +47,7 @@ use windmill_common::HUB_BASE_URL;
use windmill_common::{
db::UserDB,
error::{self, to_anyhow, Error, JsonResult, Result},
flows::{Flow, FlowWithStarred, ListFlowQuery, ListableFlow, NewFlow},
flows::{EditFlow, Flow, FlowWithStarred, ListFlowQuery, ListableFlow, NewFlow},
jobs::JobPayload,
schedule::Schedule,
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, RunnableKind, StripPath},
@@ -1007,7 +1007,7 @@ async fn update_flow(
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Json(nf): Json<NewFlow>,
Json(ef): Json<EditFlow>,
) -> Result<String> {
if authed.is_operator {
return Err(Error::NotAuthorized(
@@ -1015,6 +1015,8 @@ async fn update_flow(
));
}
let flow_path = flow_path.to_path();
// The URL identifies the flow being updated; the body path is only needed to rename.
let nf = ef.into_new_flow(flow_path);
check_scopes(&authed, || format!("flows:write:{}", flow_path))?;
if let RuleCheckResult::Blocked(msg) = check_deploy_rules(
+32 -1
View File
@@ -10775,7 +10775,7 @@ paths:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/OpenFlowWPath"
- $ref: "#/components/schemas/EditFlow"
- type: object
properties:
deployment_message:
@@ -29097,6 +29097,37 @@ components:
type: string
required:
- path
# Like OpenFlowWPath but `path` is optional: on update the flow is identified by
# the URL, so the body path is only needed to rename it. Kept as a separate schema
# (rather than making OpenFlowWPath.path optional) so createFlow still requires path.
EditFlow:
allOf:
- $ref: "../../openflow.openapi.yaml#/components/schemas/OpenFlow"
- type: object
properties:
path:
type: string
tag:
type: string
ws_error_handler_muted:
type: boolean
priority:
type: integer
dedicated_worker:
type: boolean
timeout:
type: number
visible_to_runner_only:
type: boolean
on_behalf_of_email:
type: string
preserve_on_behalf_of:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original on_behalf_of_email value instead of overwriting it."
labels:
type: array
items:
type: string
FlowPreview:
type: object
@@ -26,7 +26,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
]
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -54,7 +53,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
]
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -124,7 +122,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
"description"
]
})),
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -147,7 +144,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
})),
query_params_schema: None,
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -160,13 +156,12 @@ pub fn all_tools() -> Vec<EndpointTool> {
path_params_schema: Some(serde_json::json!({
"type": "object",
"properties": {
"path__path": {
"type": "string",
"description": "(path parameter)"
"path": {
"type": "string"
}
},
"required": [
"path__path"
"path"
]
})),
query_params_schema: Some(serde_json::json!({
@@ -205,12 +200,9 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"path__body": {
"type": "string",
"description": "The path to the variable (body parameter)"
"description": "The path to the variable (body parameter). Defaults to `path` when omitted; set it only to change the path."
}
}
})),
path_field_renames: Some(serde_json::json!({
"path__path": "path"
})),
query_field_renames: None,
body_field_renames: Some(serde_json::json!({
@@ -253,7 +245,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -307,7 +298,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -362,7 +352,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
"resource_type"
]
})),
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -385,7 +374,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
})),
query_params_schema: None,
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -398,13 +386,12 @@ pub fn all_tools() -> Vec<EndpointTool> {
path_params_schema: Some(serde_json::json!({
"type": "object",
"properties": {
"path__path": {
"type": "string",
"description": "(path parameter)"
"path": {
"type": "string"
}
},
"required": [
"path__path"
"path"
]
})),
query_params_schema: None,
@@ -433,12 +420,9 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"path__body": {
"type": "string",
"description": "The path to the resource (body parameter)"
"description": "The path to the resource (body parameter). Defaults to `path` when omitted; set it only to change the path."
}
}
})),
path_field_renames: Some(serde_json::json!({
"path__path": "path"
})),
query_field_renames: None,
body_field_renames: Some(serde_json::json!({
@@ -473,7 +457,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -535,7 +518,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -548,7 +530,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
path_params_schema: None,
query_params_schema: None,
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -646,7 +627,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -699,7 +679,6 @@ Creates a new version of an existing script when called with the same path and t
"language"
]
})),
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -722,7 +701,6 @@ Creates a new version of an existing script when called with the same path and t
})),
query_params_schema: None,
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -754,7 +732,6 @@ Creates a new version of an existing script when called with the same path and t
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -789,7 +766,6 @@ Creates a new version of an existing script when called with the same path and t
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -816,7 +792,6 @@ Creates a new version of an existing script when called with the same path and t
"description": "The arguments to pass to the script or flow",
"additionalProperties": true
})),
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -886,7 +861,6 @@ Creates a new version of an existing script when called with the same path and t
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -921,7 +895,6 @@ Creates a new version of an existing script when called with the same path and t
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -967,7 +940,6 @@ Creates a new version of an existing script when called with the same path and t
],
"description": "Top-level flow definition containing metadata, configuration, and the flow structure"
})),
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -980,13 +952,12 @@ Creates a new version of an existing script when called with the same path and t
path_params_schema: Some(serde_json::json!({
"type": "object",
"properties": {
"path__path": {
"type": "string",
"description": "(path parameter)"
"path": {
"type": "string"
}
},
"required": [
"path__path"
"path"
]
})),
query_params_schema: None,
@@ -1015,18 +986,14 @@ Creates a new version of an existing script when called with the same path and t
},
"path__body": {
"type": "string",
"description": "(body parameter)"
"description": "(body parameter). Defaults to `path` when omitted; set it only to change the path."
}
},
"required": [
"summary",
"value",
"path__body"
"value"
],
"description": "Top-level flow definition containing metadata, configuration, and the flow structure"
})),
path_field_renames: Some(serde_json::json!({
"path__path": "path"
})),
query_field_renames: None,
body_field_renames: Some(serde_json::json!({
@@ -1061,7 +1028,6 @@ Creates a new version of an existing script when called with the same path and t
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -1099,7 +1065,6 @@ Creates a new version of an existing script when called with the same path and t
"policy"
]
})),
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -1112,13 +1077,12 @@ Creates a new version of an existing script when called with the same path and t
path_params_schema: Some(serde_json::json!({
"type": "object",
"properties": {
"path__path": {
"type": "string",
"description": "(path parameter)"
"path": {
"type": "string"
}
},
"required": [
"path__path"
"path"
]
})),
query_params_schema: None,
@@ -1139,12 +1103,9 @@ Creates a new version of an existing script when called with the same path and t
},
"path__body": {
"type": "string",
"description": "(body parameter)"
"description": "(body parameter). Defaults to `path` when omitted; set it only to change the path."
}
}
})),
path_field_renames: Some(serde_json::json!({
"path__path": "path"
})),
query_field_renames: None,
body_field_renames: Some(serde_json::json!({
@@ -1174,7 +1135,6 @@ Creates a new version of an existing script when called with the same path and t
"description": "The arguments to pass to the script or flow",
"additionalProperties": true
})),
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -1269,7 +1229,6 @@ Creates a new version of an existing script when called with the same path and t
"language"
]
})),
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -1390,7 +1349,6 @@ Creates a new version of an existing script when called with the same path and t
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -1557,7 +1515,6 @@ Creates a new version of an existing script when called with the same path and t
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -1596,7 +1553,6 @@ Creates a new version of an existing script when called with the same path and t
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -1628,7 +1584,6 @@ Creates a new version of an existing script when called with the same path and t
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -1841,7 +1796,6 @@ You should get the schema of the script or flow before creating the schedule to
"args"
]
})),
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -2047,7 +2001,6 @@ You should get the schema of the script or flow before updating the schedule to
"args"
]
})),
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -2070,7 +2023,6 @@ You should get the schema of the script or flow before updating the schedule to
})),
query_params_schema: None,
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -2102,7 +2054,6 @@ You should get the schema of the script or flow before updating the schedule to
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -2168,7 +2119,6 @@ You should get the schema of the script or flow before updating the schedule to
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
@@ -2198,7 +2148,6 @@ You should get the schema of the script or flow before updating the schedule to
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
}
-1
View File
@@ -295,7 +295,6 @@ impl McpBackend for WindmillBackend {
workspace_id,
args_map,
&endpoint_tool.path_params_schema,
&endpoint_tool.path_field_renames,
)?;
let query_string = build_query_string(
args_map,
+57 -8
View File
@@ -295,7 +295,7 @@ pub async fn get_hub_script_schema(path: &str, db: &DB) -> Result<Option<Schema>
// ============================================================================
/// Look up the original field name from a field_renames map.
/// field_renames maps renamed_key -> original_key (e.g. {"path__path": "path"}).
/// field_renames maps renamed_key -> original_key (e.g. {"path__body": "path"}).
fn get_original_name(renamed_key: &str, field_renames: &Option<Value>) -> String {
field_renames
.as_ref()
@@ -373,20 +373,17 @@ pub fn substitute_path_params(
workspace_id: &str,
args_map: &serde_json::Map<String, Value>,
path_schema: &Option<Value>,
path_field_renames: &Option<Value>,
) -> BackendResult<String> {
let mut path_template = path.replace("{workspace}", workspace_id);
if let Some(schema) = path_schema {
if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
for (param_name, _) in props {
// param_name may be renamed (e.g. "path__path"), get original for URL placeholder
let original_name = get_original_name(param_name, path_field_renames);
let placeholder = format!("{{{}}}", original_name);
let placeholder = format!("{{{}}}", param_name);
match args_map.get(param_name) {
Some(param_value) => {
if let Some(str_val) = param_value.as_str() {
validate_path_param_value(&original_name, str_val)?;
validate_path_param_value(param_name, str_val)?;
path_template = path_template.replace(&placeholder, str_val);
}
}
@@ -675,6 +672,60 @@ mod tests {
);
}
// updateFlow-shaped: `path` is both a path parameter and a body field. The path
// parameter keeps the plain name; only the body side is mangled.
fn update_flow_schemas() -> (Option<Value>, Option<Value>, Option<Value>) {
(
Some(json!({
"type": "object",
"properties": { "path": { "type": "string" } },
"required": ["path"]
})),
Some(json!({
"type": "object",
"properties": {
"summary": { "type": "string" },
"value": { "type": "object" },
"path__body": { "type": "string" }
},
"required": ["summary", "value"]
})),
Some(json!({ "path__body": "path" })),
)
}
#[test]
fn build_request_body_maps_body_path_alias_for_rename() {
// The mangled body field carries the *new* path when renaming; it must reach the
// API under its original name `path`. (An omitted `path__body` is intentionally
// absent from the body; the server defaults it from the URL path parameter.)
let (path_schema, body_schema, body_renames) = update_flow_schemas();
let args: serde_json::Map<String, Value> = json!({
"path": "f/team/my_flow",
"path__body": "f/team/renamed_flow",
"summary": "s",
"value": {}
})
.as_object()
.unwrap()
.clone();
let body = build_request_body(
"POST",
&args,
&body_schema,
&body_renames,
&path_schema,
&None,
)
.expect("body should be built");
assert_eq!(
body.as_object().unwrap().get("path"),
Some(&json!("f/team/renamed_flow")),
"path__body must be sent as `path` so a rename takes effect"
);
}
#[test]
fn build_request_body_get_has_no_body() {
let body_schema = Some(json!({ "type": "object", "additionalProperties": true }));
@@ -745,7 +796,6 @@ mod tests {
"dev",
&args,
&path_schema,
&None,
);
assert!(
result.is_err(),
@@ -767,7 +817,6 @@ mod tests {
"dev",
&args,
&path_schema,
&None,
)
.expect("legitimate path should substitute");
assert_eq!(result, "/w/dev/scripts/get/p/u/alice/my_script");
@@ -21,7 +21,6 @@ pub struct EndpointTool {
pub path_params_schema: Option<serde_json::Value>,
pub query_params_schema: Option<serde_json::Value>,
pub body_schema: Option<serde_json::Value>,
pub path_field_renames: Option<serde_json::Value>,
pub query_field_renames: Option<serde_json::Value>,
pub body_field_renames: Option<serde_json::Value>,
}
@@ -215,7 +214,6 @@ mod tests {
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
}
+65
View File
@@ -145,6 +145,57 @@ impl NewFlow {
}
}
/// Body for updating an existing flow. Mirrors `NewFlow`, but `path` is optional: the
/// flow to update is identified by the URL, so the body only needs `path` to rename it.
/// This matches the `EditVariable` / `EditResource` / `EditApp` convention and lets a
/// caller update in place without restating the path.
#[derive(Debug, Deserialize)]
pub struct EditFlow {
#[serde(default)]
pub path: Option<String>,
pub summary: String,
pub description: Option<String>,
#[serde(deserialize_with = "validate_flow_value")]
pub value: Box<RawValue>,
pub schema: Option<Schema>,
pub tag: Option<String>,
pub dedicated_worker: Option<bool>,
pub timeout: Option<i32>,
pub deployment_message: Option<String>,
pub visible_to_runner_only: Option<bool>,
pub on_behalf_of_email: Option<String>,
pub preserve_on_behalf_of: Option<bool>,
pub ws_error_handler_muted: Option<bool>,
#[serde(default)]
pub labels: Option<Vec<String>>,
#[serde(default)]
pub skip_draft_deletion: Option<bool>,
}
impl EditFlow {
/// Resolve into a `NewFlow`, defaulting the target path to `current_path` (the flow's
/// URL path) when the body omits it. A body `path` that differs renames the flow.
pub fn into_new_flow(self, current_path: &str) -> NewFlow {
NewFlow {
path: self.path.unwrap_or_else(|| current_path.to_string()),
summary: self.summary,
description: self.description,
value: self.value,
schema: self.schema,
tag: self.tag,
dedicated_worker: self.dedicated_worker,
timeout: self.timeout,
deployment_message: self.deployment_message,
visible_to_runner_only: self.visible_to_runner_only,
on_behalf_of_email: self.on_behalf_of_email,
preserve_on_behalf_of: self.preserve_on_behalf_of,
ws_error_handler_muted: self.ws_error_handler_muted,
labels: self.labels,
skip_draft_deletion: self.skip_draft_deletion,
}
}
}
fn validate_retry(retry: &Retry, module_id: &str) -> anyhow::Result<()> {
if retry.exponential.attempts > 0 && retry.exponential.seconds == 0 {
return Err(anyhow::anyhow!(
@@ -1233,6 +1284,20 @@ mod tests {
use super::*;
use serde_json::json;
#[test]
fn edit_flow_defaults_path_from_url_and_renames_when_given() {
// An omitted body path resolves to the URL path; an explicit body path renames.
let ef: EditFlow =
serde_json::from_value(json!({ "summary": "s", "value": { "modules": [] } })).unwrap();
assert_eq!(ef.into_new_flow("f/team/my_flow").path, "f/team/my_flow");
let ef: EditFlow = serde_json::from_value(
json!({ "path": "f/team/renamed", "summary": "s", "value": { "modules": [] } }),
)
.unwrap();
assert_eq!(ef.into_new_flow("f/team/my_flow").path, "f/team/renamed");
}
#[test]
fn flow_value_ignores_notes_and_groups() {
// FlowValue should parse successfully even when notes/groups are present —
+17 -69
View File
@@ -10,7 +10,6 @@ export interface EndpointTool {
pathParamsSchema?: object;
queryParamsSchema?: object;
bodySchema?: object;
pathFieldRenames?: Record<string, string>;
queryFieldRenames?: Record<string, string>;
bodyFieldRenames?: Record<string, string>;
}
@@ -36,7 +35,6 @@ export const mcpEndpointTools: EndpointTool[] = [
]
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -64,7 +62,6 @@ export const mcpEndpointTools: EndpointTool[] = [
]
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -134,7 +131,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"description"
]
},
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -157,7 +153,6 @@ export const mcpEndpointTools: EndpointTool[] = [
},
queryParamsSchema: undefined,
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -170,13 +165,12 @@ export const mcpEndpointTools: EndpointTool[] = [
pathParamsSchema: {
"type": "object",
"properties": {
"path__path": {
"type": "string",
"description": "(path parameter)"
"path": {
"type": "string"
}
},
"required": [
"path__path"
"path"
]
},
queryParamsSchema: {
@@ -215,12 +209,9 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"path__body": {
"type": "string",
"description": "The path to the variable (body parameter)"
"description": "The path to the variable (body parameter). Defaults to `path` when omitted; set it only to change the path."
}
}
},
pathFieldRenames: {
"path__path": "path"
},
queryFieldRenames: undefined,
bodyFieldRenames: {
@@ -263,7 +254,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -317,7 +307,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -372,7 +361,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"resource_type"
]
},
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -395,7 +383,6 @@ export const mcpEndpointTools: EndpointTool[] = [
},
queryParamsSchema: undefined,
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -408,13 +395,12 @@ export const mcpEndpointTools: EndpointTool[] = [
pathParamsSchema: {
"type": "object",
"properties": {
"path__path": {
"type": "string",
"description": "(path parameter)"
"path": {
"type": "string"
}
},
"required": [
"path__path"
"path"
]
},
queryParamsSchema: undefined,
@@ -443,12 +429,9 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"path__body": {
"type": "string",
"description": "The path to the resource (body parameter)"
"description": "The path to the resource (body parameter). Defaults to `path` when omitted; set it only to change the path."
}
}
},
pathFieldRenames: {
"path__path": "path"
},
queryFieldRenames: undefined,
bodyFieldRenames: {
@@ -483,7 +466,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -545,7 +527,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -558,7 +539,6 @@ export const mcpEndpointTools: EndpointTool[] = [
pathParamsSchema: undefined,
queryParamsSchema: undefined,
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -656,7 +636,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -708,7 +687,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"language"
]
},
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -731,7 +709,6 @@ export const mcpEndpointTools: EndpointTool[] = [
},
queryParamsSchema: undefined,
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -763,7 +740,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -798,7 +774,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -825,7 +800,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"description": "The arguments to pass to the script or flow",
"additionalProperties": true
},
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -895,7 +869,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -930,7 +903,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -976,7 +948,6 @@ export const mcpEndpointTools: EndpointTool[] = [
],
"description": "Top-level flow definition containing metadata, configuration, and the flow structure"
},
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -989,13 +960,12 @@ export const mcpEndpointTools: EndpointTool[] = [
pathParamsSchema: {
"type": "object",
"properties": {
"path__path": {
"type": "string",
"description": "(path parameter)"
"path": {
"type": "string"
}
},
"required": [
"path__path"
"path"
]
},
queryParamsSchema: undefined,
@@ -1024,18 +994,14 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"path__body": {
"type": "string",
"description": "(body parameter)"
"description": "(body parameter). Defaults to `path` when omitted; set it only to change the path."
}
},
"required": [
"summary",
"value",
"path__body"
"value"
],
"description": "Top-level flow definition containing metadata, configuration, and the flow structure"
},
pathFieldRenames: {
"path__path": "path"
},
queryFieldRenames: undefined,
bodyFieldRenames: {
@@ -1070,7 +1036,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -1108,7 +1073,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"policy"
]
},
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -1121,13 +1085,12 @@ export const mcpEndpointTools: EndpointTool[] = [
pathParamsSchema: {
"type": "object",
"properties": {
"path__path": {
"type": "string",
"description": "(path parameter)"
"path": {
"type": "string"
}
},
"required": [
"path__path"
"path"
]
},
queryParamsSchema: undefined,
@@ -1148,12 +1111,9 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"path__body": {
"type": "string",
"description": "(body parameter)"
"description": "(body parameter). Defaults to `path` when omitted; set it only to change the path."
}
}
},
pathFieldRenames: {
"path__path": "path"
},
queryFieldRenames: undefined,
bodyFieldRenames: {
@@ -1183,7 +1143,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"description": "The arguments to pass to the script or flow",
"additionalProperties": true
},
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -1278,7 +1237,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"language"
]
},
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -1399,7 +1357,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -1566,7 +1523,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -1605,7 +1561,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -1637,7 +1592,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -1847,7 +1801,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"args"
]
},
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -2050,7 +2003,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"args"
]
},
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -2073,7 +2025,6 @@ export const mcpEndpointTools: EndpointTool[] = [
},
queryParamsSchema: undefined,
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -2105,7 +2056,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -2171,7 +2121,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
@@ -2201,7 +2150,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
}