From 7cf0951cfa969bb3d9faf9cb1ed4ef76ca52846b Mon Sep 17 00:00:00 2001 From: panxinying Date: Mon, 7 Sep 2026 16:43:41 +0800 Subject: [PATCH] feat: mcp node --- .../workflow-canvas/icons/mcp-node-icon.vue | 6 + .../component/McpServerInputDialog.vue | 62 +++ .../workflow-canvas/nodes/mcp-node/index.ts | 11 + .../workflow-canvas/nodes/mcp-node/index.vue | 408 ++++++++++++++++++ .../store/api/workspace/index.ts | 23 +- ui/src/workflow-canvas/store/index.ts | 37 +- 6 files changed, 543 insertions(+), 4 deletions(-) create mode 100644 ui/src/workflow-canvas/icons/mcp-node-icon.vue create mode 100644 ui/src/workflow-canvas/nodes/mcp-node/component/McpServerInputDialog.vue create mode 100644 ui/src/workflow-canvas/nodes/mcp-node/index.ts create mode 100644 ui/src/workflow-canvas/nodes/mcp-node/index.vue diff --git a/ui/src/workflow-canvas/icons/mcp-node-icon.vue b/ui/src/workflow-canvas/icons/mcp-node-icon.vue new file mode 100644 index 00000000000..7ea4a139f8c --- /dev/null +++ b/ui/src/workflow-canvas/icons/mcp-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow-canvas/nodes/mcp-node/component/McpServerInputDialog.vue b/ui/src/workflow-canvas/nodes/mcp-node/component/McpServerInputDialog.vue new file mode 100644 index 00000000000..f74e60edc90 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/mcp-node/component/McpServerInputDialog.vue @@ -0,0 +1,62 @@ + + + diff --git a/ui/src/workflow-canvas/nodes/mcp-node/index.ts b/ui/src/workflow-canvas/nodes/mcp-node/index.ts new file mode 100644 index 00000000000..6244004aabc --- /dev/null +++ b/ui/src/workflow-canvas/nodes/mcp-node/index.ts @@ -0,0 +1,11 @@ +import McpNodeVue from './index.vue' +import { WorkflowNodeModel, WorkflowNodeView } from '@/workflow-canvas/core/workflow-node' +import { WorkflowNodeType } from '@/workflow-canvas/types' + +class McpNodeView extends WorkflowNodeView { + constructor(props: ConstructorParameters[0]) { + super(props, McpNodeVue) + } +} + +export default { type: WorkflowNodeType.McpNode, model: WorkflowNodeModel, view: McpNodeView } diff --git a/ui/src/workflow-canvas/nodes/mcp-node/index.vue b/ui/src/workflow-canvas/nodes/mcp-node/index.vue new file mode 100644 index 00000000000..6252d7d3258 --- /dev/null +++ b/ui/src/workflow-canvas/nodes/mcp-node/index.vue @@ -0,0 +1,408 @@ + + + diff --git a/ui/src/workflow-canvas/store/api/workspace/index.ts b/ui/src/workflow-canvas/store/api/workspace/index.ts index ded3c1566cd..f4a29b740b6 100644 --- a/ui/src/workflow-canvas/store/api/workspace/index.ts +++ b/ui/src/workflow-canvas/store/api/workspace/index.ts @@ -1,3 +1,24 @@ +import { post } from '@/api/admin/core/request' import modelAPI from '@/api/admin/workspace/model/model' import providerAPI from '@/api/admin/model-provider' -export default { getModelList: modelAPI.getModelList, getProviderList: providerAPI.getProviderList, getModelParamsForm: modelAPI.getModelParamsForm } +import toolAPI from '@/api/admin/workspace/tool/tool' +import type { McpTool } from '@/workflow-canvas/store' +import { getWorkspaceId } from '@/utils/resource-context' + +/** resourceType: application | tool | knowledge */ +const getMcpTools = (resourceType: string, resourceId: string, mcpServers: string) => { + const workspaceId = getWorkspaceId() + return post<{ mcp_servers: string }, McpTool[]>( + `/workspace/${workspaceId}/${resourceType}/${resourceId}/mcp_tools`, + { mcp_servers: mcpServers }, + ) +} + +export default { + getModelList: modelAPI.getModelList, + getProviderList: providerAPI.getProviderList, + getModelParamsForm: modelAPI.getModelParamsForm, + getMcpTools, + getAllToolList: toolAPI.getAllTool, + getToolById: toolAPI.getToolDetail, +} diff --git a/ui/src/workflow-canvas/store/index.ts b/ui/src/workflow-canvas/store/index.ts index ca6fc25b4b7..f35ed720928 100644 --- a/ui/src/workflow-canvas/store/index.ts +++ b/ui/src/workflow-canvas/store/index.ts @@ -1,14 +1,33 @@ -import type { Dict, DynamicFormField, ModelItem, ModelProviderItem } from '@/api/types' +import type { Dict, DynamicFormField, ModelItem, ModelProviderItem, ToolItem } from '@/api/types' + +export interface McpTool { + args_schema: Record + description: string + name: string + server: string +} // 底层 api/*/index.ts 提供的原始接口实现,不含缓存层能力。 type ApiModule = { getModelList: (query?: Dict) => Promise getProviderList: () => Promise getModelParamsForm: (modelId: string) => Promise + getMcpTools?: (resourceType: string, resourceId: string, mcpServers: string) => Promise + getAllToolList?: (query?: Dict) => Promise + getToolById?: (toolId: string) => Promise } // useWorkflowStore 返回的包装接口:默认走缓存,通过 store.force.xxx() 强制刷新。 -type WorkflowStore = ApiModule & { force: ApiModule } +export type WorkflowStoreApi = { + getModelList: (query?: Dict) => Promise + getProviderList: () => Promise + getModelParamsForm: (modelId: string) => Promise + getMcpTools: (resourceType: string, resourceId: string, mcpServers: string) => Promise + getAllToolList: (query?: Dict) => Promise + getToolById: (toolId: string) => Promise +} + +type WorkflowStore = WorkflowStoreApi & { force: WorkflowStoreApi } const apiModules = import.meta.glob<{ default: ApiModule }>('./api/*/index.ts', { eager: true }) @@ -51,7 +70,7 @@ export function useWorkflowStore(apiType: string): WorkflowStore { } // 生成一组接口方法;force 为 true 时对应的调用会跳过缓存强制刷新。 - function build(force: boolean): ApiModule { + function build(force: boolean): WorkflowStoreApi { return { getModelList(query?: Dict): Promise { return withCache(`model:${JSON.stringify(query ?? {})}`, () => resolvedApi.getModelList(query), force) @@ -62,6 +81,18 @@ export function useWorkflowStore(apiType: string): WorkflowStore { getModelParamsForm(modelId: string): Promise { return withCache(`modelParamsForm:${modelId}`, () => resolvedApi.getModelParamsForm(modelId), force) }, + getMcpTools(resourceType: string, resourceId: string, mcpServers: string): Promise { + if (!resolvedApi.getMcpTools) return Promise.resolve([]) + return withCache(`mcp-tools:${resourceType}:${resourceId}:${mcpServers}`, () => resolvedApi.getMcpTools!(resourceType, resourceId, mcpServers), force) + }, + getAllToolList(query?: Dict): Promise { + if (!resolvedApi.getAllToolList) return Promise.resolve([]) + return withCache(`tool-list:${JSON.stringify(query ?? {})}`, () => resolvedApi.getAllToolList!(query), force) + }, + getToolById(toolId: string): Promise { + if (!resolvedApi.getToolById) return Promise.resolve({} as ToolItem) + return withCache(`tool:${toolId}`, () => resolvedApi.getToolById!(toolId), force) + }, } }