mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 16:02:14 +00:00
reorg mcp_tools
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user