diff --git a/backend/.sqlx/query-5840477599957b528e10b638e0616b6fb9d04b78271b827c21a399a1474627d7.json b/backend/.sqlx/query-5840477599957b528e10b638e0616b6fb9d04b78271b827c21a399a1474627d7.json new file mode 100644 index 0000000000..d87380577a --- /dev/null +++ b/backend/.sqlx/query-5840477599957b528e10b638e0616b6fb9d04b78271b827c21a399a1474627d7.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n variable.path,\n variable.account as account_id,\n (now() > account.expires_at) as \"is_expired: bool\"\n FROM variable\n LEFT JOIN account ON variable.account = account.id AND account.workspace_id = $2\n WHERE variable.path = $1 AND variable.workspace_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "account_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "is_expired: bool", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + true, + null + ] + }, + "hash": "5840477599957b528e10b638e0616b6fb9d04b78271b827c21a399a1474627d7" +} diff --git a/backend/.sqlx/query-bbc28b92ae8ec3d120a8976be7d3966282fe6543e0eb957fc10864dbf58de58f.json b/backend/.sqlx/query-b1bd088c2e1aca3104bede7d0953369b6b17ad3ad62692ae6f2303be890e6391.json similarity index 68% rename from backend/.sqlx/query-bbc28b92ae8ec3d120a8976be7d3966282fe6543e0eb957fc10864dbf58de58f.json rename to backend/.sqlx/query-b1bd088c2e1aca3104bede7d0953369b6b17ad3ad62692ae6f2303be890e6391.json index e3f60d4a5c..bda29ecd5f 100644 --- a/backend/.sqlx/query-bbc28b92ae8ec3d120a8976be7d3966282fe6543e0eb957fc10864dbf58de58f.json +++ b/backend/.sqlx/query-b1bd088c2e1aca3104bede7d0953369b6b17ad3ad62692ae6f2303be890e6391.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO account (workspace_id, client, expires_at, refresh_token, grant_type, cc_client_id, cc_client_secret, cc_token_url) VALUES ($1, $2, now() + ($3 || ' seconds')::interval, $4, $5, $6, $7, $8) RETURNING id", + "query": "INSERT INTO account (workspace_id, client, expires_at, refresh_token, grant_type, cc_client_id, cc_client_secret, cc_token_url, mcp_server_url) VALUES ($1, $2, now() + ($3 || ' seconds')::interval, $4, $5, $6, $7, $8, $9) RETURNING id", "describe": { "columns": [ { @@ -18,12 +18,13 @@ "Varchar", "Varchar", "Varchar", - "Varchar" + "Varchar", + "Text" ] }, "nullable": [ false ] }, - "hash": "bbc28b92ae8ec3d120a8976be7d3966282fe6543e0eb957fc10864dbf58de58f" + "hash": "b1bd088c2e1aca3104bede7d0953369b6b17ad3ad62692ae6f2303be890e6391" } diff --git a/backend/.sqlx/query-b4fc94adfe55bb87d2c4b6b45ed2eb5ac25e84f3619a0b403a2d619a0eb51432.json b/backend/.sqlx/query-b4fc94adfe55bb87d2c4b6b45ed2eb5ac25e84f3619a0b403a2d619a0eb51432.json new file mode 100644 index 0000000000..61d48bd904 --- /dev/null +++ b/backend/.sqlx/query-b4fc94adfe55bb87d2c4b6b45ed2eb5ac25e84f3619a0b403a2d619a0eb51432.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n variable.account as account_id,\n (now() > account.expires_at) as \"is_expired: bool\"\n FROM variable\n LEFT JOIN account ON variable.account = account.id AND account.workspace_id = $2\n WHERE variable.path = $1 AND variable.workspace_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "account_id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "is_expired: bool", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + true, + null + ] + }, + "hash": "b4fc94adfe55bb87d2c4b6b45ed2eb5ac25e84f3619a0b403a2d619a0eb51432" +} diff --git a/backend/.sqlx/query-e26ccc6607a9c78c1a8c1fd7b3bec931cf0ed27f79f852ae7f63a0ed6e12042f.json b/backend/.sqlx/query-e26ccc6607a9c78c1a8c1fd7b3bec931cf0ed27f79f852ae7f63a0ed6e12042f.json new file mode 100644 index 0000000000..53a30348d5 --- /dev/null +++ b/backend/.sqlx/query-e26ccc6607a9c78c1a8c1fd7b3bec931cf0ed27f79f852ae7f63a0ed6e12042f.json @@ -0,0 +1,59 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT client, refresh_token, grant_type, cc_client_id, cc_client_secret, cc_token_url, mcp_server_url FROM account WHERE workspace_id = $1 AND id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "client", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "refresh_token", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "grant_type", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "cc_client_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "cc_client_secret", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "cc_token_url", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "mcp_server_url", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + true, + true, + true, + true + ] + }, + "hash": "e26ccc6607a9c78c1a8c1fd7b3bec931cf0ed27f79f852ae7f63a0ed6e12042f" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 4f160bc4e9..1d3e934541 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -10944,6 +10944,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", + "oauth2", "pastey", "pin-project-lite", "rand 0.9.0", @@ -10959,6 +10960,7 @@ dependencies = [ "tokio-util", "tower-service", "tracing", + "url", "uuid", ] @@ -15561,6 +15563,7 @@ name = "windmill-mcp" version = "1.605.0" dependencies = [ "anyhow", + "oauth2", "reqwest 0.12.28", "rmcp", "serde", diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index cc8096628a..d2edc86dd8 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -27dc729e842c200c6b097dc69d99b6a0927465f2 \ No newline at end of file +6262e373ede45b5b8d45505a63ff73968c04336a \ No newline at end of file diff --git a/backend/migrations/20260112102339_mcp_oauth_client.down.sql b/backend/migrations/20260112102339_mcp_oauth_client.down.sql new file mode 100644 index 0000000000..90a59a24b1 --- /dev/null +++ b/backend/migrations/20260112102339_mcp_oauth_client.down.sql @@ -0,0 +1,3 @@ +DROP INDEX IF EXISTS idx_account_mcp_server_url; +ALTER TABLE account DROP COLUMN IF EXISTS mcp_server_url; +DROP TABLE IF EXISTS mcp_oauth_client; diff --git a/backend/migrations/20260112102339_mcp_oauth_client.up.sql b/backend/migrations/20260112102339_mcp_oauth_client.up.sql new file mode 100644 index 0000000000..17f891f271 --- /dev/null +++ b/backend/migrations/20260112102339_mcp_oauth_client.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE mcp_oauth_client ( + mcp_server_url TEXT PRIMARY KEY, + client_id TEXT NOT NULL, + client_secret TEXT, + client_secret_expires_at TIMESTAMP, + token_endpoint TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX idx_mcp_oauth_client_expires ON mcp_oauth_client(client_secret_expires_at); + +ALTER TABLE account ADD COLUMN mcp_server_url TEXT; +CREATE INDEX idx_account_mcp_server_url ON account(mcp_server_url) WHERE mcp_server_url IS NOT NULL; diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index 1a7ceda958..08845d1e05 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -36,7 +36,7 @@ sqs_trigger = ["dep:aws-sdk-sqs", "dep:aws-sdk-sts", "dep:aws-sdk-sso", "dep:aws deno_core = ["dep:deno_core", "dep:deno_error"] gcp_trigger = ["dep:thiserror", "dep:google-cloud-pubsub", "dep:google-cloud-googleapis", "dep:tonic"] cloud = ["windmill-common/cloud"] -mcp = ["dep:windmill-mcp", "windmill-mcp/server"] +mcp = ["dep:windmill-mcp", "windmill-mcp/server", "windmill-mcp/auth"] python = [] [dependencies] @@ -158,5 +158,6 @@ tonic = { workspace = true, optional = true } deno_error = { workspace = true, optional = true } deno_core = { workspace = true, optional = true } backon = {workspace = true, optional = true} + [build-dependencies] deno_core = { workspace = true, optional = true } diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 1e3abd5c5f..9ced1000e5 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -4224,6 +4224,9 @@ paths: cc_token_url: type: string description: "OAuth token URL override for resource-level authentication (client_credentials flow only)" + mcp_server_url: + type: string + description: "MCP server URL for MCP OAuth token refresh" required: - refresh_token - expires_in @@ -15842,6 +15845,98 @@ paths: items: $ref: "#/components/schemas/EndpointTool" + /mcp/oauth/discover: + post: + summary: discover MCP server OAuth metadata + operationId: discoverMcpOAuth + tags: + - mcp_oauth + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - mcp_server_url + properties: + mcp_server_url: + type: string + description: URL of the MCP server to discover OAuth metadata from + responses: + "200": + description: OAuth metadata from MCP server + content: + application/json: + schema: + type: object + properties: + scopes_supported: + type: array + items: + type: string + authorization_endpoint: + type: string + token_endpoint: + type: string + registration_endpoint: + type: string + supports_dynamic_registration: + type: boolean + + /mcp/oauth/start: + get: + summary: start MCP OAuth popup flow + description: Opens in a popup, discovers OAuth metadata, registers client, and redirects to OAuth provider + operationId: startMcpOAuthPopup + tags: + - mcp_oauth + parameters: + - name: mcp_server_url + in: query + required: true + schema: + type: string + description: URL of the MCP server to connect to + - name: scopes + in: query + required: false + schema: + type: string + description: Comma-separated list of OAuth scopes to request + responses: + "302": + description: Redirect to OAuth provider authorization URL + + /mcp/oauth/callback: + get: + security: [] + summary: MCP OAuth callback + description: Handles OAuth callback, exchanges code for tokens, returns HTML that posts message to opener + operationId: mcpOAuthCallback + tags: + - mcp_oauth + parameters: + - name: code + in: query + required: true + schema: + type: string + description: OAuth authorization code + - name: state + in: query + required: true + schema: + type: string + description: CSRF state token + responses: + "200": + description: HTML page with JavaScript that posts tokens to opener window and closes + content: + text/html: + schema: + type: string + components: securitySchemes: bearerAuth: diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index cb344cb6fc..a46f37a811 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -191,6 +191,10 @@ mod workspaces_oss; #[cfg(feature = "mcp")] mod mcp; +#[cfg(all(feature = "mcp", feature = "private"))] +mod mcp_oauth_ee; +#[cfg(feature = "mcp")] +mod mcp_oauth_oss; pub use apps::EditApp; pub const DEFAULT_BODY_LIMIT: usize = 2097152 * 100; // 200MB @@ -668,6 +672,15 @@ pub async fn run_server( #[cfg(not(feature = "oauth2"))] Router::new() }) + .nest("/mcp/oauth", { + #[cfg(feature = "mcp")] + { + mcp_oauth_oss::global_service() + } + + #[cfg(not(feature = "mcp"))] + Router::new() + }) .nest("/r", { #[cfg(feature = "http_trigger")] { diff --git a/backend/windmill-api/src/mcp_oauth_oss.rs b/backend/windmill-api/src/mcp_oauth_oss.rs new file mode 100644 index 0000000000..5162e79e8c --- /dev/null +++ b/backend/windmill-api/src/mcp_oauth_oss.rs @@ -0,0 +1,115 @@ +// Re-export from EE when private feature is enabled +#[cfg(feature = "private")] +pub use crate::mcp_oauth_ee::*; + +// OSS stub implementations when private feature is not enabled +#[cfg(not(feature = "private"))] +mod oss_impl { + use axum::{ + extract::Query, + response::{Html, Redirect}, + routing::{get, post}, + Json, Router, + }; + use serde::{Deserialize, Serialize}; + use windmill_common::error::{self, JsonResult}; + + /// Global routes for MCP OAuth (OSS stub - returns errors) + pub fn global_service() -> Router { + Router::new() + .route("/discover", post(discover_mcp_oauth)) + .route("/start", get(start_mcp_oauth)) + .route("/callback", get(mcp_oauth_callback)) + .route("/client-metadata.json", get(get_client_metadata)) + } + + #[derive(Serialize)] + pub struct ClientMetadata { + pub client_name: &'static str, + pub redirect_uris: Vec, + pub grant_types: Vec<&'static str>, + pub response_types: Vec<&'static str>, + pub token_endpoint_auth_method: &'static str, + } + + pub async fn get_client_metadata() -> Json { + Json(ClientMetadata { + client_name: "Windmill", + redirect_uris: vec![], + grant_types: vec!["authorization_code", "refresh_token"], + response_types: vec!["code"], + token_endpoint_auth_method: "none", + }) + } + + #[derive(Deserialize)] + #[allow(dead_code)] + pub struct DiscoverRequest { + pub mcp_server_url: String, + } + + #[derive(Serialize)] + pub struct DiscoverResponse { + pub scopes_supported: Option>, + pub authorization_endpoint: String, + pub token_endpoint: String, + pub registration_endpoint: Option, + pub supports_dynamic_registration: bool, + } + + pub async fn discover_mcp_oauth( + Json(_req): Json, + ) -> JsonResult { + Err(error::Error::BadRequest( + "Not implemented in Windmill's Open Source repository".to_string(), + )) + } + + #[derive(Deserialize)] + #[allow(dead_code)] + pub struct StartPopupParams { + pub mcp_server_url: String, + #[serde(default)] + pub scopes: Option, + } + + pub async fn start_mcp_oauth( + Query(_params): Query, + ) -> Result { + Err(error::Error::BadRequest( + "Not implemented in Windmill's Open Source repository".to_string(), + )) + } + + #[derive(Deserialize)] + #[allow(dead_code)] + pub struct CallbackParams { + pub code: String, + pub state: String, + } + + pub async fn mcp_oauth_callback( + Query(_params): Query, + ) -> Result, error::Error> { + let html = r#" + +MCP OAuth Error + + +

Not implemented in Windmill's Open Source repository

+ +"#; + Ok(Html(html.to_string())) + } +} + +#[cfg(not(feature = "private"))] +pub use oss_impl::*; diff --git a/backend/windmill-api/src/public_app_layer.rs b/backend/windmill-api/src/public_app_layer.rs index cce8af1b35..8a55e61289 100644 --- a/backend/windmill-api/src/public_app_layer.rs +++ b/backend/windmill-api/src/public_app_layer.rs @@ -68,6 +68,7 @@ fn is_public_route_whitelisted(path: &str) -> bool { "/api/oauth/login/*", "/api/oauth/connect/*", "/oauth/callback/*", + "/api/mcp/oauth/callback", "/user/login_callback/*", "/api/workspaces/users", "/api/users/whoami", diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index b7127a4907..f1f9c471ec 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -1414,7 +1414,7 @@ async fn get_mcp_tools( let path = path.to_path(); check_scopes(&authed, || format!("resources:read:{}", path))?; - let mut tx = user_db.begin(&authed).await?; + let mut tx = user_db.clone().begin(&authed).await?; // Fetch the MCP resource from database let resource_value_o = sqlx::query_scalar!( @@ -1435,9 +1435,53 @@ async fn get_mcp_tools( .ok_or_else(|| Error::BadRequest(format!("Empty resource value for {}", path)))?; // Parse MCP resource - let mcp_resource = - serde_json::from_str::(resource_value.0.get()) - .map_err(|e| Error::BadRequest(format!("Failed to parse MCP resource: {}", e)))?; + let mcp_resource = serde_json::from_str::(resource_value.0.get()) + .map_err(|e| Error::BadRequest(format!("Failed to parse MCP resource: {}", e)))?; + + // Check if token needs refresh before creating MCP client + #[cfg(feature = "oauth2")] + { + tracing::info!("Checking if token needs refresh before creating MCP client"); + if let Some(ref token_path) = mcp_resource.token { + let token_var_path = token_path.trim_start_matches("$var:"); + + // Query to check if token is expired + let token_info = sqlx::query!( + r#" + SELECT + variable.account as account_id, + (now() > account.expires_at) as "is_expired: bool" + FROM variable + LEFT JOIN account ON variable.account = account.id AND account.workspace_id = $2 + WHERE variable.path = $1 AND variable.workspace_id = $2 + "#, + token_var_path, + &w_id + ) + .fetch_optional(&db) + .await?; + + if let Some(info) = token_info { + if let (Some(account_id), Some(true)) = (info.account_id, info.is_expired) { + let refresh_tx = user_db.begin(&authed).await?; + if let Err(e) = crate::oauth2_oss::_refresh_token( + refresh_tx, + token_var_path, + &w_id, + account_id, + &db, + ) + .await + { + tracing::warn!( + "Failed to refresh token for MCP resource: {}. Proceeding with possibly expired token.", + e + ); + } + } + } + } + } // Create MCP client connection let client = windmill_mcp::McpClient::from_resource(mcp_resource, &db, &w_id) diff --git a/backend/windmill-mcp/Cargo.toml b/backend/windmill-mcp/Cargo.toml index e09e665ff8..a3e4bfed5f 100644 --- a/backend/windmill-mcp/Cargo.toml +++ b/backend/windmill-mcp/Cargo.toml @@ -11,8 +11,10 @@ path = "src/lib.rs" [features] default = [] server = ["rmcp/transport-streamable-http-server", "rmcp/transport-streamable-http-server-session", "rmcp/transport-worker"] +auth = ["rmcp/auth", "dep:oauth2"] [dependencies] +oauth2 = { version = "5.0", optional = true } windmill-common = { workspace = true, default-features = false } anyhow.workspace = true reqwest = { version = "=0.12", features = ["json", "stream", "gzip"] } diff --git a/backend/windmill-mcp/src/lib.rs b/backend/windmill-mcp/src/lib.rs index a6a93e4770..eb3df05b08 100644 --- a/backend/windmill-mcp/src/lib.rs +++ b/backend/windmill-mcp/src/lib.rs @@ -47,6 +47,20 @@ pub mod server { pub use rmcp::ErrorData; } +// Re-export rmcp auth types when auth feature is enabled +#[cfg(feature = "auth")] +pub mod oauth { + //! Re-exports of rmcp auth and oauth2 types for MCP OAuth implementations + + pub use rmcp::transport::auth::AuthorizationManager; + + // Re-export oauth2 types needed for MCP OAuth flow + pub use oauth2::{ + basic::BasicClient, AuthUrl, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, + RedirectUrl, Scope, TokenUrl, + }; +} + use std::collections::HashMap; use serde::{Deserialize, Serialize}; diff --git a/backend/windmill-worker/src/ai/utils.rs b/backend/windmill-worker/src/ai/utils.rs index 41da22c6a9..5f017a4e75 100644 --- a/backend/windmill-worker/src/ai/utils.rs +++ b/backend/windmill-worker/src/ai/utils.rs @@ -451,6 +451,80 @@ fn apply_tool_filters( tools } +/// Check if a token variable is expired and refresh it if needed via API call +#[cfg(feature = "mcp")] +async fn refresh_token_if_expired( + db: &DB, + workspace_id: &str, + token_path: &str, + auth_token: &str, +) -> Result<(), Error> { + // Query variable with account join to check expiration + let token_info = sqlx::query!( + r#" + SELECT + variable.path, + variable.account as account_id, + (now() > account.expires_at) as "is_expired: bool" + FROM variable + LEFT JOIN account ON variable.account = account.id AND account.workspace_id = $2 + WHERE variable.path = $1 AND variable.workspace_id = $2 + "#, + token_path, + workspace_id + ) + .fetch_optional(db) + .await?; + + let Some(token_info) = token_info else { + return Ok(()); + }; + + let Some(account_id) = token_info.account_id else { + return Ok(()); + }; + + if !token_info.is_expired.unwrap_or(false) { + return Ok(()); + } + + tracing::debug!( + "Token variable {} is expired, triggering refresh", + token_path + ); + + // Call the API refresh endpoint + let base_url = windmill_common::BASE_URL.read().await.clone(); + let refresh_url = format!( + "{}/api/w/{}/oauth/refresh_token/{}", + base_url, workspace_id, account_id + ); + + #[derive(serde::Serialize)] + struct RefreshRequest { + path: String, + } + + let response = windmill_common::utils::HTTP_CLIENT + .post(&refresh_url) + .header("Authorization", format!("Bearer {}", auth_token)) + .json(&RefreshRequest { path: token_path.to_string() }) + .send() + .await + .map_err(|e| { + Error::internal_err(format!("Failed to call token refresh endpoint: {}", e)) + })?; + + if !response.status().is_success() { + let error_text = response.text().await.unwrap_or_default(); + return Err(Error::internal_err(format!( + "Token refresh failed: {}", + error_text + ))); + } + Ok(()) +} + /// Load tools from MCP servers and return both the clients and tools /// Returns a map of resource name -> client, and a vector of tools #[cfg(feature = "mcp")] @@ -458,6 +532,7 @@ pub async fn load_mcp_tools( db: &DB, workspace_id: &str, mcp_configs: Vec, + auth_token: &str, ) -> Result<(HashMap>, Vec), Error> { let mut all_mcp_tools = Vec::new(); let mut mcp_clients = HashMap::new(); @@ -484,6 +559,19 @@ pub async fn load_mcp_tools( let resource_name = mcp_resource.name.clone(); + // Check if token needs refresh before creating MCP client + if let Some(ref token_path) = mcp_resource.token { + let token_var_path = token_path.trim_start_matches("$var:"); + if let Err(e) = + refresh_token_if_expired(db, workspace_id, token_var_path, auth_token).await + { + tracing::warn!( + "Failed to refresh token for MCP resource {}: {}. Proceeding with possibly expired token.", + resource_name, e + ); + } + } + // Create new MCP client for this execution tracing::debug!("Creating fresh MCP client for {}", resource_name); let client = McpClient::from_resource(mcp_resource, db, workspace_id) @@ -561,6 +649,7 @@ pub async fn load_mcp_tools( _db: &DB, _workspace_id: &str, _mcp_configs: Vec, + _auth_token: &str, ) -> Result<(HashMap>, Vec), Error> { Ok((HashMap::new(), Vec::new())) } diff --git a/backend/windmill-worker/src/ai_executor.rs b/backend/windmill-worker/src/ai_executor.rs index 9e981556b1..f8c51e5675 100644 --- a/backend/windmill-worker/src/ai_executor.rs +++ b/backend/windmill-worker/src/ai_executor.rs @@ -315,7 +315,7 @@ pub async fn handle_ai_agent_job( let mut tools = tools; let mcp_clients = if !mcp_configs.is_empty() { - let (clients, mcp_tools) = load_mcp_tools(db, &job.workspace_id, mcp_configs).await?; + let (clients, mcp_tools) = load_mcp_tools(db, &job.workspace_id, mcp_configs, &client.token).await?; tools.extend(mcp_tools); clients } else { diff --git a/frontend/src/lib/components/Path.svelte b/frontend/src/lib/components/Path.svelte index dafd4320af..4d6c73fcd8 100644 --- a/frontend/src/lib/components/Path.svelte +++ b/frontend/src/lib/components/Path.svelte @@ -394,7 +394,7 @@
-
+
{#if meta != undefined} {#if !hideUser} @@ -462,7 +462,7 @@ {/if}
/
-