mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-30 16:17:25 +00:00
feat: Integrate MCP with hub (#5685)
* draft * working draft * tool to list scripts * working sse server * add proxy + parse query params * working mcp that runs script * remove useless dependencies * log context * update crate * mcp no proxy * use custom sdk * integrate in windmill api * draft * put rmcp sdk here * do not use mcp as crate * use extensions for get scripts * remove unused crate * list actual scripts * fix * give schema in list tools * cleaning + take workspace id from context * implement calling the script with tool * cleaning + fix ctrl-c * make post path a param * cleaning * better name for tools * fix error with tool name * cleaning * draft cleaning * more cleaning * list script based on settings * fix query * add params in openapi * use rmcp fork from git * remove files * cleaning + fix query * remove settings and use favorite by default * add flows in tools list * handle running flows * remove frontend for mcp settings * handle resource args * send list of resource in shema * handle mcp url setting with token scope * cleaning * avoid calling list tools in call tool * apply scope to flows * cleaning * cleaning * cleaning * cleaning * format files * fix typos * remove log * add back missing dispatch * fix transform for resource-obj + put every resource in description * transform obj to string * cleaner code * better frontend * cleaner code * cleaner logic * add parentheses just in case * add func to fetch hub scripts * fix typos * working fetch and run hub script * also fetch flows from hub * improvments * merge create tool logic * add integrations in description * cleaning * cleaning * small fix * get schema for flow * filter tools fetch by token scope * remove hub flows * remove prints * add hub script integration choice * higher limits * cleaning * fix merge * better naming for hub scripts * no workspace for hub * alow multiple app in one token * plural * fix * cleaning * add documentation * fix bad code * use id directly * cleaner bindings * fix disabled condition * add cancel button + reset apps if not hub token * reset mcp apps * combine all/favorites + hub * small fix
This commit is contained in:
@@ -103,7 +103,9 @@
|
||||
}}
|
||||
disabled={!$enterpriseLicense || !$superadmin}
|
||||
>
|
||||
Save {#if !$superadmin} <span class="text-2xs text-tertiary">superadmin only</span> {/if}
|
||||
Save {#if !$superadmin}
|
||||
<span class="text-2xs text-tertiary">superadmin only</span>
|
||||
{/if}
|
||||
</Button>
|
||||
|
||||
<span class="text-2xs text-tertiary"
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
<script lang="ts">
|
||||
// @ts-ignore
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
|
||||
import { createFloatingActions } from 'svelte-floating-ui'
|
||||
import { tick } from 'svelte'
|
||||
import { offset, flip, shift } from 'svelte-floating-ui/dom'
|
||||
import MultiSelect from '$lib/components/multiselect/MultiSelect.svelte'
|
||||
import DarkModeObserver from '../DarkModeObserver.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
let {
|
||||
items,
|
||||
value = $bindable([]),
|
||||
placeholder = undefined,
|
||||
target = undefined,
|
||||
topPlacement = false,
|
||||
allowUserOptions = undefined
|
||||
} = $props<{
|
||||
items: any[]
|
||||
value?: string[]
|
||||
placeholder?: string
|
||||
target?: string | HTMLElement
|
||||
topPlacement?: boolean
|
||||
allowUserOptions?: boolean | 'append'
|
||||
}>()
|
||||
|
||||
export let items: any[]
|
||||
let propValue: string[] | undefined = []
|
||||
export { propValue as value }
|
||||
$: value = structuredClone(propValue)
|
||||
export let placeholder: string | undefined = undefined
|
||||
export let target: string | HTMLElement | undefined = undefined
|
||||
export let topPlacement = false
|
||||
export let allowUserOptions: boolean | 'append' | undefined = undefined
|
||||
const [floatingRef, floatingContent] = createFloatingActions({
|
||||
strategy: 'absolute',
|
||||
placement: topPlacement ? 'top-start' : 'bottom-start',
|
||||
middleware: [offset(5), flip(), shift()]
|
||||
})
|
||||
|
||||
let outerDiv: HTMLDivElement | undefined = undefined
|
||||
let portalRef: HTMLDivElement | undefined = undefined
|
||||
|
||||
let outerDiv = $state<HTMLDivElement | undefined>(undefined)
|
||||
let portalRef = $state<HTMLDivElement | undefined>(undefined)
|
||||
let darkMode = $state(false)
|
||||
let w = $state(0)
|
||||
let open = $state(false)
|
||||
function moveOptionsToPortal() {
|
||||
// Find ul element with class 'options' within the outerDiv
|
||||
const ul = outerDiv?.querySelector('.options')
|
||||
@@ -34,17 +43,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if (portalRef && outerDiv && (allowUserOptions || items?.length > 0)) {
|
||||
tick().then(() => {
|
||||
moveOptionsToPortal()
|
||||
})
|
||||
}
|
||||
|
||||
// bg-indigo-100 text-indigo-800 dark:bg-indigo-200 dark:text-indigo-900
|
||||
let darkMode: boolean = false
|
||||
|
||||
let w = 0
|
||||
let open: boolean = false
|
||||
$effect(() => {
|
||||
if (portalRef && outerDiv && (allowUserOptions || items?.length > 0)) {
|
||||
tick().then(() => {
|
||||
moveOptionsToPortal()
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<DarkModeObserver bind:darkMode />
|
||||
@@ -62,10 +67,14 @@
|
||||
--sms-focus-border={'none'}
|
||||
--sms-selected-bg={darkMode ? '#c7d2fe' : '#e0e7ff'}
|
||||
--sms-selected-text-color={darkMode ? '#312e81' : '#3730a3'}
|
||||
bind:selected={value}
|
||||
on:change={() => {
|
||||
propValue = value
|
||||
}}
|
||||
bind:selected={
|
||||
() => [...value],
|
||||
(newVal) => {
|
||||
if (!deepEqual(value, newVal)) {
|
||||
value = newVal
|
||||
}
|
||||
}
|
||||
}
|
||||
{placeholder}
|
||||
options={items}
|
||||
on:close={() => {
|
||||
@@ -77,15 +86,19 @@
|
||||
let:option
|
||||
disableRemoveAll
|
||||
>
|
||||
<!-- needed because portal doesn't work for mouseup event en mobile -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="w-full text-sm"
|
||||
on:mouseup|stopPropagation
|
||||
on:pointerdown|stopPropagation={(e) => {
|
||||
role="option"
|
||||
tabindex="0"
|
||||
onmouseup={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onpointerdown={(e) => {
|
||||
e.stopPropagation()
|
||||
let newe = new MouseEvent('mouseup')
|
||||
e.target?.['parentElement']?.dispatchEvent(newe)
|
||||
}}
|
||||
aria-selected={value?.includes(option)}
|
||||
>
|
||||
{option}
|
||||
</div>
|
||||
@@ -93,13 +106,17 @@
|
||||
</div>
|
||||
<Portal {target} name="multi-select">
|
||||
<div use:floatingContent class="z5000" hidden={!open}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={portalRef}
|
||||
class="multiselect"
|
||||
style={`min-width: ${w}px;`}
|
||||
on:click|stopPropagation
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
role="listbox"
|
||||
tabindex="0"
|
||||
></div>
|
||||
</div>
|
||||
</Portal>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import TableCustom from '$lib/components/TableCustom.svelte'
|
||||
import { displayDate, copyToClipboard } from '$lib/utils'
|
||||
import type { TruncatedToken, NewToken } from '$lib/gen'
|
||||
import { UserService } from '$lib/gen'
|
||||
import { IntegrationService, UserService } from '$lib/gen'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Clipboard, Plus } from 'lucide-svelte'
|
||||
import { workspaceStore, userWorkspaces, type UserWorkspace } from '$lib/stores'
|
||||
@@ -12,6 +12,7 @@
|
||||
import Toggle from '../Toggle.svelte'
|
||||
import ClipboardPanel from '../details/ClipboardPanel.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import MultiSelectWrapper from '../multiselect/MultiSelectWrapper.svelte'
|
||||
|
||||
// --- Props ---
|
||||
interface Props {
|
||||
@@ -37,10 +38,14 @@
|
||||
let newToken = $state<string | undefined>(undefined)
|
||||
let newTokenExpiration = $state<number | undefined>(undefined)
|
||||
let newTokenWorkspace = $state<string | undefined>(defaultNewTokenWorkspace)
|
||||
let newMcpApps = $state<string[]>([])
|
||||
let displayCreateToken = $state(scopes != undefined)
|
||||
let mcpCreationMode = $state(false)
|
||||
let newMcpScope = $state('favorites')
|
||||
let newMcpToken = $state<string | undefined>(undefined)
|
||||
let loadingApps = $state(false)
|
||||
let errorFetchApps = $state(false)
|
||||
let allApps = $state<string[]>([])
|
||||
|
||||
function ensureCurrentWorkspaceIncluded(
|
||||
workspacesList: UserWorkspace[],
|
||||
@@ -67,6 +72,14 @@
|
||||
listTokens()
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (mcpCreationMode) {
|
||||
getAllApps()
|
||||
} else {
|
||||
newMcpApps = []
|
||||
}
|
||||
})
|
||||
|
||||
// --- Functions ---
|
||||
async function createToken(mcpMode: boolean = false): Promise<void> {
|
||||
try {
|
||||
@@ -75,7 +88,13 @@
|
||||
date = new Date(new Date().getTime() + newTokenExpiration * 1000)
|
||||
}
|
||||
|
||||
let tokenScopes = mcpMode ? [`mcp:${newMcpScope}`] : scopes
|
||||
let tokenScopes = scopes
|
||||
if (mcpMode) {
|
||||
tokenScopes = [`mcp:${newMcpScope}`]
|
||||
if (newMcpApps.length > 0) {
|
||||
tokenScopes.push(`mcp:hub:${newMcpApps.join(',')}`)
|
||||
}
|
||||
}
|
||||
|
||||
const createdToken = await UserService.createToken({
|
||||
requestBody: {
|
||||
@@ -107,6 +126,7 @@
|
||||
newMcpToken = undefined
|
||||
newToken = undefined
|
||||
newTokenExpiration = undefined
|
||||
newMcpApps = []
|
||||
newTokenLabel = type === 'mcpUrl' ? 'MCP token' : undefined
|
||||
}
|
||||
|
||||
@@ -141,6 +161,26 @@
|
||||
tokenPage -= 1
|
||||
listTokens()
|
||||
}
|
||||
|
||||
async function getAllApps() {
|
||||
if (allApps.length > 0) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
loadingApps = true
|
||||
allApps = (
|
||||
await IntegrationService.listHubIntegrations({
|
||||
kind: 'script'
|
||||
})
|
||||
).map((x) => x.name)
|
||||
} catch (err) {
|
||||
console.error('Hub is not available')
|
||||
allApps = []
|
||||
errorFetchApps = true
|
||||
} finally {
|
||||
loadingApps = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-2 pt-8 pb-1" class:pt-8={scopes == undefined}>
|
||||
@@ -192,66 +232,103 @@
|
||||
{#if displayCreateToken}
|
||||
<div class="py-3 px-3 border rounded-md mb-6 bg-surface-secondary min-w-min">
|
||||
<h3 class="pb-3 font-semibold">Add a new token</h3>
|
||||
{#if scopes != undefined}
|
||||
{#each scopes as scope}
|
||||
<div class="flex flex-col mb-4">
|
||||
<label for="label">Scope</label>
|
||||
<input disabled type="text" value={scope} />
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if showMcpMode}
|
||||
<Toggle
|
||||
on:change={(e) => {
|
||||
mcpCreationMode = e.detail
|
||||
if (e.detail) {
|
||||
newTokenLabel = 'MCP token'
|
||||
newTokenExpiration = undefined
|
||||
newTokenWorkspace = $workspaceStore
|
||||
} else {
|
||||
newTokenLabel = undefined
|
||||
newTokenExpiration = undefined
|
||||
newTokenWorkspace = defaultNewTokenWorkspace
|
||||
}
|
||||
}}
|
||||
checked={mcpCreationMode}
|
||||
options={{
|
||||
right: 'Generate MCP URL',
|
||||
rightTooltip:
|
||||
'Generate a new MCP URL to make your scripts and flows available as tools through your LLM clients.',
|
||||
rightDocumentationLink: 'https://www.windmill.dev/docs/core_concepts/mcp'
|
||||
}}
|
||||
class="mb-4"
|
||||
size="xs"
|
||||
/>
|
||||
<div class="mb-4 flex flex-row flex-shrink-0">
|
||||
<Toggle
|
||||
on:change={(e) => {
|
||||
mcpCreationMode = e.detail
|
||||
if (e.detail) {
|
||||
newTokenLabel = 'MCP token'
|
||||
newTokenExpiration = undefined
|
||||
newTokenWorkspace = $workspaceStore
|
||||
} else {
|
||||
newTokenLabel = undefined
|
||||
newTokenExpiration = undefined
|
||||
newTokenWorkspace = defaultNewTokenWorkspace
|
||||
}
|
||||
}}
|
||||
checked={mcpCreationMode}
|
||||
options={{
|
||||
right: 'Generate MCP URL',
|
||||
rightTooltip:
|
||||
'Generate a new MCP URL to make your scripts and flows available as tools through your LLM clients.',
|
||||
rightDocumentationLink: 'https://www.windmill.dev/docs/core_concepts/mcp'
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-row flex-wrap gap-2 w-full justify-between">
|
||||
|
||||
{#if scopes != undefined}
|
||||
<div class="mb-4">
|
||||
<span class="block mb-1">Scope</span>
|
||||
{#each scopes as scope}
|
||||
<input disabled type="text" value={scope} class="mb-2 w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{#if mcpCreationMode}
|
||||
<div class="flex flex-col">
|
||||
<label for="label">Scope</label>
|
||||
<div>
|
||||
<span class="block mb-1">Scope</span>
|
||||
<ToggleButtonGroup bind:selected={newMcpScope} allowEmpty={false} let:item>
|
||||
<ToggleButton {item} value="favorites" label="Favorites Only" />
|
||||
<ToggleButton {item} value="all" label="All Resources" />
|
||||
<ToggleButton
|
||||
{item}
|
||||
value="favorites"
|
||||
label="Favorites only"
|
||||
tooltip="Make only your favorite scripts and flows available as tools"
|
||||
/>
|
||||
<ToggleButton
|
||||
{item}
|
||||
value="all"
|
||||
label="All scripts/flows"
|
||||
tooltip="Make all your scripts and flows available as tools"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label for="label">Workspace</label>
|
||||
<select bind:value={newTokenWorkspace} disabled={workspaces.length === 1}>
|
||||
|
||||
<div>
|
||||
<span class="block mb-1">Hub scripts (optional)</span>
|
||||
{#if loadingApps}
|
||||
<div>Loading...</div>
|
||||
{:else if errorFetchApps}
|
||||
<div>Error fetching apps</div>
|
||||
{:else}
|
||||
<MultiSelectWrapper
|
||||
items={allApps}
|
||||
placeholder="Select apps"
|
||||
bind:value={newMcpApps}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="block mb-1">Workspace</span>
|
||||
<select
|
||||
bind:value={newTokenWorkspace}
|
||||
disabled={workspaces.length === 1}
|
||||
class="w-full"
|
||||
>
|
||||
{#each workspaces as workspace}
|
||||
<option value={workspace.id}>{workspace.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<label for="label">Label <span class="text-xs text-tertiary">(optional)</span></label>
|
||||
<input type="text" bind:value={newTokenLabel} />
|
||||
|
||||
<div>
|
||||
<span class="block mb-1">Label <span class="text-xs text-tertiary">(optional)</span></span
|
||||
>
|
||||
<input type="text" bind:value={newTokenLabel} class="w-full" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label for="expires"
|
||||
>Expires In <span class="text-xs text-tertiary">(optional)</span>
|
||||
</label>
|
||||
<select bind:value={newTokenExpiration} disabled={mcpCreationMode}>
|
||||
|
||||
<div>
|
||||
<span class="block mb-1"
|
||||
>Expires In <span class="text-xs text-tertiary">(optional)</span></span
|
||||
>
|
||||
<select bind:value={newTokenExpiration} disabled={mcpCreationMode} class="w-full">
|
||||
<option value={undefined}>No expiration</option>
|
||||
<option value={15 * 60}>15m</option>
|
||||
<option value={30 * 60}>30m</option>
|
||||
@@ -262,15 +339,23 @@
|
||||
<option value={90 * 24 * 60 * 60}>90d</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<Button
|
||||
btnClasses="!mt-2"
|
||||
on:click={() => createToken(mcpCreationMode)}
|
||||
disabled={mcpCreationMode && newTokenWorkspace === undefined}
|
||||
>
|
||||
New token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end gap-2 flex-row">
|
||||
<Button
|
||||
on:click={() => {
|
||||
mcpCreationMode = false
|
||||
displayCreateToken = false
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => createToken(mcpCreationMode)}
|
||||
disabled={mcpCreationMode && newTokenWorkspace == undefined}
|
||||
>
|
||||
New token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user