mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-22 08:38:13 +02:00
feat(mcp): generic MCP tool source with per-node function filtering (#301)
* feat(mcp): generic MCP tool source with per-node function filtering
Adds a Model Context Protocol tool category: connect a customer MCP
server and expose its tools to the agent, with optional per-node
allow-listing of individual MCP functions.
- ToolCategory.MCP enum + alembic migration
- MCP definition validator and collision-safe function-name namespacing
- McpToolSession wrapper: graceful-degrade, per-call open/close lifecycle
- CustomToolManager MCP branch (schemas + proxy handlers)
- Per-node mcp_tool_filters threaded through DTO/graph/engine
- Best-effort discovered_tools catalog cache + POST /tools/{uuid}/mcp/refresh
- UI: MCP create/edit config, tabbed ToolSelector with per-node toggles
* feat: refactor for code standardisation and documentation
---------
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
This commit is contained in:
parent
0097974444
commit
75839f9de5
40 changed files with 3028 additions and 137 deletions
|
|
@ -9,9 +9,10 @@ import { Badge } from "@/components/ui/badge";
|
|||
interface ToolBadgesProps {
|
||||
toolUuids: string[];
|
||||
onStaleUuidsDetected?: (staleUuids: string[]) => void;
|
||||
mcpToolFilters?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export function ToolBadges({ toolUuids, onStaleUuidsDetected }: ToolBadgesProps) {
|
||||
export function ToolBadges({ toolUuids, onStaleUuidsDetected, mcpToolFilters }: ToolBadgesProps) {
|
||||
const { tools } = useWorkflow();
|
||||
const [selectedTools, setSelectedTools] = useState<ToolResponse[]>([]);
|
||||
|
||||
|
|
@ -50,15 +51,29 @@ export function ToolBadges({ toolUuids, onStaleUuidsDetected }: ToolBadgesProps)
|
|||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selectedTools.map((tool) => (
|
||||
<Badge
|
||||
key={tool.tool_uuid}
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
>
|
||||
{tool.name}
|
||||
</Badge>
|
||||
))}
|
||||
{selectedTools.map((tool) => {
|
||||
const isMcp = tool.category === "mcp";
|
||||
const enabledFns = isMcp ? (mcpToolFilters?.[tool.tool_uuid] ?? []) : [];
|
||||
|
||||
if (isMcp && enabledFns.length > 0) {
|
||||
return enabledFns.map((fn) => (
|
||||
<Badge
|
||||
key={`${tool.tool_uuid}-${fn}`}
|
||||
variant="outline"
|
||||
className="text-xs flex items-center gap-1.5"
|
||||
>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-green-500 shrink-0" />
|
||||
{fn}
|
||||
</Badge>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge key={tool.tool_uuid} variant="outline" className="text-xs">
|
||||
{tool.name}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { ExternalLink, RefreshCw } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
import { renderToolIcon } from "@/app/tools/config";
|
||||
import { useWorkflowOptional } from "@/app/workflow/[workflowId]/contexts/WorkflowContext";
|
||||
import type { ToolResponse } from "@/client/types.gen";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { TOOLS_INTRODUCTION_DOC_URL } from "@/constants/documentation";
|
||||
|
||||
import { type McpDiscoveredTool, refreshMcpTools } from "./mcpRefresh";
|
||||
|
||||
interface ToolSelectorProps {
|
||||
value: string[];
|
||||
onChange: (uuids: string[]) => void;
|
||||
|
|
@ -18,6 +23,46 @@ interface ToolSelectorProps {
|
|||
label?: string;
|
||||
description?: string;
|
||||
showLabel?: boolean;
|
||||
mcpToolFilters?: Record<string, string[]>;
|
||||
onMcpToolFiltersChange?: (next: Record<string, string[]>) => void;
|
||||
}
|
||||
|
||||
function isMcp(tool: ToolResponse): boolean {
|
||||
return tool.category === "mcp";
|
||||
}
|
||||
|
||||
function discoveredOf(tool: ToolResponse): McpDiscoveredTool[] {
|
||||
const def = (tool.definition ?? {}) as {
|
||||
config?: { discovered_tools?: McpDiscoveredTool[] };
|
||||
};
|
||||
return def.config?.discovered_tools ?? [];
|
||||
}
|
||||
|
||||
function withDiscoveredTools(
|
||||
tool: ToolResponse,
|
||||
discoveredTools: McpDiscoveredTool[],
|
||||
): ToolResponse {
|
||||
const definition =
|
||||
tool.definition && typeof tool.definition === "object"
|
||||
? tool.definition
|
||||
: {};
|
||||
const config =
|
||||
"config" in definition &&
|
||||
definition.config &&
|
||||
typeof definition.config === "object"
|
||||
? definition.config
|
||||
: {};
|
||||
|
||||
return {
|
||||
...tool,
|
||||
definition: {
|
||||
...definition,
|
||||
config: {
|
||||
...config,
|
||||
discovered_tools: discoveredTools,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function ToolSelector({
|
||||
|
|
@ -28,18 +73,64 @@ export function ToolSelector({
|
|||
label = "Tools",
|
||||
description = "Select tools that the agent can use during the conversation.",
|
||||
showLabel = true,
|
||||
mcpToolFilters = {},
|
||||
onMcpToolFiltersChange = () => {},
|
||||
}: ToolSelectorProps) {
|
||||
// Filter to only show active tools
|
||||
const activeTools = tools.filter((tool) => tool.status === "active");
|
||||
const workflow = useWorkflowOptional();
|
||||
const activeTools = tools.filter((t) => t.status === "active");
|
||||
const httpTools = activeTools.filter((t) => !isMcp(t));
|
||||
const mcpTools = activeTools.filter(isMcp);
|
||||
|
||||
const handleToggle = (toolUuid: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
onChange([...value, toolUuid]);
|
||||
} else {
|
||||
onChange(value.filter((id) => id !== toolUuid));
|
||||
}
|
||||
const [refreshing, setRefreshing] = useState<Record<string, boolean>>({});
|
||||
const [refreshError, setRefreshError] = useState<Record<string, string>>({});
|
||||
|
||||
const httpHandleToggle = (toolUuid: string, checked: boolean) => {
|
||||
if (checked) onChange([...value, toolUuid]);
|
||||
else onChange(value.filter((id) => id !== toolUuid));
|
||||
};
|
||||
|
||||
const mcpFnToggle = (toolUuid: string, fnName: string, checked: boolean) => {
|
||||
const current = mcpToolFilters[toolUuid] ?? [];
|
||||
const nextFns = checked
|
||||
? Array.from(new Set([...current, fnName]))
|
||||
: current.filter((n) => n !== fnName);
|
||||
|
||||
const nextFilters = { ...mcpToolFilters };
|
||||
if (nextFns.length > 0) nextFilters[toolUuid] = nextFns;
|
||||
else delete nextFilters[toolUuid];
|
||||
onMcpToolFiltersChange(nextFilters);
|
||||
|
||||
const hasUuid = value.includes(toolUuid);
|
||||
if (nextFns.length > 0 && !hasUuid) onChange([...value, toolUuid]);
|
||||
else if (nextFns.length === 0 && hasUuid)
|
||||
onChange(value.filter((id) => id !== toolUuid));
|
||||
};
|
||||
|
||||
const doRefresh = async (toolUuid: string) => {
|
||||
setRefreshing((r) => ({ ...r, [toolUuid]: true }));
|
||||
setRefreshError((e) => {
|
||||
const n = { ...e };
|
||||
delete n[toolUuid];
|
||||
return n;
|
||||
});
|
||||
const res = await refreshMcpTools(toolUuid);
|
||||
setRefreshing((r) => ({ ...r, [toolUuid]: false }));
|
||||
if (res.error && res.discovered_tools.length === 0) {
|
||||
setRefreshError((e) => ({ ...e, [toolUuid]: res.error as string }));
|
||||
return;
|
||||
}
|
||||
workflow?.updateTool?.(toolUuid, (tool) =>
|
||||
withDiscoveredTools(tool, res.discovered_tools),
|
||||
);
|
||||
};
|
||||
|
||||
const selectedCount =
|
||||
httpTools.filter((t) => value.includes(t.tool_uuid)).length +
|
||||
mcpTools.reduce(
|
||||
(acc, t) => acc + (mcpToolFilters[t.tool_uuid]?.length ?? 0),
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
{showLabel && (
|
||||
|
|
@ -48,7 +139,14 @@ export function ToolSelector({
|
|||
{description && (
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{description}{" "}
|
||||
<a href={TOOLS_INTRODUCTION_DOC_URL} target="_blank" rel="noopener noreferrer" className="underline">Learn more</a>
|
||||
<a
|
||||
href={TOOLS_INTRODUCTION_DOC_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</Label>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -67,45 +165,178 @@ export function ToolSelector({
|
|||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-md divide-y">
|
||||
{activeTools.map((tool) => {
|
||||
const isSelected = value.includes(tool.tool_uuid);
|
||||
return (
|
||||
<label
|
||||
key={tool.tool_uuid}
|
||||
className={`flex items-center gap-3 p-3 cursor-pointer hover:bg-muted/50 ${
|
||||
disabled ? "opacity-50 cursor-not-allowed" : ""
|
||||
}`}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={disabled}
|
||||
onCheckedChange={(checked) => {
|
||||
handleToggle(tool.tool_uuid, checked === true);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="w-6 h-6 rounded flex items-center justify-center shrink-0"
|
||||
style={{
|
||||
backgroundColor: tool.icon_color || "#3B82F6",
|
||||
}}
|
||||
>
|
||||
{renderToolIcon(tool.category, "h-3 w-3 text-white")}
|
||||
<Tabs defaultValue="http">
|
||||
<TabsList>
|
||||
<TabsTrigger value="http">
|
||||
HTTP & Tools ({httpTools.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="mcp">
|
||||
MCP ({mcpTools.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="http">
|
||||
<div className="border rounded-md divide-y">
|
||||
{httpTools.length === 0 && (
|
||||
<div className="p-3 text-sm text-muted-foreground">
|
||||
No HTTP/native tools.
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className="text-sm font-medium truncate">
|
||||
{tool.name}
|
||||
</span>
|
||||
{tool.description && (
|
||||
<span className="text-xs text-muted-foreground break-words">
|
||||
{tool.description}
|
||||
</span>
|
||||
)}
|
||||
)}
|
||||
{httpTools.map((tool) => {
|
||||
const isSelected = value.includes(tool.tool_uuid);
|
||||
return (
|
||||
<label
|
||||
key={tool.tool_uuid}
|
||||
className={`flex items-center gap-3 p-3 cursor-pointer hover:bg-muted/50 ${
|
||||
disabled ? "opacity-50 cursor-not-allowed" : ""
|
||||
}`}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={disabled}
|
||||
onCheckedChange={(c) =>
|
||||
httpHandleToggle(tool.tool_uuid, c === true)
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="w-6 h-6 rounded flex items-center justify-center shrink-0"
|
||||
style={{
|
||||
backgroundColor: tool.icon_color || "#3B82F6",
|
||||
}}
|
||||
>
|
||||
{renderToolIcon(tool.category, "h-3 w-3 text-white")}
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className="text-sm font-medium truncate">
|
||||
{tool.name}
|
||||
</span>
|
||||
{tool.description && (
|
||||
<span className="text-xs text-muted-foreground break-words">
|
||||
{tool.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="mcp">
|
||||
<div className="border rounded-md divide-y">
|
||||
{mcpTools.length === 0 && (
|
||||
<div className="p-3 text-sm text-muted-foreground">
|
||||
No MCP tools.
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
<div className="p-2 bg-muted/30">
|
||||
)}
|
||||
{mcpTools.map((tool) => {
|
||||
const fns = discoveredOf(tool);
|
||||
const selected = mcpToolFilters[tool.tool_uuid] ?? [];
|
||||
const busy = !!refreshing[tool.tool_uuid];
|
||||
const err = refreshError[tool.tool_uuid];
|
||||
return (
|
||||
<details key={tool.tool_uuid} className="p-3">
|
||||
<summary className="flex items-center gap-3 cursor-pointer list-none">
|
||||
<div
|
||||
className="w-6 h-6 rounded flex items-center justify-center shrink-0"
|
||||
style={{
|
||||
backgroundColor: tool.icon_color || "#8B5CF6",
|
||||
}}
|
||||
>
|
||||
{renderToolIcon(tool.category, "h-3 w-3 text-white")}
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className="text-sm font-medium truncate">
|
||||
{tool.name}
|
||||
</span>
|
||||
{tool.description && (
|
||||
<span className="text-xs text-muted-foreground break-words">
|
||||
{tool.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
{selected.length}/{fns.length} tools
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<div className="mt-3 pl-9 grid gap-2">
|
||||
<div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => doRefresh(tool.tool_uuid)}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-3 w-3 mr-2 ${busy ? "animate-spin" : ""}`}
|
||||
/>
|
||||
Refresh tools
|
||||
</Button>
|
||||
</div>
|
||||
{err && (
|
||||
<p className="text-xs text-destructive">{err}</p>
|
||||
)}
|
||||
{fns.length === 0 && !err && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No tools discovered — Refresh.
|
||||
</p>
|
||||
)}
|
||||
{fns.map((fn) => {
|
||||
const checked = selected.includes(fn.name);
|
||||
return (
|
||||
<label
|
||||
key={fn.name}
|
||||
className="flex items-start gap-3 cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onCheckedChange={(c) =>
|
||||
mcpFnToggle(tool.tool_uuid, fn.name, c === true)
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className="text-sm font-medium">
|
||||
{fn.name}
|
||||
</span>
|
||||
{fn.description && (
|
||||
<span className="text-xs text-muted-foreground break-words">
|
||||
{fn.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{selected
|
||||
.filter((n) => !fns.some((f) => f.name === n))
|
||||
.map((n) => (
|
||||
<label
|
||||
key={`stale-${n}`}
|
||||
className="flex items-start gap-3 cursor-pointer opacity-60"
|
||||
>
|
||||
<Checkbox
|
||||
checked
|
||||
disabled={disabled}
|
||||
onCheckedChange={() =>
|
||||
mcpFnToggle(tool.tool_uuid, n, false)
|
||||
}
|
||||
/>
|
||||
<span className="text-sm line-through">
|
||||
{n} (unavailable)
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<div className="mt-2 p-2 bg-muted/30 rounded-md">
|
||||
<Link
|
||||
href="/tools"
|
||||
target="_blank"
|
||||
|
|
@ -115,12 +346,12 @@ export function ToolSelector({
|
|||
Manage Tools
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
{value.length > 0 && (
|
||||
{selectedCount > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{value.length} tool{value.length !== 1 ? "s" : ""} selected
|
||||
{selectedCount} tool{selectedCount !== 1 ? "s" : ""} selected
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
68
ui/src/components/flow/mcpRefresh.ts
Normal file
68
ui/src/components/flow/mcpRefresh.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { refreshMcpToolsApiV1ToolsToolUuidMcpRefreshPost } from "@/client/sdk.gen";
|
||||
import type { McpRefreshResponse } from "@/client/types.gen";
|
||||
|
||||
export interface McpDiscoveredTool {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface McpRefreshResult {
|
||||
tool_uuid: string;
|
||||
discovered_tools: McpDiscoveredTool[];
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
function normalizeDiscoveredTools(
|
||||
discoveredTools: McpRefreshResponse["discovered_tools"],
|
||||
): McpDiscoveredTool[] {
|
||||
if (!Array.isArray(discoveredTools)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return discoveredTools.flatMap((tool) => {
|
||||
if (!tool || typeof tool !== "object") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const name = "name" in tool ? tool.name : undefined;
|
||||
if (typeof name !== "string" || !name.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const description =
|
||||
"description" in tool && typeof tool.description === "string"
|
||||
? tool.description
|
||||
: "";
|
||||
|
||||
return [{ name, description }];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-discover an MCP tool's server catalog.
|
||||
* Uses the shared generated `client` (auth bearer is injected by interceptor).
|
||||
*/
|
||||
export async function refreshMcpTools(
|
||||
toolUuid: string,
|
||||
): Promise<McpRefreshResult> {
|
||||
const { data, error } = await refreshMcpToolsApiV1ToolsToolUuidMcpRefreshPost({
|
||||
path: {
|
||||
tool_uuid: toolUuid,
|
||||
},
|
||||
});
|
||||
if (error || !data) {
|
||||
return {
|
||||
tool_uuid: toolUuid,
|
||||
discovered_tools: [],
|
||||
error:
|
||||
typeof error === "string"
|
||||
? error
|
||||
: "Refresh request failed. Check the MCP server and try again.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
tool_uuid: data.tool_uuid,
|
||||
discovered_tools: normalizeDiscoveredTools(data.discovered_tools),
|
||||
error: data.error ?? null,
|
||||
};
|
||||
}
|
||||
|
|
@ -205,6 +205,7 @@ function CanvasPreview({
|
|||
<ToolBadges
|
||||
toolUuids={data.tool_uuids}
|
||||
onStaleUuidsDetected={onStaleTools}
|
||||
mcpToolFilters={data.mcp_tool_filters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -396,14 +397,22 @@ export const GenericNode = memo(({ data, selected, id, type }: GenericNodeProps)
|
|||
const spec = bySpecName.get(type);
|
||||
|
||||
// ── Form state ─────────────────────────────────────────────────────
|
||||
const [values, setValues] = useState<Record<string, unknown>>(() =>
|
||||
spec ? seedValues(data, spec) : {},
|
||||
// mcp_tool_filters is not a spec property, so seedValues won't carry it;
|
||||
// seed merges it back in alongside the spec-derived values.
|
||||
const seed = useCallback(
|
||||
() =>
|
||||
spec
|
||||
? { ...seedValues(data, spec), mcp_tool_filters: data.mcp_tool_filters }
|
||||
: {},
|
||||
[data, spec],
|
||||
);
|
||||
|
||||
const [values, setValues] = useState<Record<string, unknown>>(seed);
|
||||
|
||||
// Re-seed once the spec arrives (initial fetch race).
|
||||
useEffect(() => {
|
||||
if (spec && Object.keys(values).length === 0) {
|
||||
setValues(seedValues(data, spec));
|
||||
setValues(seed());
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [spec]);
|
||||
|
|
@ -464,7 +473,11 @@ export const GenericNode = memo(({ data, selected, id, type }: GenericNodeProps)
|
|||
const isDirty = useMemo(() => {
|
||||
if (!spec) return false;
|
||||
const baseline = seedValues(data, spec);
|
||||
return propertyNames.some((n) => values[n] !== baseline[n]);
|
||||
if (propertyNames.some((n) => values[n] !== baseline[n])) return true;
|
||||
return (
|
||||
JSON.stringify(values.mcp_tool_filters ?? {}) !==
|
||||
JSON.stringify(data.mcp_tool_filters ?? {})
|
||||
);
|
||||
}, [values, data, spec, propertyNames]);
|
||||
|
||||
const handleSave = async () => {
|
||||
|
|
@ -478,12 +491,12 @@ export const GenericNode = memo(({ data, selected, id, type }: GenericNodeProps)
|
|||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
if (newOpen && spec) setValues(seedValues(data, spec));
|
||||
if (newOpen && spec) setValues(seed());
|
||||
setOpen(newOpen);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open && spec) setValues(seedValues(data, spec));
|
||||
if (open && spec) setValues(seed());
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, open]);
|
||||
|
||||
|
|
@ -562,6 +575,18 @@ export const GenericNode = memo(({ data, selected, id, type }: GenericNodeProps)
|
|||
tools: tools ?? [],
|
||||
documents: documents ?? [],
|
||||
recordings: recordings ?? [],
|
||||
mcpToolFilters:
|
||||
(values.mcp_tool_filters as
|
||||
| Record<string, string[]>
|
||||
| undefined) ?? {},
|
||||
onMcpToolFiltersChange: (next) =>
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
mcp_tool_filters:
|
||||
Object.keys(next).length > 0
|
||||
? next
|
||||
: undefined,
|
||||
})),
|
||||
}}
|
||||
/>
|
||||
{type === "trigger" && (
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ export interface RendererContext {
|
|||
tools: ToolResponse[];
|
||||
documents: DocumentResponseSchema[];
|
||||
recordings: RecordingResponseSchema[];
|
||||
/** Per-node MCP function allowlist (sibling of tool_uuids on node data). */
|
||||
mcpToolFilters?: Record<string, string[]>;
|
||||
/** Persist a new mcp_tool_filters object onto the node form values. */
|
||||
onMcpToolFiltersChange?: (next: Record<string, string[]>) => void;
|
||||
}
|
||||
|
||||
export interface PropertyInputProps {
|
||||
|
|
@ -83,6 +87,10 @@ export function PropertyInput({ spec, value, onChange, context }: PropertyInputP
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
tools={context.tools}
|
||||
mcpToolFilters={context.mcpToolFilters ?? {}}
|
||||
onMcpToolFiltersChange={
|
||||
context.onMcpToolFiltersChange ?? (() => {})
|
||||
}
|
||||
/>
|
||||
);
|
||||
case "document_refs":
|
||||
|
|
@ -401,7 +409,13 @@ function ToolRefsWidget({
|
|||
value,
|
||||
onChange,
|
||||
tools,
|
||||
}: WidgetProps & { tools: ToolResponse[] }) {
|
||||
mcpToolFilters,
|
||||
onMcpToolFiltersChange,
|
||||
}: WidgetProps & {
|
||||
tools: ToolResponse[];
|
||||
mcpToolFilters: Record<string, string[]>;
|
||||
onMcpToolFiltersChange: (next: Record<string, string[]>) => void;
|
||||
}) {
|
||||
return (
|
||||
<ToolSelector
|
||||
value={(value as string[] | undefined) ?? []}
|
||||
|
|
@ -409,6 +423,8 @@ function ToolRefsWidget({
|
|||
tools={tools}
|
||||
label={spec.display_name}
|
||||
description={spec.description}
|
||||
mcpToolFilters={mcpToolFilters}
|
||||
onMcpToolFiltersChange={onMcpToolFiltersChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ export type FlowNodeData = {
|
|||
qa_sample_rate?: number;
|
||||
// Tools - array of tool UUIDs that can be invoked by this node
|
||||
tool_uuids?: string[];
|
||||
// Per-node MCP function allowlist: { toolUuid: [raw MCP tool name, ...] }.
|
||||
// Default-none: a toolUuid absent here (or mapped to []) exposes zero
|
||||
// functions of that MCP server on this node.
|
||||
mcp_tool_filters?: Record<string, string[]>;
|
||||
// Documents - array of knowledge base document UUIDs that can be referenced by this node
|
||||
document_uuids?: string[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue