feat: add MCP tools

This commit is contained in:
crazywoola 2025-12-24 15:30:36 +08:00
parent 336769deb1
commit 9d496ed3dc
4 changed files with 94 additions and 17 deletions

View File

@ -148,20 +148,28 @@ You are an expert workflow designer. Generate a Mermaid flowchart based on the u
Constraints:
- Use only node types listed in <available_nodes>.
- Use only tools listed in <available_tools>. When using a tool node, set type=tool and tool=<provider_id>/<tool_name>.
- Use only tools listed in <available_tools>. When using a tool node, set type=tool and tool=<tool_key>.
- Tools may include MCP providers (provider_type=mcp). Tool selection still uses tool_key.
- Prefer reusing node titles from <existing_nodes> when possible.
- Output must be valid Mermaid flowchart syntax, no markdown, no extra text.
- First line must be: flowchart LR
- Every node must be declared on its own line using:
<id>["type=<type>|title=<title>|tool=<provider_id>/<tool_name>"]
<id>["type=<type>|title=<title>|tool=<tool_key>"]
- type is required and must match a type in <available_nodes>.
- title is required for non-tool nodes.
- tool is required only when type=tool, otherwise omit tool.
- Declare all node lines before any edges.
- Edges must use:
<id> --> <id>
<id> -->|true| <id>
<id> -->|false| <id>
- Keep node ids unique and simple (N1, N2, ...).
- For complex orchestration:
- Break the request into stages (ingest, transform, decision, action, output).
- Use IfElse for branching and label edges true/false only.
- Fan-in branches by connecting multiple nodes into a shared downstream node.
- Avoid cycles unless explicitly requested.
- Keep each branch complete with a clear downstream target.
<user_request>
{{TASK_DESCRIPTION}}

View File

@ -0,0 +1,39 @@
import type { ActionItem } from './types'
import { RiSparklingFill } from '@remixicon/react'
import * as React from 'react'
import { isInWorkflowPage } from '@/app/components/workflow/constants'
import i18n from '@/i18n-config/i18next-config'
const BANANA_PROMPT_EXAMPLE = 'Summarize a document, classify sentiment, then notify Slack'
export const bananaAction: ActionItem = {
key: '@banana',
shortcut: '@banana',
title: i18n.t('app.gotoAnything.actions.vibeTitle'),
description: i18n.t('app.gotoAnything.actions.vibeDesc'),
search: async (_query, searchTerm = '', locale) => {
if (!isInWorkflowPage())
return []
const trimmed = searchTerm.trim()
const hasInput = !!trimmed
return [{
id: 'banana-vibe',
title: i18n.t('app.gotoAnything.actions.vibeTitle', { lng: locale }) || 'Banana',
description: hasInput
? i18n.t('app.gotoAnything.actions.vibeDesc', { lng: locale })
: i18n.t('app.gotoAnything.actions.vibeHint', { lng: locale, prompt: BANANA_PROMPT_EXAMPLE }),
type: 'command' as const,
icon: (
<div className="flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg">
<RiSparklingFill className="h-4 w-4 text-text-tertiary" />
</div>
),
data: {
command: 'workflow.vibe',
args: { dsl: trimmed },
},
}]
},
}

View File

@ -64,7 +64,7 @@ type ParsedEdge = {
}
type ParseError = {
error: 'invalidMermaid' | 'missingNodeType' | 'unknownNodeType' | 'unknownTool' | 'missingNodeDefinition'
error: 'invalidMermaid' | 'missingNodeType' | 'unknownNodeType' | 'unknownTool' | 'missingNodeDefinition' | 'unknownNodeId' | 'unsupportedEdgeLabel'
detail?: string
}
@ -111,6 +111,9 @@ const parseNodeLabel = (label: string) => {
info.type = tokens[0]
}
if (!info.tool && info.tool_key)
info.tool = info.tool_key
return info
}
@ -125,6 +128,17 @@ const parseNodeToken = (token: string) => {
return null
}
const normalizeBranchLabel = (label?: string) => {
if (!label)
return ''
const normalized = label.trim().toLowerCase()
if (['true', 'yes', 'y', '1'].includes(normalized))
return 'true'
if (['false', 'no', 'n', '0'].includes(normalized))
return 'false'
return ''
}
const parseMermaidFlowchart = (
raw: string,
nodeTypeLookup: Map<string, BlockEnum>,
@ -137,6 +151,7 @@ const parseMermaidFlowchart = (
}).filter(Boolean)
const nodesMap = new Map<string, ParsedNodeDraft>()
const declaredNodeIds = new Set<string>()
const edges: ParsedEdge[] = []
const registerNode = (id: string, label?: string): ParseError | null => {
@ -172,6 +187,7 @@ const parseMermaidFlowchart = (
}
nodesMap.set(id, { ...(existing || {}), ...nodeData })
declaredNodeIds.add(id)
return null
}
@ -189,6 +205,11 @@ const parseMermaidFlowchart = (
if (!sourceToken || !targetToken)
return { error: 'invalidMermaid', detail: line }
if (!sourceToken.label && !declaredNodeIds.has(sourceToken.id))
return { error: 'unknownNodeId', detail: sourceToken.id }
if (!targetToken.label && !declaredNodeIds.has(targetToken.id))
return { error: 'unknownNodeId', detail: targetToken.id }
const sourceError = registerNode(sourceToken.id, sourceToken.label)
if (sourceError)
return sourceError
@ -213,15 +234,26 @@ const parseMermaidFlowchart = (
}
const parsedNodes: ParsedNode[] = []
const nodeTypeById = new Map<string, BlockEnum>()
for (const node of nodesMap.values()) {
if (!node.type)
return { error: 'missingNodeDefinition', detail: node.id }
parsedNodes.push(node as ParsedNode)
nodeTypeById.set(node.id, node.type)
}
if (!parsedNodes.length)
return { error: 'invalidMermaid', detail: '' }
for (const edge of edges) {
if (!edge.label)
continue
const sourceType = nodeTypeById.get(edge.sourceId)
const branchLabel = normalizeBranchLabel(edge.label)
if (sourceType !== BlockEnum.IfElse || !branchLabel)
return { error: 'unsupportedEdgeLabel', detail: edge.label }
}
return { nodes: parsedNodes, edges }
}
@ -231,17 +263,6 @@ const dedupeHandles = (handles?: string[]) => {
return Array.from(new Set(handles))
}
const normalizeBranchLabel = (label?: string) => {
if (!label)
return ''
const normalized = label.trim().toLowerCase()
if (['true', 'yes', 'y', '1'].includes(normalized))
return 'true'
if (['false', 'no', 'n', '0'].includes(normalized))
return 'false'
return ''
}
const buildToolParams = (parameters?: Tool['parameters']) => {
const params: Record<string, string> = {}
if (!parameters)
@ -426,6 +447,7 @@ export const useWorkflowVibe = () => {
const toolsPayload = toolOptions.map(tool => ({
provider_id: tool.provider_id,
provider_name: tool.provider_name,
provider_type: tool.provider_type,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
tool_key: `${tool.provider_id}/${tool.tool_name}`,
@ -467,12 +489,18 @@ export const useWorkflowVibe = () => {
case 'missingNodeDefinition':
Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') })
return
case 'unknownNodeId':
Toast.notify({ type: 'error', message: t('workflow.vibe.unknownNodeId', { id: parseResult.detail }) })
return
case 'unknownNodeType':
Toast.notify({ type: 'error', message: t('workflow.vibe.nodeTypeUnavailable', { type: parseResult.detail }) })
return
case 'unknownTool':
Toast.notify({ type: 'error', message: t('workflow.vibe.toolUnavailable', { tool: parseResult.detail }) })
return
case 'unsupportedEdgeLabel':
Toast.notify({ type: 'error', message: t('workflow.vibe.unsupportedEdgeLabel', { label: parseResult.detail }) })
return
default:
Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') })
return
@ -547,11 +575,11 @@ export const useWorkflowVibe = () => {
})
const newEdges: Edge[] = []
parseResult.edges.forEach((edgeSpec) => {
for (const edgeSpec of parseResult.edges) {
const sourceNode = nodeIdMap.get(edgeSpec.sourceId)
const targetNode = nodeIdMap.get(edgeSpec.targetId)
if (!sourceNode || !targetNode)
return
continue
let sourceHandle = 'source'
if (sourceNode.data.type === BlockEnum.IfElse) {
@ -565,7 +593,7 @@ export const useWorkflowVibe = () => {
}
newEdges.push(buildEdge(sourceNode, targetNode, sourceHandle))
})
}
const bounds = nodes.reduce(
(acc, node) => {

View File

@ -132,6 +132,8 @@ const translation = {
invalidFlowchart: 'The generated flowchart could not be parsed.',
nodeTypeUnavailable: 'Node type "{{type}}" is not available in this workflow.',
toolUnavailable: 'Tool "{{tool}}" is not available in this workspace.',
unknownNodeId: 'Node "{{id}}" is used before it is defined.',
unsupportedEdgeLabel: 'Unsupported edge label "{{label}}". Only true/false are allowed for if/else.',
},
publishLimit: {
startNodeTitlePrefix: 'Upgrade to',