mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 00:02:23 +00:00
feat(mcp): allow filtering by folder (#6366)
* allow filtering mcp by folder * nit * add error if wrong format
This commit is contained in:
@@ -242,7 +242,7 @@ impl Runner {
|
||||
|| scopes
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|scope| scope != "mcp:all" && scope != "mcp:favorites" && !scope.starts_with("mcp:hub:"))
|
||||
.all(|scope| !scope.starts_with("mcp:all") && !scope.starts_with("mcp:favorites") && !scope.starts_with("mcp:hub:"))
|
||||
{
|
||||
tracing::error!("Unauthorized: missing mcp scope");
|
||||
return Err(Error::internal_error("Unauthorized: missing mcp scope".to_string(), None));
|
||||
@@ -405,6 +405,7 @@ impl Runner {
|
||||
workspace_id: &str,
|
||||
scope_type: &str,
|
||||
item_type: &str,
|
||||
scope_path: Option<&str>,
|
||||
) -> Result<Vec<T>, Error> {
|
||||
let mut sqlb = SqlBuilder::select_from(&format!("{} as o", item_type));
|
||||
let fields = vec!["o.path", "o.summary", "o.description", "o.schema"];
|
||||
@@ -422,6 +423,17 @@ impl Runner {
|
||||
sqlb.and_where("(o.no_main_func IS NOT TRUE OR o.no_main_func IS NULL)");
|
||||
}
|
||||
|
||||
// scope path is always a folder path, format is f/my_folder/*
|
||||
if let Some(scope_path) = scope_path {
|
||||
if scope_path.split("/").count() != 3 || !scope_path.starts_with("f/") || !scope_path.ends_with("/*") {
|
||||
return Err(Error::internal_error(
|
||||
format!("Invalid folder format: {}, expected format is f/my_folder/*", scope_path),
|
||||
None,
|
||||
));
|
||||
}
|
||||
sqlb.and_where_like_left("o.path", &scope_path[..scope_path.len() - 2]);
|
||||
}
|
||||
|
||||
sqlb.order_by(
|
||||
if item_type == "flow" {
|
||||
"o.edited_at"
|
||||
@@ -1011,9 +1023,9 @@ impl ServerHandler for Runner {
|
||||
.find(|scope| scope.starts_with("mcp:") && !scope.contains("hub"))
|
||||
});
|
||||
let hub_scope = scopes.and_then(|scopes| scopes.iter().find(|scope| scope.starts_with("mcp:hub")));
|
||||
let scope_type = owned_scope.map_or("all", |scope| {
|
||||
let (scope_type, scope_path) = owned_scope.map_or(("all", None), |scope| {
|
||||
let parts = scope.split(":").collect::<Vec<&str>>();
|
||||
parts[1]
|
||||
(parts[1], if parts.len() == 3 { Some(parts[2]) } else { None })
|
||||
});
|
||||
let scope_integrations = hub_scope.and_then(|scope| {
|
||||
let parts = scope.split(":").collect::<Vec<&str>>();
|
||||
@@ -1030,9 +1042,10 @@ impl ServerHandler for Runner {
|
||||
&workspace_id,
|
||||
scope_type,
|
||||
"script",
|
||||
scope_path.as_deref(),
|
||||
);
|
||||
let flows_fn =
|
||||
Runner::inner_get_items::<FlowInfo>(user_db, authed, &workspace_id, scope_type, "flow");
|
||||
Runner::inner_get_items::<FlowInfo>(user_db, authed, &workspace_id, scope_type, "flow", scope_path.as_deref());
|
||||
let resources_types_fn = Runner::inner_get_resources_types(user_db, authed, &workspace_id);
|
||||
let hub_scripts_fn = Runner::inner_get_scripts_from_hub(db, scope_integrations.as_deref());
|
||||
let (scripts, flows, resources_types, hub_scripts) = if scope_integrations.is_some() {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import TokenDisplay from './TokenDisplay.svelte'
|
||||
import ScopeSelector from './ScopeSelector.svelte'
|
||||
import Alert from '../common/alert/Alert.svelte'
|
||||
import FolderPicker from '../FolderPicker.svelte'
|
||||
|
||||
interface Props {
|
||||
showMcpMode?: boolean
|
||||
@@ -42,6 +43,7 @@
|
||||
let allApps = $state<string[]>([])
|
||||
let loadingRunnables = $state(false)
|
||||
let includedRunnables = $state<string[]>([])
|
||||
let selectedFolder = $state<string>('')
|
||||
|
||||
let runnablesCache = new Map<string, string[]>()
|
||||
|
||||
@@ -71,7 +73,12 @@
|
||||
|
||||
let tokenScopes = scopes
|
||||
if (mcpMode) {
|
||||
tokenScopes = [`mcp:${newMcpScope}`]
|
||||
if (newMcpScope === 'folder') {
|
||||
const folderPath = `f/${selectedFolder}/*`
|
||||
tokenScopes = [`mcp:all:${folderPath}`]
|
||||
} else {
|
||||
tokenScopes = [`mcp:${newMcpScope}`]
|
||||
}
|
||||
if (newMcpApps.length > 0) {
|
||||
tokenScopes.push(`mcp:hub:${newMcpApps.join(',')}`)
|
||||
}
|
||||
@@ -134,30 +141,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function getScripts(favoriteOnly: boolean = false, workspace: string) {
|
||||
async function getScripts(favoriteOnly: boolean = false, workspace: string, folder: string | undefined) {
|
||||
if (!workspace) {
|
||||
return []
|
||||
}
|
||||
const pathStart = folder ? `f/${folder}` : undefined
|
||||
const scripts = await ScriptService.listScripts({
|
||||
starredOnly: favoriteOnly,
|
||||
workspace
|
||||
workspace,
|
||||
pathStart
|
||||
})
|
||||
return scripts.map((x) => x.path)
|
||||
}
|
||||
|
||||
async function getFlows(favoriteOnly: boolean = false, workspace: string) {
|
||||
async function getFlows(favoriteOnly: boolean = false, workspace: string, folder: string | undefined) {
|
||||
if (!workspace) {
|
||||
return []
|
||||
}
|
||||
const pathStart = folder ? `f/${folder}` : undefined
|
||||
const flows = await FlowService.listFlows({
|
||||
starredOnly: favoriteOnly,
|
||||
workspace
|
||||
workspace,
|
||||
pathStart
|
||||
})
|
||||
return flows.map((x) => x.path)
|
||||
}
|
||||
|
||||
async function getScriptsAndFlows(favoriteOnly: boolean = false, workspace: string) {
|
||||
const cacheKey = `${workspace}-${favoriteOnly}`
|
||||
async function getScriptsAndFlows(favoriteOnly: boolean = false, workspace: string, folder: string | undefined) {
|
||||
const cacheKey = `${workspace}-${favoriteOnly}${folder ? `-${folder}` : ''}`
|
||||
if (runnablesCache.has(cacheKey)) {
|
||||
includedRunnables = runnablesCache.get(cacheKey) || []
|
||||
return
|
||||
@@ -165,7 +176,7 @@
|
||||
|
||||
try {
|
||||
loadingRunnables = true
|
||||
const [scripts, flows] = await Promise.all([getScripts(favoriteOnly, workspace), getFlows(favoriteOnly, workspace)])
|
||||
const [scripts, flows] = await Promise.all([getScripts(favoriteOnly, workspace, folder), getFlows(favoriteOnly, workspace, folder)])
|
||||
const combined = [...scripts, ...flows]
|
||||
runnablesCache.set(cacheKey, combined)
|
||||
includedRunnables = combined
|
||||
@@ -178,12 +189,19 @@
|
||||
if (mcpCreationMode) {
|
||||
const workspace = newTokenWorkspace || $workspaceStore
|
||||
if (workspace) {
|
||||
getScriptsAndFlows(newMcpScope === 'favorites', workspace)
|
||||
const folderParam = selectedFolder.length > 0 ? selectedFolder : undefined
|
||||
getScriptsAndFlows(newMcpScope === 'favorites', workspace, folderParam)
|
||||
}
|
||||
} else {
|
||||
includedRunnables = []
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (mcpCreationMode && newMcpScope !== 'folder') {
|
||||
selectedFolder = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -270,10 +288,23 @@
|
||||
label="All scripts/flows"
|
||||
tooltip="Make all your scripts and flows available as tools"
|
||||
/>
|
||||
<ToggleButton
|
||||
{item}
|
||||
value="folder"
|
||||
label="Folder"
|
||||
tooltip="Make all scripts and flows in the selected folder available as tools"
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
{#if newMcpScope === 'folder'}
|
||||
<div>
|
||||
<span class="block mb-1">Select Folder</span>
|
||||
<FolderPicker bind:folderName={selectedFolder} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<span class="block mb-1">Hub scripts (optional)</span>
|
||||
{#if loadingApps}
|
||||
@@ -321,7 +352,9 @@
|
||||
<option value={90 * 24 * 60 * 60}>90d</option>
|
||||
</select>
|
||||
</div>
|
||||
{:else if loadingRunnables}
|
||||
{/if}
|
||||
{#if mcpCreationMode && (newMcpScope !== 'folder' || selectedFolder.length > 0)}
|
||||
{#if loadingRunnables}
|
||||
<div class="flex flex-col gap-2 col-span-2 pr-4">
|
||||
<span class="block text-xs text-tertiary">Scripts & Flows that will be available via MCP</span>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@@ -358,6 +391,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -371,7 +405,7 @@
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => createToken(mcpCreationMode)}
|
||||
disabled={mcpCreationMode && newTokenWorkspace == undefined}
|
||||
disabled={mcpCreationMode && (newTokenWorkspace == undefined || (newMcpScope === 'folder' && !selectedFolder))}
|
||||
>
|
||||
New token
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user