reorg mcp_tools

This commit is contained in:
Ruben Fiszel
2026-02-11 06:44:10 +00:00
parent 5e9b4cfa99
commit 20bb97fc1f
7 changed files with 119 additions and 126 deletions
+1 -3
View File
@@ -11,7 +11,6 @@ use crate::{
auth::OptTokened,
db::{ApiAuthed, DB},
jobs::RunJobQuery,
resources::get_resource_value_interpolated_internal,
users::{require_owner_of_path, OptAuthed},
utils::{check_scopes, WithStarredInfoQuery},
webhook_util::{WebhookMessage, WebhookShared},
@@ -68,6 +67,7 @@ use windmill_common::{
workspaces::{check_user_against_rule, ProtectionRuleKind, RuleCheckResult},
HUB_BASE_URL,
};
use windmill_store::resources::get_resource_value_interpolated_internal;
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::{push, PushArgs, PushArgsOwned, PushIsolationLevel};
@@ -1494,7 +1494,6 @@ async fn update_app(
let path = path.to_path();
check_scopes(&authed, || format!("apps:write:{}", path))?;
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
&w_id,
&ProtectionRuleKind::DisableDirectDeployment,
@@ -1539,7 +1538,6 @@ async fn update_app_raw<'a>(
));
}
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
&w_id,
&ProtectionRuleKind::DisableDirectDeployment,
+5 -1
View File
@@ -15,6 +15,7 @@ use crate::oauth2_oss::SlackVerifier;
use crate::smtp_server_oss::SmtpServer;
#[cfg(feature = "enterprise")]
use windmill_api_auth::ee_oss::ExternalJwks;
use windmill_store::resources::public_service;
#[cfg(feature = "mcp")]
use crate::mcp::{extract_and_store_workspace_id, setup_mcp_server};
@@ -192,6 +193,9 @@ mod workspaces;
pub mod workspaces_ee;
mod workspaces_export;
#[cfg(feature = "mcp")]
mod mcp_tools;
#[cfg(feature = "mcp")]
mod mcp;
#[cfg(all(feature = "mcp", feature = "private"))]
@@ -689,7 +693,7 @@ pub async fn run_server(
})
.nest(
"/w/:workspace_id/resources_u",
resources::public_service().layer(cors.clone()),
public_service().layer(cors.clone()),
)
.nest(
"/w/:workspace_id/capture_u",
+106
View File
@@ -0,0 +1,106 @@
use axum::{
extract::{Extension, Path},
Json,
};
use serde_json::value::RawValue;
use windmill_api_auth::{check_scopes, ApiAuthed};
use windmill_common::{
db::{UserDB, DB},
error::{Error, JsonResult, Result},
utils::{not_found_if_none, StripPath},
};
use windmill_store::resources::explain_resource_perm_error;
pub(crate) async fn get_mcp_tools(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Vec<serde_json::Value>> {
let path = path.to_path();
check_scopes(&authed, || format!("resources:read:{}", path))?;
let mut tx = user_db.clone().begin(&authed).await?;
let resource_value_o = sqlx::query_scalar!(
"SELECT value as \"value: sqlx::types::Json<Box<RawValue>>\" FROM resource WHERE path = $1 AND workspace_id = $2",
&path,
&w_id
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
if resource_value_o.is_none() {
explain_resource_perm_error(&path, &w_id, &db, &authed).await?;
}
let resource_value = not_found_if_none(resource_value_o, "Resource", path)?
.ok_or_else(|| Error::BadRequest(format!("Empty resource value for {}", path)))?;
let mcp_resource = serde_json::from_str::<windmill_mcp::McpResource>(resource_value.0.get())
.map_err(|e| Error::BadRequest(format!("Failed to parse MCP resource: {}", e)))?;
#[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:");
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
);
}
}
}
}
}
let client = windmill_mcp::McpClient::from_resource(mcp_resource, &db, &w_id)
.await
.map_err(|e| Error::ExecutionErr(format!("Failed to connect to MCP server: {}", e)))?;
let tools: Vec<serde_json::Value> = client
.available_tools()
.iter()
.map(|tool| {
serde_json::to_value(tool)
.map_err(|e| Error::ExecutionErr(format!("Failed to serialize MCP tool: {}", e)))
})
.collect::<Result<Vec<_>>>()?;
if let Err(e) = client.shutdown().await {
tracing::warn!("Failed to shutdown MCP client: {}", e);
}
Ok(Json(tools))
}
+1 -1
View File
@@ -20,10 +20,10 @@ use windmill_common::{
utils::{deserialize_url, empty_as_none, is_empty, RunnableKind},
DB,
};
use windmill_store::resources::try_get_resource_from_db_as;
use crate::{
db::ApiAuthed,
resources::try_get_resource_from_db_as,
triggers::http::{
http_trigger_auth::ApiKeyAuthentication, AuthenticationMethod, HttpMethod, RequestType,
},
+3 -114
View File
@@ -1,22 +1,5 @@
// Re-export everything from windmill-store
pub use windmill_store::resources::*;
#[cfg(feature = "mcp")]
use axum::{ extract::{Extension, Path}, routing::get,
Json, Router,
};
#[cfg(feature = "mcp")]
use serde_json::value::RawValue;
#[cfg(feature = "mcp")]
use windmill_api_auth::{check_scopes, ApiAuthed};
#[cfg(feature = "mcp")]
use windmill_common::{
db::{UserDB, DB},
error::{Error, JsonResult, Result},
utils::{not_found_if_none, StripPath},
};
#[cfg(not(feature = "mcp"))]
use axum::routing::get;
use axum::Router;
/// Wraps the subcrate's workspaced_service with the mcp_tools route
@@ -24,104 +7,10 @@ use axum::Router;
pub fn workspaced_service() -> Router {
let router = windmill_store::resources::workspaced_service();
#[cfg(feature = "mcp")]
use crate::mcp_tools::get_mcp_tools;
#[cfg(feature = "mcp")]
let router = router.route("/mcp_tools/*path", get(get_mcp_tools));
router
}
/// Get list of tools from an MCP resource
#[cfg(feature = "mcp")]
async fn get_mcp_tools(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Vec<serde_json::Value>> {
let path = path.to_path();
check_scopes(&authed, || format!("resources:read:{}", path))?;
let mut tx = user_db.clone().begin(&authed).await?;
let resource_value_o = sqlx::query_scalar!(
"SELECT value as \"value: sqlx::types::Json<Box<RawValue>>\" FROM resource WHERE path = $1 AND workspace_id = $2",
&path,
&w_id
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
if resource_value_o.is_none() {
explain_resource_perm_error(&path, &w_id, &db, &authed).await?;
}
let resource_value = not_found_if_none(resource_value_o, "Resource", path)?
.ok_or_else(|| Error::BadRequest(format!("Empty resource value for {}", path)))?;
let mcp_resource = serde_json::from_str::<windmill_mcp::McpResource>(resource_value.0.get())
.map_err(|e| Error::BadRequest(format!("Failed to parse MCP resource: {}", e)))?;
#[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:");
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
);
}
}
}
}
}
let client = windmill_mcp::McpClient::from_resource(mcp_resource, &db, &w_id)
.await
.map_err(|e| Error::ExecutionErr(format!("Failed to connect to MCP server: {}", e)))?;
let tools: Vec<serde_json::Value> = client
.available_tools()
.iter()
.map(|tool| {
serde_json::to_value(tool)
.map_err(|e| Error::ExecutionErr(format!("Failed to serialize MCP tool: {}", e)))
})
.collect::<Result<Vec<_>>>()?;
if let Err(e) = client.shutdown().await {
tracing::warn!("Failed to shutdown MCP client: {}", e);
}
Ok(Json(tools))
}
@@ -6,7 +6,6 @@ use crate::{
auth::{AuthCache, OptTokened},
db::{ApiAuthed, DB},
jobs::start_job_update_sse_stream,
resources::try_get_resource_from_db_as,
triggers::trigger_helpers::{
get_runnable_format, trigger_runnable, trigger_runnable_and_wait_for_result,
trigger_runnable_inner, RunnableId,
@@ -30,6 +29,7 @@ use windmill_common::{
triggers::{TriggerKind, TriggerMetadata},
utils::{not_found_if_none, StripPath},
};
use windmill_store::resources::try_get_resource_from_db_as;
use windmill_trigger::TriggerMode;
#[cfg(feature = "parquet")]
@@ -10,12 +10,7 @@ use std::collections::HashMap;
use crate::db::ApiAuthed;
use crate::{
apps::AppWithLastVersion,
db::DB,
folders::Folder,
resources::{Resource, ResourceType},
};
use crate::{apps::AppWithLastVersion, db::DB, folders::Folder};
#[cfg(any(
feature = "http_trigger",
@@ -64,6 +59,7 @@ use serde_json::Value;
use tempfile::TempDir;
use tokio::fs::File;
use tokio_util::io::ReaderStream;
use windmill_store::resources::{Resource, ResourceType};
#[derive(Serialize)]
struct ScriptMetadata {