diff --git a/backend/generate_mcp_endpoints_tools/generate_mcp_tools.py b/backend/generate_mcp_endpoints_tools/generate_mcp_tools.py index 154e3e4376..7bcf125b8e 100644 --- a/backend/generate_mcp_endpoints_tools/generate_mcp_tools.py +++ b/backend/generate_mcp_endpoints_tools/generate_mcp_tools.py @@ -29,7 +29,39 @@ def load_openapi_spec(file_path: str) -> Dict[str, Any]: print(f"Error loading OpenAPI spec: {e}", file=sys.stderr) sys.exit(1) -def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any], required_fields: Optional[List[str]] = None, base_path: str = "") -> tuple: +def flatten_allof_schema(schema: Dict[str, Any]) -> Dict[str, Any]: + """Flatten an allOf schema into a single object schema by merging all properties.""" + if 'allOf' not in schema: + return schema + + merged = {"type": "object", "properties": {}, "required": []} + + def collect_from(s: Dict[str, Any]): + if 'allOf' in s: + for item in s['allOf']: + if isinstance(item, dict): + collect_from(item) + if 'properties' in s: + merged['properties'].update(s['properties']) + if 'required' in s and isinstance(s['required'], list): + merged['required'].extend(s['required']) + if 'description' in s and 'description' not in merged: + merged['description'] = s['description'] + + collect_from(schema) + + # Preserve additional top-level keys from the original schema + preserved_keys = {'additionalProperties', 'title', 'nullable', 'default', 'example'} + for key in preserved_keys: + if key in schema and key not in merged: + merged[key] = schema[key] + + if not merged['required']: + del merged['required'] + + return merged + +def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any], required_fields: Optional[List[str]] = None, base_path: str = "", include_fields: Optional[List[str]] = None, opaque_fields: Optional[List[str]] = None, include_query_params: Optional[List[str]] = None) -> tuple: """Extract separate schemas for path parameters, query parameters, and request body.""" path_params_schema = { "type": "object", @@ -72,7 +104,7 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt path_params_schema['properties'][param_name] = param_schema if param_required: path_params_schema['required'].append(param_name) - elif param_in == 'query': + elif param_in == 'query' and (include_query_params is None or param_name in include_query_params): query_params_schema['properties'][param_name] = param_schema if param_required: query_params_schema['required'].append(param_name) @@ -80,7 +112,28 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt # Process request body if present if request_body: body_schema = extract_request_body_schema(request_body, spec, base_path) - + + # Flatten allOf schemas into a single object schema for filtering + if body_schema and (include_fields is not None or opaque_fields): + body_schema = flatten_allof_schema(body_schema) + + # Apply include_fields filter: only keep listed top-level properties + if body_schema and include_fields is not None and 'properties' in body_schema: + body_schema['properties'] = { + k: v for k, v in body_schema['properties'].items() + if k in include_fields + } + if 'required' in body_schema: + body_schema['required'] = [ + r for r in body_schema['required'] if r in include_fields + ] + + # Apply opaque_fields: simplify listed properties to {"type": "object"} + if body_schema and opaque_fields and 'properties' in body_schema: + for field in opaque_fields: + if field in body_schema['properties']: + body_schema['properties'][field] = {"type": "object"} + # If we have required fields specified and a body schema, update the required array if body_schema and required_fields: if 'required' not in body_schema: @@ -95,11 +148,60 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt # Log warning when a required field is missing from schema properties print(f"Warning: Required field '{field}' not found in body schema properties", file=sys.stderr) + # Sanitize empty schemas for JSON Schema draft 2020-12 compliance + path_params_schema = sanitize_empty_schemas(path_params_schema) + query_params_schema = sanitize_empty_schemas(query_params_schema) + body_schema = sanitize_empty_schemas(body_schema) + + # Convert enums to descriptions for client compatibility + path_params_schema = convert_enums_to_descriptions(path_params_schema) + query_params_schema = convert_enums_to_descriptions(query_params_schema) + body_schema = convert_enums_to_descriptions(body_schema) + + # Detect overlapping property names across schemas and rename with suffixes + path_keys = set(path_params_schema['properties'].keys()) if path_params_schema and path_params_schema.get('properties') else set() + query_keys = set(query_params_schema['properties'].keys()) if query_params_schema and query_params_schema.get('properties') else set() + body_keys = set(body_schema['properties'].keys()) if body_schema and body_schema.get('properties') else set() + + 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: + 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), + ] + for schema, keys, suffix, renames_map in schemas_and_renames: + if field in keys and schema and 'properties' in schema: + new_name = field + suffix + # Rename in properties + schema['properties'][new_name] = schema['properties'].pop(field) + # Update description to clarify the renamed field + if 'description' not in schema['properties'][new_name]: + schema['properties'][new_name] = dict(schema['properties'][new_name]) + prop = schema['properties'][new_name] + if isinstance(prop, dict): + existing_desc = prop.get('description', '') + location = suffix.lstrip('_') + if not existing_desc: + prop['description'] = f"({location} parameter)" + else: + prop['description'] = f"{existing_desc} ({location} parameter)" + # Rename in required array + if 'required' in schema and field in schema['required']: + schema['required'] = [new_name if r == field else r for r in schema['required']] + # Store the reverse mapping: renamed -> original + renames_map[new_name] = field + # Return None for empty schemas - path_params_schema = path_params_schema if path_params_schema['properties'] else None - query_params_schema = query_params_schema if query_params_schema['properties'] else None - - return (path_params_schema, query_params_schema, body_schema) + 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) # Cache for loaded external files _external_file_cache: Dict[str, Dict[str, Any]] = {} @@ -160,18 +262,25 @@ def resolve_ref(ref_path: str, spec: Dict[str, Any], base_path: str = "") -> tup return (current if isinstance(current, dict) else None), spec -def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path: str = "") -> Dict[str, Any]: +def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path: str = "", _visited_refs: Optional[set] = None) -> Dict[str, Any]: """Recursively resolve all $ref references in a schema.""" + if _visited_refs is None: + _visited_refs = set() + if not isinstance(schema, dict): return schema # If this is a $ref, resolve it if '$ref' in schema: ref_path = schema['$ref'] + if ref_path in _visited_refs: + # Circular reference detected - return empty object to break the cycle + return {"type": "object"} + _visited_refs = _visited_refs | {ref_path} resolved, resolved_spec = resolve_ref(ref_path, spec, base_path) if resolved: # Recursively resolve any refs in the resolved schema using the appropriate spec - return resolve_schema_refs(resolved, resolved_spec, base_path) + return resolve_schema_refs(resolved, resolved_spec, base_path, _visited_refs) else: print(f"Warning: Could not resolve $ref: {ref_path}") return schema @@ -180,10 +289,10 @@ def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path: resolved_schema = {} for key, value in schema.items(): if isinstance(value, dict): - resolved_schema[key] = resolve_schema_refs(value, spec, base_path) + resolved_schema[key] = resolve_schema_refs(value, spec, base_path, _visited_refs) elif isinstance(value, list): resolved_schema[key] = [ - resolve_schema_refs(item, spec, base_path) if isinstance(item, dict) else item + resolve_schema_refs(item, spec, base_path, _visited_refs) if isinstance(item, dict) else item for item in value ] else: @@ -191,6 +300,56 @@ def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path: return resolved_schema +def convert_enums_to_descriptions(schema: Any) -> Any: + """Recursively convert enum arrays into description text to avoid client compatibility issues.""" + if isinstance(schema, list): + return [convert_enums_to_descriptions(item) for item in schema] + if not isinstance(schema, dict): + return schema + + result = {} + enum_value = None + # First pass: copy all non-enum keys so 'description' is available before enum processing + for key, value in schema.items(): + if key == 'enum': + enum_value = value + else: + result[key] = convert_enums_to_descriptions(value) + + # Second pass: process enum using the already-copied description + if enum_value is not None: + values_str = ', '.join(str(v) for v in enum_value) + existing = result.get('description', '') + enum_desc = f"Possible values: {values_str}" + result['description'] = f"{existing}. {enum_desc}" if existing else enum_desc + + return result + +def sanitize_empty_schemas(schema: Any) -> Any: + """Replace empty {} schemas with valid JSON Schema draft 2020-12 equivalents. + + In OpenAPI, {} means 'any value' but strict JSON Schema validators (e.g. Claude's API) + reject empty objects. This converts them to proper schemas. + """ + if isinstance(schema, list): + return [sanitize_empty_schemas(item) for item in schema] + if not isinstance(schema, dict): + return schema + + result = {} + for key, value in schema.items(): + if key == 'additionalProperties' and isinstance(value, dict) and len(value) == 0: + result[key] = True + elif key == 'properties' and isinstance(value, dict): + # properties is a map of name -> schema; sanitize each property schema + result[key] = { + k: {"type": "object"} if isinstance(v, dict) and len(v) == 0 else sanitize_empty_schemas(v) + for k, v in value.items() + } + else: + result[key] = sanitize_empty_schemas(value) + return result + def extract_request_body_schema(request_body: Dict[str, Any], spec: Dict[str, Any], base_path: str = "") -> Optional[Dict[str, Any]]: """Extract request body schema from OpenAPI requestBody definition and resolve refs.""" if not request_body: @@ -243,6 +402,9 @@ def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]: 'parameters': operation.get('parameters', []), 'requestBody': operation.get('requestBody'), 'required_fields': operation.get('x-mcp-required-fields', []), + 'include_fields': operation.get('x-mcp-tool-include-fields'), + 'opaque_fields': operation.get('x-mcp-tool-opaque-fields'), + 'include_query_params': operation.get('x-mcp-tool-include-query-params'), } tools.append(tool) @@ -278,14 +440,18 @@ export const mcpEndpointTools: EndpointTool[] = []; method = tool['method'].upper() # Generate separate schemas - path_params_schema, query_params_schema, body_schema = extract_separate_schemas( - tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path + path_params_schema, query_params_schema, body_schema, path_field_renames, 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') ) # Convert schemas to TypeScript - use 'as const' for better type inference 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" # Generate tool definition tool_def = f""" {{ @@ -296,7 +462,10 @@ export const mcpEndpointTools: EndpointTool[] = []; method: "{method}", pathParamsSchema: {path_params_ts}, queryParamsSchema: {query_params_ts}, - bodySchema: {body_schema_ts} + bodySchema: {body_schema_ts}, + pathFieldRenames: {path_field_renames_ts}, + queryFieldRenames: {query_field_renames_ts}, + bodyFieldRenames: {body_field_renames_ts} }}""" tool_definitions.append(tool_def) @@ -315,6 +484,9 @@ export interface EndpointTool {{ pathParamsSchema?: object; queryParamsSchema?: object; bodySchema?: object; + pathFieldRenames?: Record; + queryFieldRenames?: Record; + bodyFieldRenames?: Record; }} export const mcpEndpointTools: EndpointTool[] = [ @@ -344,13 +516,17 @@ pub fn all_tools() -> Vec {{ method = tool['method'].upper() # Generate separate schemas - path_params_schema, query_params_schema, body_schema = extract_separate_schemas( - tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path + path_params_schema, query_params_schema, body_schema, path_field_renames, 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') ) 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) # Generate tool definition tool_def = f""" EndpointTool {{ @@ -362,6 +538,9 @@ pub fn all_tools() -> Vec {{ 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}, }}""" tool_definitions.append(tool_def) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 0047421126..367c24c6de 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -5932,6 +5932,17 @@ paths: post: summary: create script operationId: createScript + x-mcp-tool: true + x-mcp-instructions: "To create a script, specify the path (e.g., 'f/my_folder/my_script'), the content (source code), and the language. For TypeScript, use 'bun' unless deno-specific APIs are needed." + x-mcp-tool-include-fields: + - path + - content + - language + - summary + - description + - kind + - tag + - deployment_message tags: - script parameters: @@ -6208,6 +6219,7 @@ paths: post: summary: delete script by hash (erase content but keep hash, require admin) operationId: deleteScriptByHash + x-mcp-tool: true tags: - script parameters: @@ -6225,6 +6237,7 @@ paths: post: summary: delete script at a given path (require admin) operationId: deleteScriptByPath + x-mcp-tool: true tags: - script parameters: @@ -6615,6 +6628,9 @@ paths: post: summary: run script by path operationId: runScriptByPath + x-mcp-tool: true + x-mcp-instructions: "You should first use getScriptByPath to retrieve the script's schema and understand what arguments are expected." + x-mcp-tool-include-query-params: [] tags: - job parameters: @@ -7557,6 +7573,18 @@ paths: post: summary: create flow operationId: createFlow + x-mcp-tool: true + x-mcp-tool-include-fields: + - path + - summary + - description + - value + - schema + - tag + - deployment_message + x-mcp-tool-opaque-fields: + - value + - schema tags: - flow parameters: @@ -7587,6 +7615,18 @@ paths: post: summary: update flow operationId: updateFlow + x-mcp-tool: true + x-mcp-tool-include-fields: + - path + - summary + - description + - value + - schema + - tag + - deployment_message + x-mcp-tool-opaque-fields: + - value + - schema tags: - flow parameters: @@ -7644,6 +7684,7 @@ paths: delete: summary: delete flow by path operationId: deleteFlowByPath + x-mcp-tool: true tags: - flow parameters: @@ -7886,6 +7927,16 @@ paths: post: summary: create app operationId: createApp + x-mcp-tool: true + x-mcp-tool-include-fields: + - path + - value + - summary + - policy + - deployment_message + x-mcp-tool-opaque-fields: + - value + - policy tags: - app parameters: @@ -8233,6 +8284,16 @@ paths: post: summary: update app operationId: updateApp + x-mcp-tool: true + x-mcp-tool-include-fields: + - path + - value + - summary + - policy + - deployment_message + x-mcp-tool-opaque-fields: + - value + - policy tags: - app parameters: @@ -8514,6 +8575,9 @@ paths: post: summary: run flow by path operationId: runFlowByPath + x-mcp-tool: true + x-mcp-instructions: "You should first use getFlowByPath to retrieve the flow's schema and understand what arguments are expected." + x-mcp-tool-include-query-params: [] tags: - job parameters: @@ -17909,7 +17973,7 @@ components: ScriptArgs: type: object description: The arguments to pass to the script or flow - additionalProperties: {} + additionalProperties: true Input: type: object @@ -21916,8 +21980,7 @@ components: created_at: type: string format: date-time - value: - type: object + value: {} policy: $ref: "#/components/schemas/Policy" execution_mode: @@ -22466,8 +22529,7 @@ components: properties: name: type: string - value: - type: object + value: {} required: - name - value diff --git a/backend/windmill-api/src/mcp/auto_generated_endpoints.rs b/backend/windmill-api/src/mcp/auto_generated_endpoints.rs index 91a07e3f7a..9425b6a01f 100644 --- a/backend/windmill-api/src/mcp/auto_generated_endpoints.rs +++ b/backend/windmill-api/src/mcp/auto_generated_endpoints.rs @@ -26,6 +26,9 @@ pub fn all_tools() -> Vec { "query" ] })), + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("createVariable"), @@ -84,6 +87,9 @@ pub fn all_tools() -> Vec { "description" ] })), + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("deleteVariable"), @@ -104,6 +110,9 @@ pub fn all_tools() -> Vec { })), query_params_schema: None, body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("updateVariable"), @@ -114,12 +123,13 @@ pub fn all_tools() -> Vec { path_params_schema: Some(serde_json::json!({ "type": "object", "properties": { - "path": { - "type": "string" + "path__path": { + "type": "string", + "description": "(path parameter)" } }, "required": [ - "path" + "path__path" ] })), query_params_schema: Some(serde_json::json!({ @@ -135,10 +145,6 @@ pub fn all_tools() -> Vec { body_schema: Some(serde_json::json!({ "type": "object", "properties": { - "path": { - "type": "string", - "description": "The path to the variable" - }, "value": { "type": "string", "description": "The new value of the variable" @@ -150,8 +156,19 @@ pub fn all_tools() -> Vec { "description": { "type": "string", "description": "The new description of the variable" + }, + "path__body": { + "type": "string", + "description": "The path to the variable (body parameter)" } } +})), + path_field_renames: Some(serde_json::json!({ + "path__path": "path" +})), + query_field_renames: None, + body_field_renames: Some(serde_json::json!({ + "path__body": "path" })), }, EndpointTool { @@ -186,6 +203,9 @@ pub fn all_tools() -> Vec { "required": [] })), body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("listVariable"), @@ -213,6 +233,9 @@ pub fn all_tools() -> Vec { "required": [] })), body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("createResource"), @@ -238,7 +261,9 @@ pub fn all_tools() -> Vec { "type": "string", "description": "The path to the resource" }, - "value": {}, + "value": { + "type": "object" + }, "description": { "type": "string", "description": "The description of the resource" @@ -254,6 +279,9 @@ pub fn all_tools() -> Vec { "resource_type" ] })), + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("deleteResource"), @@ -274,6 +302,9 @@ pub fn all_tools() -> Vec { })), query_params_schema: None, body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("updateResource"), @@ -284,32 +315,42 @@ pub fn all_tools() -> Vec { path_params_schema: Some(serde_json::json!({ "type": "object", "properties": { - "path": { - "type": "string" + "path__path": { + "type": "string", + "description": "(path parameter)" } }, "required": [ - "path" + "path__path" ] })), query_params_schema: None, body_schema: Some(serde_json::json!({ "type": "object", "properties": { - "path": { - "type": "string", - "description": "The path to the resource" - }, "description": { "type": "string", "description": "The new description of the resource" }, - "value": {}, + "value": { + "type": "object" + }, "resource_type": { "type": "string", "description": "The new resource_type to be associated with the resource" + }, + "path__body": { + "type": "string", + "description": "The path to the resource (body parameter)" } } +})), + path_field_renames: Some(serde_json::json!({ + "path__path": "path" +})), + query_field_renames: None, + body_field_renames: Some(serde_json::json!({ + "path__body": "path" })), }, EndpointTool { @@ -331,6 +372,9 @@ pub fn all_tools() -> Vec { })), query_params_schema: None, body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("listResource"), @@ -366,6 +410,9 @@ pub fn all_tools() -> Vec { "required": [] })), body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("listResourceType"), @@ -376,6 +423,9 @@ pub fn all_tools() -> Vec { path_params_schema: None, query_params_schema: None, body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("listScripts"), @@ -458,11 +508,123 @@ pub fn all_tools() -> Vec { "without_description": { "type": "boolean", "description": "(default false)\nIf true, the description field will be omitted from the response.\n" + }, + "dedicated_worker": { + "type": "boolean", + "description": "(default regardless)\nIf true, show only scripts with dedicated_worker enabled.\nIf false, show only scripts with dedicated_worker disabled.\n" } }, "required": [] })), body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, + }, + EndpointTool { + name: Cow::Borrowed("createScript"), + description: Cow::Borrowed("create script"), + instructions: Cow::Borrowed("To create a script, specify the path (e.g., 'f/my_folder/my_script'), the content (source code), and the language. For TypeScript, use 'bun' unless deno-specific APIs are needed."), + path: Cow::Borrowed("/w/{workspace}/scripts/create"), + method: Cow::Borrowed("POST"), + path_params_schema: None, + query_params_schema: None, + body_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "content": { + "type": "string" + }, + "language": { + "type": "string", + "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative" + }, + "kind": { + "type": "string", + "description": "Possible values: script, failure, trigger, command, approval, preprocessor" + }, + "tag": { + "type": "string" + }, + "deployment_message": { + "type": "string" + } + }, + "required": [ + "path", + "summary", + "description", + "content", + "language" + ] +})), + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, + }, + EndpointTool { + name: Cow::Borrowed("deleteScriptByHash"), + description: Cow::Borrowed("delete script by hash (erase content but keep hash, require admin)"), + instructions: Cow::Borrowed(""), + path: Cow::Borrowed("/w/{workspace}/scripts/delete/h/{hash}"), + method: Cow::Borrowed("POST"), + path_params_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "hash": { + "type": "string" + } + }, + "required": [ + "hash" + ] +})), + query_params_schema: None, + body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, + }, + EndpointTool { + name: Cow::Borrowed("deleteScriptByPath"), + description: Cow::Borrowed("delete script at a given path (require admin)"), + instructions: Cow::Borrowed(""), + path: Cow::Borrowed("/w/{workspace}/scripts/delete/p/{path}"), + method: Cow::Borrowed("POST"), + path_params_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] +})), + query_params_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "keep_captures": { + "type": "boolean", + "description": "keep captures" + } + }, + "required": [] +})), + body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("getScriptByPath"), @@ -491,6 +653,36 @@ pub fn all_tools() -> Vec { "required": [] })), body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, + }, + EndpointTool { + name: Cow::Borrowed("runScriptByPath"), + description: Cow::Borrowed("run script by path"), + instructions: Cow::Borrowed("You should first use getScriptByPath to retrieve the script's schema and understand what arguments are expected."), + path: Cow::Borrowed("/w/{workspace}/jobs/run/p/{path}"), + method: Cow::Borrowed("POST"), + path_params_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] +})), + query_params_schema: None, + body_schema: Some(serde_json::json!({ + "type": "object", + "description": "The arguments to pass to the script or flow", + "additionalProperties": true +})), + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("listFlows"), @@ -545,11 +737,18 @@ pub fn all_tools() -> Vec { "without_description": { "type": "boolean", "description": "(default false)\nIf true, the description field will be omitted from the response.\n" + }, + "dedicated_worker": { + "type": "boolean", + "description": "(default regardless)\nIf true, show only flows with dedicated_worker enabled.\nIf false, show only flows with dedicated_worker disabled.\n" } }, "required": [] })), body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("getFlowByPath"), @@ -578,6 +777,262 @@ pub fn all_tools() -> Vec { "required": [] })), body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, + }, + EndpointTool { + name: Cow::Borrowed("createFlow"), + description: Cow::Borrowed("create flow"), + instructions: Cow::Borrowed(""), + path: Cow::Borrowed("/w/{workspace}/flows/create"), + method: Cow::Borrowed("POST"), + path_params_schema: None, + query_params_schema: None, + body_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "summary": { + "type": "string", + "description": "Short description of what this flow does" + }, + "description": { + "type": "string", + "description": "Detailed documentation for this flow" + }, + "value": { + "type": "object" + }, + "schema": { + "type": "object" + }, + "path": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "deployment_message": { + "type": "string" + } + }, + "required": [ + "summary", + "value", + "path" + ], + "description": "Top-level flow definition containing metadata, configuration, and the flow structure" +})), + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, + }, + EndpointTool { + name: Cow::Borrowed("updateFlow"), + description: Cow::Borrowed("update flow"), + instructions: Cow::Borrowed(""), + path: Cow::Borrowed("/w/{workspace}/flows/update/{path}"), + method: Cow::Borrowed("POST"), + path_params_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "path__path": { + "type": "string", + "description": "(path parameter)" + } + }, + "required": [ + "path__path" + ] +})), + query_params_schema: None, + body_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "summary": { + "type": "string", + "description": "Short description of what this flow does" + }, + "description": { + "type": "string", + "description": "Detailed documentation for this flow" + }, + "value": { + "type": "object" + }, + "schema": { + "type": "object" + }, + "tag": { + "type": "string" + }, + "deployment_message": { + "type": "string" + }, + "path__body": { + "type": "string", + "description": "(body parameter)" + } + }, + "required": [ + "summary", + "value", + "path__body" + ], + "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!({ + "path__body": "path" +})), + }, + EndpointTool { + name: Cow::Borrowed("deleteFlowByPath"), + description: Cow::Borrowed("delete flow by path"), + instructions: Cow::Borrowed(""), + path: Cow::Borrowed("/w/{workspace}/flows/delete/{path}"), + method: Cow::Borrowed("DELETE"), + path_params_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] +})), + query_params_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "keep_captures": { + "type": "boolean", + "description": "keep captures" + } + }, + "required": [] +})), + body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, + }, + EndpointTool { + name: Cow::Borrowed("createApp"), + description: Cow::Borrowed("create app"), + instructions: Cow::Borrowed(""), + path: Cow::Borrowed("/w/{workspace}/apps/create"), + method: Cow::Borrowed("POST"), + path_params_schema: None, + query_params_schema: None, + body_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "value": { + "type": "object" + }, + "summary": { + "type": "string" + }, + "policy": { + "type": "object" + }, + "deployment_message": { + "type": "string" + } + }, + "required": [ + "path", + "value", + "summary", + "policy" + ] +})), + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, + }, + EndpointTool { + name: Cow::Borrowed("updateApp"), + description: Cow::Borrowed("update app"), + instructions: Cow::Borrowed(""), + path: Cow::Borrowed("/w/{workspace}/apps/update/{path}"), + method: Cow::Borrowed("POST"), + path_params_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "path__path": { + "type": "string", + "description": "(path parameter)" + } + }, + "required": [ + "path__path" + ] +})), + query_params_schema: None, + body_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "value": { + "type": "object" + }, + "policy": { + "type": "object" + }, + "deployment_message": { + "type": "string" + }, + "path__body": { + "type": "string", + "description": "(body parameter)" + } + } +})), + path_field_renames: Some(serde_json::json!({ + "path__path": "path" +})), + query_field_renames: None, + body_field_renames: Some(serde_json::json!({ + "path__body": "path" +})), + }, + EndpointTool { + name: Cow::Borrowed("runFlowByPath"), + description: Cow::Borrowed("run flow by path"), + instructions: Cow::Borrowed("You should first use getFlowByPath to retrieve the flow's schema and understand what arguments are expected."), + path: Cow::Borrowed("/w/{workspace}/jobs/run/f/{path}"), + method: Cow::Borrowed("POST"), + path_params_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] +})), + query_params_schema: None, + body_schema: Some(serde_json::json!({ + "type": "object", + "description": "The arguments to pass to the script or flow", + "additionalProperties": true +})), + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("runScriptPreviewAndWaitResult"), @@ -605,45 +1060,18 @@ pub fn all_tools() -> Vec { "args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "language": { "type": "string", - "enum": [ - "python3", - "deno", - "go", - "bash", - "powershell", - "postgresql", - "mysql", - "bigquery", - "snowflake", - "mssql", - "oracledb", - "graphql", - "nativets", - "bun", - "php", - "rust", - "ansible", - "csharp", - "nu", - "java", - "ruby", - "duckdb" - ] + "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative" }, "tag": { "type": "string" }, "kind": { "type": "string", - "enum": [ - "code", - "identity", - "http" - ] + "description": "Possible values: code, identity, http" }, "dedicated_worker": { "type": "boolean" @@ -658,6 +1086,9 @@ pub fn all_tools() -> Vec { "language" ] })), + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("listQueue"), @@ -703,22 +1134,8 @@ pub fn all_tools() -> Vec { "description": "mask to filter by trigger path" }, "trigger_kind": { - "description": "trigger kind (schedule, http, websocket...)", - "type": "string", - "enum": [ - "webhook", - "default_email", - "email", - "schedule", - "http", - "websocket", - "postgres", - "kafka", - "nats", - "mqtt", - "sqs", - "gcp" - ] + "description": "trigger kind (schedule, http, websocket...). Possible values: webhook, default_email, email, schedule, http, websocket, postgres, kafka, nats, mqtt, sqs, gcp", + "type": "string" }, "script_hash": { "type": "string", @@ -790,6 +1207,9 @@ pub fn all_tools() -> Vec { "required": [] })), body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("listJobs"), @@ -911,22 +1331,8 @@ pub fn all_tools() -> Vec { "description": "number of items to return for a given page (default 30, max 100)" }, "trigger_kind": { - "description": "trigger kind (schedule, http, websocket...)", - "type": "string", - "enum": [ - "webhook", - "default_email", - "email", - "schedule", - "http", - "websocket", - "postgres", - "kafka", - "nats", - "mqtt", - "sqs", - "gcp" - ] + "description": "trigger kind (schedule, http, websocket...). Possible values: webhook, default_email, email, schedule, http, websocket, postgres, kafka, nats, mqtt, sqs, gcp", + "type": "string" }, "is_skipped": { "type": "boolean", @@ -956,6 +1362,9 @@ pub fn all_tools() -> Vec { "required": [] })), body_schema: None, + path_field_renames: None, + query_field_renames: None, + body_field_renames: None, }, EndpointTool { name: Cow::Borrowed("createSchedule"), @@ -973,75 +1382,75 @@ You should get the schema of the script or flow before creating the schedule to "properties": { "path": { "type": "string", - "description": "The path where the schedule will be created" + "description": "The unique path identifier for this schedule" }, "schedule": { "type": "string", - "description": "The cron schedule to trigger the script or flow. Should include seconds." + "description": "Cron expression with 6 fields (seconds, minutes, hours, day of month, month, day of week). Example '0 0 12 * * *' for daily at noon" }, "timezone": { "type": "string", - "description": "The timezone to use for the cron schedule" + "description": "IANA timezone for the schedule (e.g., 'UTC', 'Europe/Paris', 'America/New_York')" }, "script_path": { "type": "string", - "description": "The path to the script or flow to trigger" + "description": "Path to the script or flow to execute when triggered" }, "is_flow": { "type": "boolean", - "description": "Whether the schedule is for a flow" + "description": "True if script_path points to a flow, false if it points to a script" }, "args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "enabled": { "type": "boolean", - "description": "Whether the schedule is enabled" + "description": "Whether the schedule is currently active and will trigger jobs" }, "on_failure": { "type": "string", - "description": "The path to the script or flow to trigger on failure" + "description": "Path to a script or flow to run when the scheduled job fails" }, "on_failure_times": { "type": "number", - "description": "The number of times to retry on failure" + "description": "Number of consecutive failures before the on_failure handler is triggered (default 1)" }, "on_failure_exact": { "type": "boolean", - "description": "Whether the schedule should only run on the exact time" + "description": "If true, trigger on_failure handler only on exactly N failures, not on every failure after N" }, "on_failure_extra_args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "on_recovery": { "type": "string", - "description": "The path to the script or flow to trigger on recovery" + "description": "Path to a script or flow to run when the schedule recovers after failures" }, "on_recovery_times": { "type": "number", - "description": "The number of times to retry on recovery" + "description": "Number of consecutive successes before the on_recovery handler is triggered (default 1)" }, "on_recovery_extra_args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "on_success": { "type": "string", - "description": "The path to the script or flow to trigger on success" + "description": "Path to a script or flow to run after each successful execution" }, "on_success_extra_args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "ws_error_handler_muted": { "type": "boolean", - "description": "Whether the WebSocket error handler is muted" + "description": "If true, the workspace-level error handler will not be triggered for this schedule's failures" }, "retry": { "type": "object", @@ -1103,32 +1512,32 @@ You should get the schema of the script or flow before creating the schedule to }, "no_flow_overlap": { "type": "boolean", - "description": "Whether the schedule should not run if a flow is already running" + "description": "If true, skip this schedule's execution if the previous run is still in progress (prevents concurrent runs)" }, "summary": { "type": "string", - "description": "The summary of the schedule" + "description": "Short summary describing the purpose of this schedule" }, "description": { "type": "string", - "description": "The description of the schedule" + "description": "Detailed description of what this schedule does" }, "tag": { "type": "string", - "description": "The tag of the schedule" + "description": "Worker tag to route jobs to specific worker groups" }, "paused_until": { "type": "string", - "description": "The date and time the schedule will be paused until", - "format": "date-time" + "format": "date-time", + "description": "ISO 8601 datetime until which the schedule is paused. Schedule resumes automatically after this time" }, "cron_version": { "type": "string", - "description": "The version of the cron schedule to use (last is v2)" + "description": "Cron parser version. Use 'v2' for extended syntax with additional features" }, "dynamic_skip": { "type": "string", - "description": "Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean." + "description": "Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)" } }, "required": [ @@ -1140,6 +1549,9 @@ 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, }, EndpointTool { name: Cow::Borrowed("updateSchedule"), @@ -1167,59 +1579,59 @@ You should get the schema of the script or flow before updating the schedule to "properties": { "schedule": { "type": "string", - "description": "The cron schedule to trigger the script or flow. Should include seconds." + "description": "Cron expression with 6 fields (seconds, minutes, hours, day of month, month, day of week). Example '0 0 12 * * *' for daily at noon" }, "timezone": { "type": "string", - "description": "The timezone to use for the cron schedule" + "description": "IANA timezone for the schedule (e.g., 'UTC', 'Europe/Paris', 'America/New_York')" }, "args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "on_failure": { "type": "string", - "description": "The path to the script or flow to trigger on failure" + "description": "Path to a script or flow to run when the scheduled job fails" }, "on_failure_times": { "type": "number", - "description": "The number of times to retry on failure" + "description": "Number of consecutive failures before the on_failure handler is triggered (default 1)" }, "on_failure_exact": { "type": "boolean", - "description": "Whether the schedule should only run on the exact time" + "description": "If true, trigger on_failure handler only on exactly N failures, not on every failure after N" }, "on_failure_extra_args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "on_recovery": { "type": "string", - "description": "The path to the script or flow to trigger on recovery" + "description": "Path to a script or flow to run when the schedule recovers after failures" }, "on_recovery_times": { "type": "number", - "description": "The number of times to retry on recovery" + "description": "Number of consecutive successes before the on_recovery handler is triggered (default 1)" }, "on_recovery_extra_args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "on_success": { "type": "string", - "description": "The path to the script or flow to trigger on success" + "description": "Path to a script or flow to run after each successful execution" }, "on_success_extra_args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "ws_error_handler_muted": { "type": "boolean", - "description": "Whether the WebSocket error handler is muted" + "description": "If true, the workspace-level error handler will not be triggered for this schedule's failures" }, "retry": { "type": "object", @@ -1281,32 +1693,32 @@ You should get the schema of the script or flow before updating the schedule to }, "no_flow_overlap": { "type": "boolean", - "description": "Whether the schedule should not run if a flow is already running" + "description": "If true, skip this schedule's execution if the previous run is still in progress (prevents concurrent runs)" }, "summary": { "type": "string", - "description": "The summary of the schedule" + "description": "Short summary describing the purpose of this schedule" }, "description": { "type": "string", - "description": "The description of the schedule" + "description": "Detailed description of what this schedule does" }, "tag": { "type": "string", - "description": "The tag of the schedule" + "description": "Worker tag to route jobs to specific worker groups" }, "paused_until": { "type": "string", - "description": "The date and time the schedule will be paused until", - "format": "date-time" + "format": "date-time", + "description": "ISO 8601 datetime until which the schedule is paused. Schedule resumes automatically after this time" }, "cron_version": { "type": "string", - "description": "The version of the cron schedule to use (last is v2)" + "description": "Cron parser version. Use 'v2' for extended syntax with additional features" }, "dynamic_skip": { "type": "string", - "description": "Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean." + "description": "Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)" } }, "required": [ @@ -1315,6 +1727,9 @@ 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, }, EndpointTool { name: Cow::Borrowed("deleteSchedule"), @@ -1335,6 +1750,9 @@ 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, }, EndpointTool { name: Cow::Borrowed("getSchedule"), @@ -1355,6 +1773,9 @@ 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, }, EndpointTool { name: Cow::Borrowed("listSchedules"), @@ -1394,6 +1815,9 @@ 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, }, EndpointTool { name: Cow::Borrowed("listWorkers"), @@ -1421,6 +1845,9 @@ 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, } ] } diff --git a/backend/windmill-api/src/mcp/core.rs b/backend/windmill-api/src/mcp/core.rs index 90419aed4d..604db2dc3e 100644 --- a/backend/windmill-api/src/mcp/core.rs +++ b/backend/windmill-api/src/mcp/core.rs @@ -323,16 +323,25 @@ 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, &endpoint_tool.query_params_schema); + let query_string = build_query_string( + args_map, + &endpoint_tool.query_params_schema, + &endpoint_tool.query_field_renames, + ); let full_url = format!( "{}/api{}{}", self.base_internal_url, path_template, query_string ); // Prepare request body - let body_json = - build_request_body(&endpoint_tool.method, args_map, &endpoint_tool.body_schema); + let body_json = build_request_body( + &endpoint_tool.method, + args_map, + &endpoint_tool.body_schema, + &endpoint_tool.body_field_renames, + ); // Create and execute request let response = create_http_request( diff --git a/backend/windmill-api/src/mcp/utils.rs b/backend/windmill-api/src/mcp/utils.rs index a2513ef5f7..d168f8fc48 100644 --- a/backend/windmill-api/src/mcp/utils.rs +++ b/backend/windmill-api/src/mcp/utils.rs @@ -242,19 +242,34 @@ pub async fn get_hub_script_schema(path: &str, db: &DB) -> Result // HTTP request utilities for endpoint tools // ============================================================================ +/// Look up the original field name from a field_renames map. +/// field_renames maps renamed_key -> original_key (e.g. {"path__path": "path"}). +fn get_original_name(renamed_key: &str, field_renames: &Option) -> String { + field_renames + .as_ref() + .and_then(|v| v.as_object()) + .and_then(|m| m.get(renamed_key)) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| renamed_key.to_string()) +} + /// Substitute path parameters in the URL template pub fn substitute_path_params( path: &str, workspace_id: &str, args_map: &serde_json::Map, path_schema: &Option, + path_field_renames: &Option, ) -> BackendResult { 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 { - let placeholder = format!("{{{}}}", param_name); + // 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); match args_map.get(param_name) { Some(param_value) => { if let Some(str_val) = param_value.as_str() { @@ -280,6 +295,7 @@ pub fn substitute_path_params( pub fn build_query_string( args_map: &serde_json::Map, query_schema: &Option, + query_field_renames: &Option, ) -> String { let Some(schema) = query_schema else { return String::new(); @@ -295,11 +311,13 @@ pub fn build_query_string( .get(param_name) .filter(|v| !v.is_null()) .map(|value| { + // Use the original name for the query parameter key + let original_name = get_original_name(param_name, query_field_renames); let value_str = value.to_string(); let str_val = value_str.trim_matches('"'); format!( "{}={}", - urlencoding::encode(param_name), + urlencoding::encode(&original_name), urlencoding::encode(str_val) ) }) @@ -318,6 +336,7 @@ pub fn build_request_body( method: &str, args_map: &serde_json::Map, body_schema: &Option, + body_field_renames: &Option, ) -> Option { if method == "GET" { return None; @@ -329,9 +348,11 @@ pub fn build_request_body( let body_map: serde_json::Map = props .keys() .filter_map(|param_name| { - args_map - .get(param_name) - .map(|value| (param_name.clone(), value.clone())) + args_map.get(param_name).map(|value| { + // Use the original name as the key in the request body + let original_name = get_original_name(param_name, body_field_renames); + (original_name, value.clone()) + }) }) .collect(); diff --git a/backend/windmill-mcp/src/server/endpoints.rs b/backend/windmill-mcp/src/server/endpoints.rs index 2a0d258e64..0c0472d69a 100644 --- a/backend/windmill-mcp/src/server/endpoints.rs +++ b/backend/windmill-mcp/src/server/endpoints.rs @@ -21,6 +21,9 @@ pub struct EndpointTool { pub path_params_schema: Option, pub query_params_schema: Option, pub body_schema: Option, + pub path_field_renames: Option, + pub query_field_renames: Option, + pub body_field_renames: Option, } /// Convert a single endpoint tool to MCP tool @@ -100,7 +103,9 @@ fn merge_schema_into( if let Some(required) = schema.get("required").and_then(|r| r.as_array()) { for req in required.iter().filter_map(|r| r.as_str()) { - combined_required.push(req.to_string()); + if !combined_required.contains(&req.to_string()) { + combined_required.push(req.to_string()); + } } } } diff --git a/frontend/src/lib/mcpEndpointTools.ts b/frontend/src/lib/mcpEndpointTools.ts index 4292e8c24d..e080914f51 100644 --- a/frontend/src/lib/mcpEndpointTools.ts +++ b/frontend/src/lib/mcpEndpointTools.ts @@ -10,6 +10,9 @@ export interface EndpointTool { pathParamsSchema?: object; queryParamsSchema?: object; bodySchema?: object; + pathFieldRenames?: Record; + queryFieldRenames?: Record; + bodyFieldRenames?: Record; } export const mcpEndpointTools: EndpointTool[] = [ @@ -32,7 +35,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "required": [ "query" ] -} +}, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "createVariable", @@ -90,7 +96,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "is_secret", "description" ] -} +}, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "deleteVariable", @@ -110,7 +119,10 @@ export const mcpEndpointTools: EndpointTool[] = [ ] }, queryParamsSchema: undefined, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "updateVariable", @@ -121,12 +133,13 @@ export const mcpEndpointTools: EndpointTool[] = [ pathParamsSchema: { "type": "object", "properties": { - "path": { - "type": "string" + "path__path": { + "type": "string", + "description": "(path parameter)" } }, "required": [ - "path" + "path__path" ] }, queryParamsSchema: { @@ -142,10 +155,6 @@ export const mcpEndpointTools: EndpointTool[] = [ bodySchema: { "type": "object", "properties": { - "path": { - "type": "string", - "description": "The path to the variable" - }, "value": { "type": "string", "description": "The new value of the variable" @@ -157,8 +166,19 @@ export const mcpEndpointTools: EndpointTool[] = [ "description": { "type": "string", "description": "The new description of the variable" + }, + "path__body": { + "type": "string", + "description": "The path to the variable (body parameter)" } } +}, + pathFieldRenames: { + "path__path": "path" +}, + queryFieldRenames: undefined, + bodyFieldRenames: { + "path__body": "path" } }, { @@ -192,7 +212,10 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "required": [] }, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "listVariable", @@ -219,7 +242,10 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "required": [] }, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "createResource", @@ -245,7 +271,9 @@ export const mcpEndpointTools: EndpointTool[] = [ "type": "string", "description": "The path to the resource" }, - "value": {}, + "value": { + "type": "object" + }, "description": { "type": "string", "description": "The description of the resource" @@ -260,7 +288,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "value", "resource_type" ] -} +}, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "deleteResource", @@ -280,7 +311,10 @@ export const mcpEndpointTools: EndpointTool[] = [ ] }, queryParamsSchema: undefined, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "updateResource", @@ -291,32 +325,42 @@ export const mcpEndpointTools: EndpointTool[] = [ pathParamsSchema: { "type": "object", "properties": { - "path": { - "type": "string" + "path__path": { + "type": "string", + "description": "(path parameter)" } }, "required": [ - "path" + "path__path" ] }, queryParamsSchema: undefined, bodySchema: { "type": "object", "properties": { - "path": { - "type": "string", - "description": "The path to the resource" - }, "description": { "type": "string", "description": "The new description of the resource" }, - "value": {}, + "value": { + "type": "object" + }, "resource_type": { "type": "string", "description": "The new resource_type to be associated with the resource" + }, + "path__body": { + "type": "string", + "description": "The path to the resource (body parameter)" } } +}, + pathFieldRenames: { + "path__path": "path" +}, + queryFieldRenames: undefined, + bodyFieldRenames: { + "path__body": "path" } }, { @@ -337,7 +381,10 @@ export const mcpEndpointTools: EndpointTool[] = [ ] }, queryParamsSchema: undefined, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "listResource", @@ -372,7 +419,10 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "required": [] }, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "listResourceType", @@ -382,7 +432,10 @@ export const mcpEndpointTools: EndpointTool[] = [ method: "GET", pathParamsSchema: undefined, queryParamsSchema: undefined, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "listScripts", @@ -465,11 +518,123 @@ export const mcpEndpointTools: EndpointTool[] = [ "without_description": { "type": "boolean", "description": "(default false)\nIf true, the description field will be omitted from the response.\n" + }, + "dedicated_worker": { + "type": "boolean", + "description": "(default regardless)\nIf true, show only scripts with dedicated_worker enabled.\nIf false, show only scripts with dedicated_worker disabled.\n" } }, "required": [] }, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined + }, + { + name: "createScript", + description: "create script", + instructions: "To create a script, specify the path (e.g., 'f/my_folder/my_script'), the content (source code), and the language. For TypeScript, use 'bun' unless deno-specific APIs are needed.", + path: "/w/{workspace}/scripts/create", + method: "POST", + pathParamsSchema: undefined, + queryParamsSchema: undefined, + bodySchema: { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "content": { + "type": "string" + }, + "language": { + "type": "string", + "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative" + }, + "kind": { + "type": "string", + "description": "Possible values: script, failure, trigger, command, approval, preprocessor" + }, + "tag": { + "type": "string" + }, + "deployment_message": { + "type": "string" + } + }, + "required": [ + "path", + "summary", + "description", + "content", + "language" + ] +}, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined + }, + { + name: "deleteScriptByHash", + description: "delete script by hash (erase content but keep hash, require admin)", + instructions: "", + path: "/w/{workspace}/scripts/delete/h/{hash}", + method: "POST", + pathParamsSchema: { + "type": "object", + "properties": { + "hash": { + "type": "string" + } + }, + "required": [ + "hash" + ] +}, + queryParamsSchema: undefined, + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined + }, + { + name: "deleteScriptByPath", + description: "delete script at a given path (require admin)", + instructions: "", + path: "/w/{workspace}/scripts/delete/p/{path}", + method: "POST", + pathParamsSchema: { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] +}, + queryParamsSchema: { + "type": "object", + "properties": { + "keep_captures": { + "type": "boolean", + "description": "keep captures" + } + }, + "required": [] +}, + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "getScriptByPath", @@ -497,7 +662,37 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "required": [] }, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined + }, + { + name: "runScriptByPath", + description: "run script by path", + instructions: "You should first use getScriptByPath to retrieve the script's schema and understand what arguments are expected.", + path: "/w/{workspace}/jobs/run/p/{path}", + method: "POST", + pathParamsSchema: { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] +}, + queryParamsSchema: undefined, + bodySchema: { + "type": "object", + "description": "The arguments to pass to the script or flow", + "additionalProperties": true +}, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "listFlows", @@ -552,11 +747,18 @@ export const mcpEndpointTools: EndpointTool[] = [ "without_description": { "type": "boolean", "description": "(default false)\nIf true, the description field will be omitted from the response.\n" + }, + "dedicated_worker": { + "type": "boolean", + "description": "(default regardless)\nIf true, show only flows with dedicated_worker enabled.\nIf false, show only flows with dedicated_worker disabled.\n" } }, "required": [] }, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "getFlowByPath", @@ -584,7 +786,263 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "required": [] }, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined + }, + { + name: "createFlow", + description: "create flow", + instructions: "", + path: "/w/{workspace}/flows/create", + method: "POST", + pathParamsSchema: undefined, + queryParamsSchema: undefined, + bodySchema: { + "type": "object", + "properties": { + "summary": { + "type": "string", + "description": "Short description of what this flow does" + }, + "description": { + "type": "string", + "description": "Detailed documentation for this flow" + }, + "value": { + "type": "object" + }, + "schema": { + "type": "object" + }, + "path": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "deployment_message": { + "type": "string" + } + }, + "required": [ + "summary", + "value", + "path" + ], + "description": "Top-level flow definition containing metadata, configuration, and the flow structure" +}, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined + }, + { + name: "updateFlow", + description: "update flow", + instructions: "", + path: "/w/{workspace}/flows/update/{path}", + method: "POST", + pathParamsSchema: { + "type": "object", + "properties": { + "path__path": { + "type": "string", + "description": "(path parameter)" + } + }, + "required": [ + "path__path" + ] +}, + queryParamsSchema: undefined, + bodySchema: { + "type": "object", + "properties": { + "summary": { + "type": "string", + "description": "Short description of what this flow does" + }, + "description": { + "type": "string", + "description": "Detailed documentation for this flow" + }, + "value": { + "type": "object" + }, + "schema": { + "type": "object" + }, + "tag": { + "type": "string" + }, + "deployment_message": { + "type": "string" + }, + "path__body": { + "type": "string", + "description": "(body parameter)" + } + }, + "required": [ + "summary", + "value", + "path__body" + ], + "description": "Top-level flow definition containing metadata, configuration, and the flow structure" +}, + pathFieldRenames: { + "path__path": "path" +}, + queryFieldRenames: undefined, + bodyFieldRenames: { + "path__body": "path" +} + }, + { + name: "deleteFlowByPath", + description: "delete flow by path", + instructions: "", + path: "/w/{workspace}/flows/delete/{path}", + method: "DELETE", + pathParamsSchema: { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] +}, + queryParamsSchema: { + "type": "object", + "properties": { + "keep_captures": { + "type": "boolean", + "description": "keep captures" + } + }, + "required": [] +}, + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined + }, + { + name: "createApp", + description: "create app", + instructions: "", + path: "/w/{workspace}/apps/create", + method: "POST", + pathParamsSchema: undefined, + queryParamsSchema: undefined, + bodySchema: { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "value": { + "type": "object" + }, + "summary": { + "type": "string" + }, + "policy": { + "type": "object" + }, + "deployment_message": { + "type": "string" + } + }, + "required": [ + "path", + "value", + "summary", + "policy" + ] +}, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined + }, + { + name: "updateApp", + description: "update app", + instructions: "", + path: "/w/{workspace}/apps/update/{path}", + method: "POST", + pathParamsSchema: { + "type": "object", + "properties": { + "path__path": { + "type": "string", + "description": "(path parameter)" + } + }, + "required": [ + "path__path" + ] +}, + queryParamsSchema: undefined, + bodySchema: { + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "value": { + "type": "object" + }, + "policy": { + "type": "object" + }, + "deployment_message": { + "type": "string" + }, + "path__body": { + "type": "string", + "description": "(body parameter)" + } + } +}, + pathFieldRenames: { + "path__path": "path" +}, + queryFieldRenames: undefined, + bodyFieldRenames: { + "path__body": "path" +} + }, + { + name: "runFlowByPath", + description: "run flow by path", + instructions: "You should first use getFlowByPath to retrieve the flow's schema and understand what arguments are expected.", + path: "/w/{workspace}/jobs/run/f/{path}", + method: "POST", + pathParamsSchema: { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] +}, + queryParamsSchema: undefined, + bodySchema: { + "type": "object", + "description": "The arguments to pass to the script or flow", + "additionalProperties": true +}, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "runScriptPreviewAndWaitResult", @@ -612,45 +1070,18 @@ export const mcpEndpointTools: EndpointTool[] = [ "args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "language": { "type": "string", - "enum": [ - "python3", - "deno", - "go", - "bash", - "powershell", - "postgresql", - "mysql", - "bigquery", - "snowflake", - "mssql", - "oracledb", - "graphql", - "nativets", - "bun", - "php", - "rust", - "ansible", - "csharp", - "nu", - "java", - "ruby", - "duckdb" - ] + "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative" }, "tag": { "type": "string" }, "kind": { "type": "string", - "enum": [ - "code", - "identity", - "http" - ] + "description": "Possible values: code, identity, http" }, "dedicated_worker": { "type": "boolean" @@ -664,7 +1095,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "content", "language" ] -} +}, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "listQueue", @@ -710,22 +1144,8 @@ export const mcpEndpointTools: EndpointTool[] = [ "description": "mask to filter by trigger path" }, "trigger_kind": { - "description": "trigger kind (schedule, http, websocket...)", - "type": "string", - "enum": [ - "webhook", - "default_email", - "email", - "schedule", - "http", - "websocket", - "postgres", - "kafka", - "nats", - "mqtt", - "sqs", - "gcp" - ] + "description": "trigger kind (schedule, http, websocket...). Possible values: webhook, default_email, email, schedule, http, websocket, postgres, kafka, nats, mqtt, sqs, gcp", + "type": "string" }, "script_hash": { "type": "string", @@ -796,7 +1216,10 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "required": [] }, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "listJobs", @@ -918,22 +1341,8 @@ export const mcpEndpointTools: EndpointTool[] = [ "description": "number of items to return for a given page (default 30, max 100)" }, "trigger_kind": { - "description": "trigger kind (schedule, http, websocket...)", - "type": "string", - "enum": [ - "webhook", - "default_email", - "email", - "schedule", - "http", - "websocket", - "postgres", - "kafka", - "nats", - "mqtt", - "sqs", - "gcp" - ] + "description": "trigger kind (schedule, http, websocket...). Possible values: webhook, default_email, email, schedule, http, websocket, postgres, kafka, nats, mqtt, sqs, gcp", + "type": "string" }, "is_skipped": { "type": "boolean", @@ -962,7 +1371,10 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "required": [] }, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "createSchedule", @@ -977,75 +1389,75 @@ export const mcpEndpointTools: EndpointTool[] = [ "properties": { "path": { "type": "string", - "description": "The path where the schedule will be created" + "description": "The unique path identifier for this schedule" }, "schedule": { "type": "string", - "description": "The cron schedule to trigger the script or flow. Should include seconds." + "description": "Cron expression with 6 fields (seconds, minutes, hours, day of month, month, day of week). Example '0 0 12 * * *' for daily at noon" }, "timezone": { "type": "string", - "description": "The timezone to use for the cron schedule" + "description": "IANA timezone for the schedule (e.g., 'UTC', 'Europe/Paris', 'America/New_York')" }, "script_path": { "type": "string", - "description": "The path to the script or flow to trigger" + "description": "Path to the script or flow to execute when triggered" }, "is_flow": { "type": "boolean", - "description": "Whether the schedule is for a flow" + "description": "True if script_path points to a flow, false if it points to a script" }, "args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "enabled": { "type": "boolean", - "description": "Whether the schedule is enabled" + "description": "Whether the schedule is currently active and will trigger jobs" }, "on_failure": { "type": "string", - "description": "The path to the script or flow to trigger on failure" + "description": "Path to a script or flow to run when the scheduled job fails" }, "on_failure_times": { "type": "number", - "description": "The number of times to retry on failure" + "description": "Number of consecutive failures before the on_failure handler is triggered (default 1)" }, "on_failure_exact": { "type": "boolean", - "description": "Whether the schedule should only run on the exact time" + "description": "If true, trigger on_failure handler only on exactly N failures, not on every failure after N" }, "on_failure_extra_args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "on_recovery": { "type": "string", - "description": "The path to the script or flow to trigger on recovery" + "description": "Path to a script or flow to run when the schedule recovers after failures" }, "on_recovery_times": { "type": "number", - "description": "The number of times to retry on recovery" + "description": "Number of consecutive successes before the on_recovery handler is triggered (default 1)" }, "on_recovery_extra_args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "on_success": { "type": "string", - "description": "The path to the script or flow to trigger on success" + "description": "Path to a script or flow to run after each successful execution" }, "on_success_extra_args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "ws_error_handler_muted": { "type": "boolean", - "description": "Whether the WebSocket error handler is muted" + "description": "If true, the workspace-level error handler will not be triggered for this schedule's failures" }, "retry": { "type": "object", @@ -1107,32 +1519,32 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "no_flow_overlap": { "type": "boolean", - "description": "Whether the schedule should not run if a flow is already running" + "description": "If true, skip this schedule's execution if the previous run is still in progress (prevents concurrent runs)" }, "summary": { "type": "string", - "description": "The summary of the schedule" + "description": "Short summary describing the purpose of this schedule" }, "description": { "type": "string", - "description": "The description of the schedule" + "description": "Detailed description of what this schedule does" }, "tag": { "type": "string", - "description": "The tag of the schedule" + "description": "Worker tag to route jobs to specific worker groups" }, "paused_until": { "type": "string", - "description": "The date and time the schedule will be paused until", - "format": "date-time" + "format": "date-time", + "description": "ISO 8601 datetime until which the schedule is paused. Schedule resumes automatically after this time" }, "cron_version": { "type": "string", - "description": "The version of the cron schedule to use (last is v2)" + "description": "Cron parser version. Use 'v2' for extended syntax with additional features" }, "dynamic_skip": { "type": "string", - "description": "Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean." + "description": "Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)" } }, "required": [ @@ -1143,7 +1555,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "is_flow", "args" ] -} +}, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "updateSchedule", @@ -1168,59 +1583,59 @@ export const mcpEndpointTools: EndpointTool[] = [ "properties": { "schedule": { "type": "string", - "description": "The cron schedule to trigger the script or flow. Should include seconds." + "description": "Cron expression with 6 fields (seconds, minutes, hours, day of month, month, day of week). Example '0 0 12 * * *' for daily at noon" }, "timezone": { "type": "string", - "description": "The timezone to use for the cron schedule" + "description": "IANA timezone for the schedule (e.g., 'UTC', 'Europe/Paris', 'America/New_York')" }, "args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "on_failure": { "type": "string", - "description": "The path to the script or flow to trigger on failure" + "description": "Path to a script or flow to run when the scheduled job fails" }, "on_failure_times": { "type": "number", - "description": "The number of times to retry on failure" + "description": "Number of consecutive failures before the on_failure handler is triggered (default 1)" }, "on_failure_exact": { "type": "boolean", - "description": "Whether the schedule should only run on the exact time" + "description": "If true, trigger on_failure handler only on exactly N failures, not on every failure after N" }, "on_failure_extra_args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "on_recovery": { "type": "string", - "description": "The path to the script or flow to trigger on recovery" + "description": "Path to a script or flow to run when the schedule recovers after failures" }, "on_recovery_times": { "type": "number", - "description": "The number of times to retry on recovery" + "description": "Number of consecutive successes before the on_recovery handler is triggered (default 1)" }, "on_recovery_extra_args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "on_success": { "type": "string", - "description": "The path to the script or flow to trigger on success" + "description": "Path to a script or flow to run after each successful execution" }, "on_success_extra_args": { "type": "object", "description": "The arguments to pass to the script or flow", - "additionalProperties": {} + "additionalProperties": true }, "ws_error_handler_muted": { "type": "boolean", - "description": "Whether the WebSocket error handler is muted" + "description": "If true, the workspace-level error handler will not be triggered for this schedule's failures" }, "retry": { "type": "object", @@ -1282,32 +1697,32 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "no_flow_overlap": { "type": "boolean", - "description": "Whether the schedule should not run if a flow is already running" + "description": "If true, skip this schedule's execution if the previous run is still in progress (prevents concurrent runs)" }, "summary": { "type": "string", - "description": "The summary of the schedule" + "description": "Short summary describing the purpose of this schedule" }, "description": { "type": "string", - "description": "The description of the schedule" + "description": "Detailed description of what this schedule does" }, "tag": { "type": "string", - "description": "The tag of the schedule" + "description": "Worker tag to route jobs to specific worker groups" }, "paused_until": { "type": "string", - "description": "The date and time the schedule will be paused until", - "format": "date-time" + "format": "date-time", + "description": "ISO 8601 datetime until which the schedule is paused. Schedule resumes automatically after this time" }, "cron_version": { "type": "string", - "description": "The version of the cron schedule to use (last is v2)" + "description": "Cron parser version. Use 'v2' for extended syntax with additional features" }, "dynamic_skip": { "type": "string", - "description": "Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean." + "description": "Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)" } }, "required": [ @@ -1315,7 +1730,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "timezone", "args" ] -} +}, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "deleteSchedule", @@ -1335,7 +1753,10 @@ export const mcpEndpointTools: EndpointTool[] = [ ] }, queryParamsSchema: undefined, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "getSchedule", @@ -1355,7 +1776,10 @@ export const mcpEndpointTools: EndpointTool[] = [ ] }, queryParamsSchema: undefined, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "listSchedules", @@ -1394,7 +1818,10 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "required": [] }, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined }, { name: "listWorkers", @@ -1421,6 +1848,9 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "required": [] }, - bodySchema: undefined + bodySchema: undefined, + pathFieldRenames: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined } ];