feat(mcp): allow filtering by folder (#6366)

* allow filtering mcp by folder

* nit

* add error if wrong format
This commit is contained in:
centdix
2025-08-12 14:21:16 +02:00
committed by GitHub
parent 98912d47db
commit c1c65720b4
2 changed files with 62 additions and 15 deletions
+17 -4
View File
@@ -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>