feat: expose getJob and getJobLogs as MCP tools (#8632)

* feat: expose getJob and getJobLogs as MCP tools

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add integration test for getJob/getJobLogs MCP endpoint tools

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add MCP client integration test for getJob and getJobLogs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-03-31 07:09:34 +00:00
committed by GitHub
parent 9d85768287
commit cd8edcd94f
8 changed files with 629 additions and 52 deletions
+1
View File
@@ -16382,6 +16382,7 @@ dependencies = [
"rand 0.9.0",
"rdkafka",
"reqwest 0.13.1",
"rmcp",
"rumqttc",
"serde",
"serde_json",
@@ -13,7 +13,7 @@ default = []
private = ["windmill-test-utils/private", "dep:aws-config", "dep:aws-credential-types", "dep:aws-sdk-sqs", "windmill-git-sync/private"]
enterprise = ["windmill-test-utils/enterprise", "dep:base64", "windmill-git-sync/enterprise"]
deno_core = ["windmill-test-utils/deno_core"]
mcp = []
mcp = ["windmill-test-utils/mcp", "dep:rmcp"]
run_inline = ["dep:windmill-worker", "windmill-test-utils/run_inline", "windmill-test-utils/duckdb"]
[dependencies]
@@ -41,3 +41,4 @@ aws-credential-types = { workspace = true, optional = true }
aws-sdk-sqs = { workspace = true, optional = true }
base64 = { workspace = true, optional = true }
axum.workspace = true
rmcp = { workspace = true, optional = true }
@@ -1,5 +1,7 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
#[cfg(feature = "mcp")]
use uuid::Uuid;
use windmill_test_utils::*;
@@ -518,3 +520,182 @@ async fn test_mcp_tools(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[cfg(feature = "mcp")]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_mcp_endpoint_tools_list(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let resp = authed(client().get(format!(
"http://localhost:{port}/api/mcp/w/test-workspace/list_tools"
)))
.send()
.await?;
assert_eq!(resp.status(), 200);
let tools: Vec<serde_json::Value> = resp.json().await?;
let tool_names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
assert!(
tool_names.contains(&"getJob"),
"getJob not found in MCP endpoint tools: {tool_names:?}"
);
assert!(
tool_names.contains(&"getJobLogs"),
"getJobLogs not found in MCP endpoint tools: {tool_names:?}"
);
// Verify getJob has the expected path and method
let get_job_tool = tools.iter().find(|t| t["name"] == "getJob").unwrap();
assert_eq!(get_job_tool["path"], "/w/{workspace}/jobs_u/get/{id}");
assert_eq!(get_job_tool["method"], "GET");
// Verify getJobLogs has the expected path and method
let get_job_logs_tool = tools.iter().find(|t| t["name"] == "getJobLogs").unwrap();
assert_eq!(
get_job_logs_tool["path"],
"/w/{workspace}/jobs_u/get_logs/{id}"
);
assert_eq!(get_job_logs_tool["method"], "GET");
Ok(())
}
#[cfg(feature = "mcp")]
async fn insert_completed_job_with_logs(db: &Pool<Postgres>) -> Uuid {
let id = Uuid::new_v4();
sqlx::query(
"INSERT INTO v2_job (id, workspace_id, created_by, permissioned_as, kind, tag, args)
VALUES ($1, 'test-workspace', 'test-user', 'u/test-user', 'script', 'deno', '{}'::jsonb)",
)
.bind(id)
.execute(db)
.await
.unwrap();
sqlx::query(
"INSERT INTO v2_job_completed (id, workspace_id, duration_ms, result, status)
VALUES ($1, 'test-workspace', 100, '42'::jsonb, 'success')",
)
.bind(id)
.execute(db)
.await
.unwrap();
sqlx::query(
"INSERT INTO job_logs (job_id, workspace_id, logs, log_offset)
VALUES ($1, 'test-workspace', 'hello world test log', 0)",
)
.bind(id)
.execute(db)
.await
.unwrap();
id
}
#[cfg(feature = "mcp")]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_mcp_client_get_job_and_logs(db: Pool<Postgres>) -> anyhow::Result<()> {
use rmcp::model::{
CallToolRequestParams, ClientCapabilities, ClientInfo, Implementation,
InitializeRequestParams,
};
use rmcp::service::{RoleClient, RunningService};
use rmcp::transport::streamable_http_client::{
StreamableHttpClientTransport, StreamableHttpClientTransportConfig,
};
use rmcp::ServiceExt;
initialize_tracing().await;
set_jwt_secret().await;
let server = ApiServer::start_mcp(db.clone()).await?;
let port = server.addr.port();
let job_id = insert_completed_job_with_logs(&db).await;
// Create a token with MCP scopes
sqlx::query(
"INSERT INTO token (token_hash, token_prefix, token, email, label, super_admin, scopes)
VALUES (encode(sha256('MCP_TOKEN'::bytea), 'hex'), 'MCP_TOK', 'MCP_TOKEN', 'test@windmill.dev', 'mcp token', true, ARRAY['mcp:all'])",
)
.execute(&db)
.await?;
// Connect as MCP client
let config = StreamableHttpClientTransportConfig::with_uri(format!(
"http://localhost:{port}/api/mcp/w/test-workspace/mcp"
))
.auth_header("MCP_TOKEN");
let transport = StreamableHttpClientTransport::from_config(config);
let client_info = ClientInfo {
protocol_version: Default::default(),
capabilities: ClientCapabilities::default(),
client_info: Implementation {
name: "test-client".to_string(),
title: None,
version: "0.0.1".to_string(),
description: None,
website_url: None,
icons: None,
},
meta: None,
};
let client: RunningService<RoleClient, InitializeRequestParams> =
client_info.serve(transport).await?;
// --- Test getJob ---
let result = client
.call_tool(CallToolRequestParams {
name: "getJob".into(),
arguments: Some(serde_json::from_value(json!({ "id": job_id.to_string() }))?),
task: None,
meta: None,
})
.await?;
let text = result
.content
.first()
.and_then(|c| c.raw.as_text())
.expect("getJob should return text content");
let job: serde_json::Value = serde_json::from_str(&text.text)?;
assert_eq!(job["id"], job_id.to_string());
assert_eq!(job["workspace_id"], "test-workspace");
assert_eq!(job["created_by"], "test-user");
assert_eq!(job["job_kind"], "script");
assert!(
job["success"].as_bool().unwrap_or(false),
"job should be successful: {job}"
);
// --- Test getJobLogs ---
let result = client
.call_tool(CallToolRequestParams {
name: "getJobLogs".into(),
arguments: Some(serde_json::from_value(json!({ "id": job_id.to_string() }))?),
task: None,
meta: None,
})
.await?;
let text = result
.content
.first()
.and_then(|c| c.raw.as_text())
.expect("getJobLogs should return text content");
// The logs endpoint returns text/plain, which gets wrapped as a JSON string by call_endpoint
let logs: String = serde_json::from_str(&text.text)?;
assert!(
logs.contains("hello world test log"),
"expected logs to contain test log, got: {logs}"
);
client.cancel().await?;
Ok(())
}
+3 -1
View File
@@ -10598,6 +10598,7 @@ paths:
get:
summary: get job
operationId: getJob
x-mcp-tool: true
tags:
- job
parameters:
@@ -10639,7 +10640,8 @@ paths:
/w/{workspace}/jobs_u/get_logs/{id}:
get:
summary: get job logs
operationId: getJob logs
operationId: getJobLogs
x-mcp-tool: true
tags:
- job
parameters:
@@ -221,6 +221,22 @@ pub fn all_tools() -> Vec<EndpointTool> {
"type": "string",
"description": "filter variables by path prefix"
},
"path": {
"type": "string",
"description": "exact path match filter"
},
"description": {
"type": "string",
"description": "pattern match filter for description field (case-insensitive)"
},
"value": {
"type": "string",
"description": "pattern match filter for non-secret variable values (case-insensitive)"
},
"broad_filter": {
"type": "string",
"description": "broad search across multiple fields (case-insensitive substring match)"
},
"page": {
"type": "integer",
"description": "which page to return (start at 1, default 1)"
@@ -405,6 +421,22 @@ pub fn all_tools() -> Vec<EndpointTool> {
"path_start": {
"type": "string",
"description": "filter resources by path prefix"
},
"path": {
"type": "string",
"description": "exact path match filter"
},
"description": {
"type": "string",
"description": "pattern match filter for description field (case-insensitive)"
},
"value": {
"type": "string",
"description": "JSONB subset match filter using base64 encoded JSON"
},
"broad_filter": {
"type": "string",
"description": "broad search across multiple fields (case-insensitive substring match)"
}
},
"required": []
@@ -451,7 +483,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"created_by": {
"type": "string",
"description": "mask to filter exact matching user creator"
"description": "filter by exact matching user creator. Supports comma-separated list (e.g. 'alice,bob') and negation by prefixing all values with '!' (e.g. '!alice,!bob')"
},
"path_start": {
"type": "string",
@@ -562,7 +594,6 @@ pub fn all_tools() -> Vec<EndpointTool> {
"required": [
"path",
"summary",
"description",
"content",
"language"
]
@@ -708,7 +739,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"created_by": {
"type": "string",
"description": "mask to filter exact matching user creator"
"description": "filter by exact matching user creator. Supports comma-separated list (e.g. 'alice,bob') and negation by prefixing all values with '!' (e.g. '!alice,!bob')"
},
"path_start": {
"type": "string",
@@ -1078,6 +1109,37 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"lock": {
"type": "string"
},
"flow_path": {
"type": "string"
},
"modules": {
"type": "object",
"nullable": true,
"description": "Additional script modules keyed by relative file path",
"additionalProperties": {
"type": "object",
"description": "An additional module file associated with a script",
"properties": {
"content": {
"type": "string",
"description": "The source code content of this module"
},
"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"
},
"lock": {
"type": "string",
"nullable": true,
"description": "Lock file content for this module's dependencies"
}
},
"required": [
"content",
"language"
]
}
}
},
"required": [
@@ -1106,7 +1168,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"created_by": {
"type": "string",
"description": "mask to filter exact matching user creator"
"description": "filter by exact matching user creator. Supports comma-separated list (e.g. 'alice,bob') and negation by prefixing all values with '!' (e.g. '!alice,!bob')"
},
"parent_job": {
"type": "string",
@@ -1115,15 +1177,15 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"worker": {
"type": "string",
"description": "worker this job was ran on"
"description": "filter by worker this job ran on. Supports comma-separated list (e.g. 'worker-1,worker-2') and negation by prefixing all values with '!' (e.g. '!worker-1,!worker-2')"
},
"script_path_exact": {
"type": "string",
"description": "mask to filter exact matching path"
"description": "filter by exact matching script path. Supports comma-separated list (e.g. 'f/script1,f/script2') and negation by prefixing all values with '!' (e.g. '!f/script1,!f/script2')"
},
"script_path_start": {
"type": "string",
"description": "mask to filter matching starting path"
"description": "filter by script path prefix. Supports comma-separated list (e.g. 'f/folder1,f/folder2') and negation by prefixing all values with '!' (e.g. '!f/folder1,!f/folder2')"
},
"schedule_path": {
"type": "string",
@@ -1131,11 +1193,11 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"trigger_path": {
"type": "string",
"description": "mask to filter by trigger path"
"description": "filter by trigger path. Supports comma-separated list (e.g. 'f/trigger1,f/trigger2') and negation by prefixing all values with '!' (e.g. '!f/trigger1,!f/trigger2')"
},
"trigger_kind": {
"description": "trigger kind (schedule, http, websocket...). Possible values: webhook, default_email, email, schedule, http, websocket, postgres, kafka, nats, mqtt, sqs, gcp",
"type": "string"
"type": "string",
"description": "filter by trigger kind. Supports comma-separated list (e.g. 'schedule,webhook') and negation by prefixing all values with '!' (e.g. '!schedule,!webhook')"
},
"script_hash": {
"type": "string",
@@ -1161,7 +1223,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"job_kinds": {
"type": "string",
"description": "filter on job kind (values 'preview', 'script', 'dependencies', 'flow') separated by,"
"description": "filter by job kind. Supports comma-separated list of values ('preview', 'script', 'dependencies', 'flow') and negation by prefixing all values with '!' (e.g. '!preview,!dependencies')"
},
"suspended": {
"type": "boolean",
@@ -1185,7 +1247,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"tag": {
"type": "string",
"description": "filter on jobs with a given tag/worker group"
"description": "filter by tag/worker group. Supports comma-separated list (e.g. 'gpu,highmem') and negation by prefixing all values with '!' (e.g. '!gpu,!highmem')"
},
"page": {
"type": "integer",
@@ -1223,15 +1285,15 @@ pub fn all_tools() -> Vec<EndpointTool> {
"properties": {
"created_by": {
"type": "string",
"description": "mask to filter exact matching user creator"
"description": "filter by exact matching user creator. Supports comma-separated list (e.g. 'alice,bob') and negation by prefixing all values with '!' (e.g. '!alice,!bob')"
},
"label": {
"type": "string",
"description": "mask to filter exact matching job's label (job labels are completed jobs with as a result an object containing a string in the array at key 'wm_labels')"
"description": "filter by exact matching job label. Supports comma-separated list (e.g. 'deploy,release') and negation by prefixing all values with '!' (e.g. '!deploy,!release')"
},
"worker": {
"type": "string",
"description": "worker this job was ran on"
"description": "filter by worker this job ran on. Supports comma-separated list (e.g. 'worker-1,worker-2') and negation by prefixing all values with '!' (e.g. '!worker-1,!worker-2')"
},
"parent_job": {
"type": "string",
@@ -1240,11 +1302,11 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"script_path_exact": {
"type": "string",
"description": "mask to filter exact matching path"
"description": "filter by exact matching script path. Supports comma-separated list (e.g. 'f/script1,f/script2') and negation by prefixing all values with '!' (e.g. '!f/script1,!f/script2')"
},
"script_path_start": {
"type": "string",
"description": "mask to filter matching starting path"
"description": "filter by script path prefix. Supports comma-separated list (e.g. 'f/folder1,f/folder2') and negation by prefixing all values with '!' (e.g. '!f/folder1,!f/folder2')"
},
"schedule_path": {
"type": "string",
@@ -1304,7 +1366,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"job_kinds": {
"type": "string",
"description": "filter on job kind (values 'preview', 'script', 'dependencies', 'flow') separated by,"
"description": "filter by job kind. Supports comma-separated list of values ('preview', 'script', 'dependencies', 'flow') and negation by prefixing all values with '!' (e.g. '!preview,!dependencies')"
},
"suspended": {
"type": "boolean",
@@ -1316,7 +1378,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"tag": {
"type": "string",
"description": "filter on jobs with a given tag/worker group"
"description": "filter by tag/worker group. Supports comma-separated list (e.g. 'gpu,highmem') and negation by prefixing all values with '!' (e.g. '!gpu,!highmem')"
},
"result": {
"type": "string",
@@ -1331,8 +1393,8 @@ pub fn all_tools() -> Vec<EndpointTool> {
"description": "number of items to return for a given page (default 30, max 100)"
},
"trigger_kind": {
"description": "trigger kind (schedule, http, websocket...). Possible values: webhook, default_email, email, schedule, http, websocket, postgres, kafka, nats, mqtt, sqs, gcp",
"type": "string"
"type": "string",
"description": "filter by trigger kind. Supports comma-separated list (e.g. 'schedule,webhook') and negation by prefixing all values with '!' (e.g. '!schedule,!webhook')"
},
"is_skipped": {
"type": "boolean",
@@ -1357,6 +1419,77 @@ pub fn all_tools() -> Vec<EndpointTool> {
"is_not_schedule": {
"type": "boolean",
"description": "is not a scheduled job"
},
"broad_filter": {
"type": "string",
"description": "broad search across multiple fields (case-insensitive substring match on path, tag, schedule path, trigger kind, label)"
}
},
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
EndpointTool {
name: Cow::Borrowed("getJob"),
description: Cow::Borrowed("get job"),
instructions: Cow::Borrowed(""),
path: Cow::Borrowed("/w/{workspace}/jobs_u/get/{id}"),
method: Cow::Borrowed("GET"),
path_params_schema: Some(serde_json::json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
}
},
"required": [
"id"
]
})),
query_params_schema: Some(serde_json::json!({
"type": "object",
"properties": {
"no_logs": {
"type": "boolean"
},
"no_code": {
"type": "boolean"
}
},
"required": []
})),
body_schema: None,
path_field_renames: None,
query_field_renames: None,
body_field_renames: None,
},
EndpointTool {
name: Cow::Borrowed("getJobLogs"),
description: Cow::Borrowed("get job logs"),
instructions: Cow::Borrowed(""),
path: Cow::Borrowed("/w/{workspace}/jobs_u/get_logs/{id}"),
method: Cow::Borrowed("GET"),
path_params_schema: Some(serde_json::json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
}
},
"required": [
"id"
]
})),
query_params_schema: Some(serde_json::json!({
"type": "object",
"properties": {
"remove_ansi_warnings": {
"type": "boolean"
}
},
"required": []
@@ -1411,14 +1544,17 @@ You should get the schema of the script or flow before creating the schedule to
},
"on_failure": {
"type": "string",
"nullable": true,
"description": "Path to a script or flow to run when the scheduled job fails"
},
"on_failure_times": {
"type": "number",
"nullable": true,
"description": "Number of consecutive failures before the on_failure handler is triggered (default 1)"
},
"on_failure_exact": {
"type": "boolean",
"nullable": true,
"description": "If true, trigger on_failure handler only on exactly N failures, not on every failure after N"
},
"on_failure_extra_args": {
@@ -1428,10 +1564,12 @@ You should get the schema of the script or flow before creating the schedule to
},
"on_recovery": {
"type": "string",
"nullable": true,
"description": "Path to a script or flow to run when the schedule recovers after failures"
},
"on_recovery_times": {
"type": "number",
"nullable": true,
"description": "Number of consecutive successes before the on_recovery handler is triggered (default 1)"
},
"on_recovery_extra_args": {
@@ -1441,6 +1579,7 @@ You should get the schema of the script or flow before creating the schedule to
},
"on_success": {
"type": "string",
"nullable": true,
"description": "Path to a script or flow to run after each successful execution"
},
"on_success_extra_args": {
@@ -1516,28 +1655,42 @@ You should get the schema of the script or flow before creating the schedule to
},
"summary": {
"type": "string",
"nullable": true,
"description": "Short summary describing the purpose of this schedule"
},
"description": {
"type": "string",
"nullable": true,
"description": "Detailed description of what this schedule does"
},
"tag": {
"type": "string",
"nullable": true,
"description": "Worker tag to route jobs to specific worker groups"
},
"paused_until": {
"type": "string",
"nullable": true,
"format": "date-time",
"description": "ISO 8601 datetime until which the schedule is paused. Schedule resumes automatically after this time"
},
"cron_version": {
"type": "string",
"nullable": true,
"description": "Cron parser version. Use 'v2' for extended syntax with additional features"
},
"dynamic_skip": {
"type": "string",
"nullable": true,
"description": "Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)"
},
"permissioned_as": {
"type": "string",
"description": "The user or group this schedule runs as. Used during deployment to preserve the original schedule owner."
},
"preserve_permissioned_as": {
"type": "boolean",
"description": "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
}
},
"required": [
@@ -1592,14 +1745,17 @@ You should get the schema of the script or flow before updating the schedule to
},
"on_failure": {
"type": "string",
"nullable": true,
"description": "Path to a script or flow to run when the scheduled job fails"
},
"on_failure_times": {
"type": "number",
"nullable": true,
"description": "Number of consecutive failures before the on_failure handler is triggered (default 1)"
},
"on_failure_exact": {
"type": "boolean",
"nullable": true,
"description": "If true, trigger on_failure handler only on exactly N failures, not on every failure after N"
},
"on_failure_extra_args": {
@@ -1609,10 +1765,12 @@ You should get the schema of the script or flow before updating the schedule to
},
"on_recovery": {
"type": "string",
"nullable": true,
"description": "Path to a script or flow to run when the schedule recovers after failures"
},
"on_recovery_times": {
"type": "number",
"nullable": true,
"description": "Number of consecutive successes before the on_recovery handler is triggered (default 1)"
},
"on_recovery_extra_args": {
@@ -1622,6 +1780,7 @@ You should get the schema of the script or flow before updating the schedule to
},
"on_success": {
"type": "string",
"nullable": true,
"description": "Path to a script or flow to run after each successful execution"
},
"on_success_extra_args": {
@@ -1697,28 +1856,44 @@ You should get the schema of the script or flow before updating the schedule to
},
"summary": {
"type": "string",
"nullable": true,
"description": "Short summary describing the purpose of this schedule"
},
"description": {
"type": "string",
"nullable": true,
"description": "Detailed description of what this schedule does"
},
"tag": {
"type": "string",
"nullable": true,
"description": "Worker tag to route jobs to specific worker groups"
},
"paused_until": {
"type": "string",
"nullable": true,
"format": "date-time",
"description": "ISO 8601 datetime until which the schedule is paused. Schedule resumes automatically after this time"
},
"cron_version": {
"type": "string",
"nullable": true,
"description": "Cron parser version. Use 'v2' for extended syntax with additional features"
},
"dynamic_skip": {
"type": "string",
"nullable": true,
"description": "Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)"
},
"permissioned_as": {
"type": "string",
"nullable": true,
"description": "The user or group this schedule runs as (e.g., 'u/admin' or 'g/mygroup'). Only admins and wm_deployers can set this via preserve_permissioned_as."
},
"preserve_permissioned_as": {
"type": "boolean",
"nullable": true,
"description": "If true and user is admin/wm_deployers, preserve the provided permissioned_as instead of using the deploying user's identity"
}
},
"required": [
@@ -1801,7 +1976,7 @@ You should get the schema of the script or flow before updating the schedule to
},
"path": {
"type": "string",
"description": "filter by path"
"description": "filter by path (script path)"
},
"is_flow": {
"type": "boolean",
@@ -1810,6 +1985,22 @@ You should get the schema of the script or flow before updating the schedule to
"path_start": {
"type": "string",
"description": "filter schedules by path prefix"
},
"schedule_path": {
"type": "string",
"description": "exact match on the schedule's path"
},
"description": {
"type": "string",
"description": "pattern match filter for description field (case-insensitive)"
},
"summary": {
"type": "string",
"description": "pattern match filter for summary field (case-insensitive)"
},
"broad_filter": {
"type": "string",
"description": "broad search across multiple fields (case-insensitive substring match)"
}
},
"required": []
+1
View File
@@ -14,6 +14,7 @@ private = ["windmill-api/private"]
enterprise = ["windmill-api/enterprise"]
python = ["windmill-common/python"]
deno_core = ["dep:windmill-runtime-nativets"]
mcp = ["windmill-api/mcp"]
agent_worker_server = ["dep:windmill-api-agent-workers"]
run_inline = ["windmill-api/run_inline"]
duckdb = ["windmill-worker/duckdb"]
+15 -6
View File
@@ -81,20 +81,29 @@ pub struct ApiServer {
impl ApiServer {
pub async fn start(db: Pool<Postgres>) -> anyhow::Result<Self> {
Self::start_inner(db, false).await
Self::start_inner(db, false, false).await
}
pub async fn start_agent_mode(db: Pool<Postgres>) -> anyhow::Result<Self> {
Self::start_inner(db, true).await
Self::start_inner(db, true, false).await
}
/// Start the API server with server_mode=true so trigger listeners are active.
/// Alias for `start_agent_mode` with a clearer name for trigger e2e tests.
pub async fn start_with_listeners(db: Pool<Postgres>) -> anyhow::Result<Self> {
Self::start_inner(db, true).await
Self::start_inner(db, true, false).await
}
async fn start_inner(db: Pool<Postgres>, agent_mode: bool) -> anyhow::Result<Self> {
/// Start the API server with mcp_mode=true so MCP routes are active.
pub async fn start_mcp(db: Pool<Postgres>) -> anyhow::Result<Self> {
Self::start_inner(db, false, true).await
}
async fn start_inner(
db: Pool<Postgres>,
server_mode: bool,
mcp_mode: bool,
) -> anyhow::Result<Self> {
let (tx, rx) = tokio::sync::broadcast::channel::<()>(1);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
@@ -114,8 +123,8 @@ impl ApiServer {
listener,
rx,
port_tx,
agent_mode,
false,
server_mode,
mcp_mode,
format!("http://localhost:{}", addr.port()),
Some(name.clone()),
));
+213 -22
View File
@@ -231,6 +231,22 @@ export const mcpEndpointTools: EndpointTool[] = [
"type": "string",
"description": "filter variables by path prefix"
},
"path": {
"type": "string",
"description": "exact path match filter"
},
"description": {
"type": "string",
"description": "pattern match filter for description field (case-insensitive)"
},
"value": {
"type": "string",
"description": "pattern match filter for non-secret variable values (case-insensitive)"
},
"broad_filter": {
"type": "string",
"description": "broad search across multiple fields (case-insensitive substring match)"
},
"page": {
"type": "integer",
"description": "which page to return (start at 1, default 1)"
@@ -415,6 +431,22 @@ export const mcpEndpointTools: EndpointTool[] = [
"path_start": {
"type": "string",
"description": "filter resources by path prefix"
},
"path": {
"type": "string",
"description": "exact path match filter"
},
"description": {
"type": "string",
"description": "pattern match filter for description field (case-insensitive)"
},
"value": {
"type": "string",
"description": "JSONB subset match filter using base64 encoded JSON"
},
"broad_filter": {
"type": "string",
"description": "broad search across multiple fields (case-insensitive substring match)"
}
},
"required": []
@@ -461,7 +493,7 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"created_by": {
"type": "string",
"description": "mask to filter exact matching user creator"
"description": "filter by exact matching user creator. Supports comma-separated list (e.g. 'alice,bob') and negation by prefixing all values with '!' (e.g. '!alice,!bob')"
},
"path_start": {
"type": "string",
@@ -572,7 +604,6 @@ export const mcpEndpointTools: EndpointTool[] = [
"required": [
"path",
"summary",
"description",
"content",
"language"
]
@@ -718,7 +749,7 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"created_by": {
"type": "string",
"description": "mask to filter exact matching user creator"
"description": "filter by exact matching user creator. Supports comma-separated list (e.g. 'alice,bob') and negation by prefixing all values with '!' (e.g. '!alice,!bob')"
},
"path_start": {
"type": "string",
@@ -1088,6 +1119,37 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"lock": {
"type": "string"
},
"flow_path": {
"type": "string"
},
"modules": {
"type": "object",
"nullable": true,
"description": "Additional script modules keyed by relative file path",
"additionalProperties": {
"type": "object",
"description": "An additional module file associated with a script",
"properties": {
"content": {
"type": "string",
"description": "The source code content of this module"
},
"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"
},
"lock": {
"type": "string",
"nullable": true,
"description": "Lock file content for this module's dependencies"
}
},
"required": [
"content",
"language"
]
}
}
},
"required": [
@@ -1116,7 +1178,7 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"created_by": {
"type": "string",
"description": "mask to filter exact matching user creator"
"description": "filter by exact matching user creator. Supports comma-separated list (e.g. 'alice,bob') and negation by prefixing all values with '!' (e.g. '!alice,!bob')"
},
"parent_job": {
"type": "string",
@@ -1125,15 +1187,15 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"worker": {
"type": "string",
"description": "worker this job was ran on"
"description": "filter by worker this job ran on. Supports comma-separated list (e.g. 'worker-1,worker-2') and negation by prefixing all values with '!' (e.g. '!worker-1,!worker-2')"
},
"script_path_exact": {
"type": "string",
"description": "mask to filter exact matching path"
"description": "filter by exact matching script path. Supports comma-separated list (e.g. 'f/script1,f/script2') and negation by prefixing all values with '!' (e.g. '!f/script1,!f/script2')"
},
"script_path_start": {
"type": "string",
"description": "mask to filter matching starting path"
"description": "filter by script path prefix. Supports comma-separated list (e.g. 'f/folder1,f/folder2') and negation by prefixing all values with '!' (e.g. '!f/folder1,!f/folder2')"
},
"schedule_path": {
"type": "string",
@@ -1141,11 +1203,11 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"trigger_path": {
"type": "string",
"description": "mask to filter by trigger path"
"description": "filter by trigger path. Supports comma-separated list (e.g. 'f/trigger1,f/trigger2') and negation by prefixing all values with '!' (e.g. '!f/trigger1,!f/trigger2')"
},
"trigger_kind": {
"description": "trigger kind (schedule, http, websocket...). Possible values: webhook, default_email, email, schedule, http, websocket, postgres, kafka, nats, mqtt, sqs, gcp",
"type": "string"
"type": "string",
"description": "filter by trigger kind. Supports comma-separated list (e.g. 'schedule,webhook') and negation by prefixing all values with '!' (e.g. '!schedule,!webhook')"
},
"script_hash": {
"type": "string",
@@ -1171,7 +1233,7 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"job_kinds": {
"type": "string",
"description": "filter on job kind (values 'preview', 'script', 'dependencies', 'flow') separated by,"
"description": "filter by job kind. Supports comma-separated list of values ('preview', 'script', 'dependencies', 'flow') and negation by prefixing all values with '!' (e.g. '!preview,!dependencies')"
},
"suspended": {
"type": "boolean",
@@ -1195,7 +1257,7 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"tag": {
"type": "string",
"description": "filter on jobs with a given tag/worker group"
"description": "filter by tag/worker group. Supports comma-separated list (e.g. 'gpu,highmem') and negation by prefixing all values with '!' (e.g. '!gpu,!highmem')"
},
"page": {
"type": "integer",
@@ -1233,15 +1295,15 @@ export const mcpEndpointTools: EndpointTool[] = [
"properties": {
"created_by": {
"type": "string",
"description": "mask to filter exact matching user creator"
"description": "filter by exact matching user creator. Supports comma-separated list (e.g. 'alice,bob') and negation by prefixing all values with '!' (e.g. '!alice,!bob')"
},
"label": {
"type": "string",
"description": "mask to filter exact matching job's label (job labels are completed jobs with as a result an object containing a string in the array at key 'wm_labels')"
"description": "filter by exact matching job label. Supports comma-separated list (e.g. 'deploy,release') and negation by prefixing all values with '!' (e.g. '!deploy,!release')"
},
"worker": {
"type": "string",
"description": "worker this job was ran on"
"description": "filter by worker this job ran on. Supports comma-separated list (e.g. 'worker-1,worker-2') and negation by prefixing all values with '!' (e.g. '!worker-1,!worker-2')"
},
"parent_job": {
"type": "string",
@@ -1250,11 +1312,11 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"script_path_exact": {
"type": "string",
"description": "mask to filter exact matching path"
"description": "filter by exact matching script path. Supports comma-separated list (e.g. 'f/script1,f/script2') and negation by prefixing all values with '!' (e.g. '!f/script1,!f/script2')"
},
"script_path_start": {
"type": "string",
"description": "mask to filter matching starting path"
"description": "filter by script path prefix. Supports comma-separated list (e.g. 'f/folder1,f/folder2') and negation by prefixing all values with '!' (e.g. '!f/folder1,!f/folder2')"
},
"schedule_path": {
"type": "string",
@@ -1314,7 +1376,7 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"job_kinds": {
"type": "string",
"description": "filter on job kind (values 'preview', 'script', 'dependencies', 'flow') separated by,"
"description": "filter by job kind. Supports comma-separated list of values ('preview', 'script', 'dependencies', 'flow') and negation by prefixing all values with '!' (e.g. '!preview,!dependencies')"
},
"suspended": {
"type": "boolean",
@@ -1326,7 +1388,7 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"tag": {
"type": "string",
"description": "filter on jobs with a given tag/worker group"
"description": "filter by tag/worker group. Supports comma-separated list (e.g. 'gpu,highmem') and negation by prefixing all values with '!' (e.g. '!gpu,!highmem')"
},
"result": {
"type": "string",
@@ -1341,8 +1403,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...). Possible values: webhook, default_email, email, schedule, http, websocket, postgres, kafka, nats, mqtt, sqs, gcp",
"type": "string"
"type": "string",
"description": "filter by trigger kind. Supports comma-separated list (e.g. 'schedule,webhook') and negation by prefixing all values with '!' (e.g. '!schedule,!webhook')"
},
"is_skipped": {
"type": "boolean",
@@ -1367,6 +1429,77 @@ export const mcpEndpointTools: EndpointTool[] = [
"is_not_schedule": {
"type": "boolean",
"description": "is not a scheduled job"
},
"broad_filter": {
"type": "string",
"description": "broad search across multiple fields (case-insensitive substring match on path, tag, schedule path, trigger kind, label)"
}
},
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
{
name: "getJob",
description: "get job",
instructions: "",
path: "/w/{workspace}/jobs_u/get/{id}",
method: "GET",
pathParamsSchema: {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
}
},
"required": [
"id"
]
},
queryParamsSchema: {
"type": "object",
"properties": {
"no_logs": {
"type": "boolean"
},
"no_code": {
"type": "boolean"
}
},
"required": []
},
bodySchema: undefined,
pathFieldRenames: undefined,
queryFieldRenames: undefined,
bodyFieldRenames: undefined
},
{
name: "getJobLogs",
description: "get job logs",
instructions: "",
path: "/w/{workspace}/jobs_u/get_logs/{id}",
method: "GET",
pathParamsSchema: {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
}
},
"required": [
"id"
]
},
queryParamsSchema: {
"type": "object",
"properties": {
"remove_ansi_warnings": {
"type": "boolean"
}
},
"required": []
@@ -1418,14 +1551,17 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"on_failure": {
"type": "string",
"nullable": true,
"description": "Path to a script or flow to run when the scheduled job fails"
},
"on_failure_times": {
"type": "number",
"nullable": true,
"description": "Number of consecutive failures before the on_failure handler is triggered (default 1)"
},
"on_failure_exact": {
"type": "boolean",
"nullable": true,
"description": "If true, trigger on_failure handler only on exactly N failures, not on every failure after N"
},
"on_failure_extra_args": {
@@ -1435,10 +1571,12 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"on_recovery": {
"type": "string",
"nullable": true,
"description": "Path to a script or flow to run when the schedule recovers after failures"
},
"on_recovery_times": {
"type": "number",
"nullable": true,
"description": "Number of consecutive successes before the on_recovery handler is triggered (default 1)"
},
"on_recovery_extra_args": {
@@ -1448,6 +1586,7 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"on_success": {
"type": "string",
"nullable": true,
"description": "Path to a script or flow to run after each successful execution"
},
"on_success_extra_args": {
@@ -1523,28 +1662,42 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"summary": {
"type": "string",
"nullable": true,
"description": "Short summary describing the purpose of this schedule"
},
"description": {
"type": "string",
"nullable": true,
"description": "Detailed description of what this schedule does"
},
"tag": {
"type": "string",
"nullable": true,
"description": "Worker tag to route jobs to specific worker groups"
},
"paused_until": {
"type": "string",
"nullable": true,
"format": "date-time",
"description": "ISO 8601 datetime until which the schedule is paused. Schedule resumes automatically after this time"
},
"cron_version": {
"type": "string",
"nullable": true,
"description": "Cron parser version. Use 'v2' for extended syntax with additional features"
},
"dynamic_skip": {
"type": "string",
"nullable": true,
"description": "Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)"
},
"permissioned_as": {
"type": "string",
"description": "The user or group this schedule runs as. Used during deployment to preserve the original schedule owner."
},
"preserve_permissioned_as": {
"type": "boolean",
"description": "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
}
},
"required": [
@@ -1596,14 +1749,17 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"on_failure": {
"type": "string",
"nullable": true,
"description": "Path to a script or flow to run when the scheduled job fails"
},
"on_failure_times": {
"type": "number",
"nullable": true,
"description": "Number of consecutive failures before the on_failure handler is triggered (default 1)"
},
"on_failure_exact": {
"type": "boolean",
"nullable": true,
"description": "If true, trigger on_failure handler only on exactly N failures, not on every failure after N"
},
"on_failure_extra_args": {
@@ -1613,10 +1769,12 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"on_recovery": {
"type": "string",
"nullable": true,
"description": "Path to a script or flow to run when the schedule recovers after failures"
},
"on_recovery_times": {
"type": "number",
"nullable": true,
"description": "Number of consecutive successes before the on_recovery handler is triggered (default 1)"
},
"on_recovery_extra_args": {
@@ -1626,6 +1784,7 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"on_success": {
"type": "string",
"nullable": true,
"description": "Path to a script or flow to run after each successful execution"
},
"on_success_extra_args": {
@@ -1701,28 +1860,44 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"summary": {
"type": "string",
"nullable": true,
"description": "Short summary describing the purpose of this schedule"
},
"description": {
"type": "string",
"nullable": true,
"description": "Detailed description of what this schedule does"
},
"tag": {
"type": "string",
"nullable": true,
"description": "Worker tag to route jobs to specific worker groups"
},
"paused_until": {
"type": "string",
"nullable": true,
"format": "date-time",
"description": "ISO 8601 datetime until which the schedule is paused. Schedule resumes automatically after this time"
},
"cron_version": {
"type": "string",
"nullable": true,
"description": "Cron parser version. Use 'v2' for extended syntax with additional features"
},
"dynamic_skip": {
"type": "string",
"nullable": true,
"description": "Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)"
},
"permissioned_as": {
"type": "string",
"nullable": true,
"description": "The user or group this schedule runs as (e.g., 'u/admin' or 'g/mygroup'). Only admins and wm_deployers can set this via preserve_permissioned_as."
},
"preserve_permissioned_as": {
"type": "boolean",
"nullable": true,
"description": "If true and user is admin/wm_deployers, preserve the provided permissioned_as instead of using the deploying user's identity"
}
},
"required": [
@@ -1805,7 +1980,7 @@ export const mcpEndpointTools: EndpointTool[] = [
},
"path": {
"type": "string",
"description": "filter by path"
"description": "filter by path (script path)"
},
"is_flow": {
"type": "boolean",
@@ -1814,6 +1989,22 @@ export const mcpEndpointTools: EndpointTool[] = [
"path_start": {
"type": "string",
"description": "filter schedules by path prefix"
},
"schedule_path": {
"type": "string",
"description": "exact match on the schedule's path"
},
"description": {
"type": "string",
"description": "pattern match filter for description field (case-insensitive)"
},
"summary": {
"type": "string",
"description": "pattern match filter for summary field (case-insensitive)"
},
"broad_filter": {
"type": "string",
"description": "broad search across multiple fields (case-insensitive substring match)"
}
},
"required": []