From cd73e9f2c1349fc1e2fa7270c6175f3e815439ff Mon Sep 17 00:00:00 2001 From: hexqi Date: Tue, 30 Dec 2025 13:27:55 +0800 Subject: [PATCH 1/2] feat: support add custom mcp server --- docs/advanced-features/new-ai-plugin-usage.md | 17 ++ packages/common/composable/mcp/toolUtils.ts | 1 + packages/plugins/robot/meta.js | 10 ++ packages/plugins/robot/package.json | 1 + .../components/footer-extension/McpServer.vue | 5 +- .../robot/src/composables/features/useMcp.ts | 146 +++++++++++++++--- .../plugins/robot/src/services/MCPHost.ts | 134 ++++++++++++++++ 7 files changed, 291 insertions(+), 23 deletions(-) create mode 100644 packages/plugins/robot/src/services/MCPHost.ts diff --git a/docs/advanced-features/new-ai-plugin-usage.md b/docs/advanced-features/new-ai-plugin-usage.md index bb346270c4..b9867ae592 100644 --- a/docs/advanced-features/new-ai-plugin-usage.md +++ b/docs/advanced-features/new-ai-plugin-usage.md @@ -248,6 +248,23 @@ customCompatibleAIModels: [ ] ``` +#### 自定义添加 MCP Servers +在options参数中可以通过如下参数配置MCP: +- mcpConfig.mcpServers: 该参数配置添加自定义MCP 服务器,添加后的 MCP 服务器可以在AI插件的Sender输入框MCP配置中进行管理(控制服务器开关、查看并切换工具开关)。 +配置示例格式如下: +```javascript +mcpConfig: { + mcpServers: { + // 支持添加自定义MCP Server服务器 + 'img-search': { + type: 'SSE', // 支持SSE和StreamableHttp两种类型 + icon: 'https://xxx', // 自定义图标 + url: 'https://xxxx/mcp' // 自定义MCP Server地址 + } + } +} +``` + #### 自定义 Agent 模式上下文功能 - enableResourceContext: 该参数配置是否在提示词上下文携带资源插件图片,AI 会在生成的页面中自动匹配合适的图片资源,默认开启 diff --git a/packages/common/composable/mcp/toolUtils.ts b/packages/common/composable/mcp/toolUtils.ts index a45e50cb58..5e170e2a54 100644 --- a/packages/common/composable/mcp/toolUtils.ts +++ b/packages/common/composable/mcp/toolUtils.ts @@ -60,6 +60,7 @@ export const registerTools = (state: IState, tools: ToolItem[]) => { if (toolInstance) { state.toolInstanceMap.set(name, toolInstance) + state.toolList.push(tool) } return toolInstance diff --git a/packages/plugins/robot/meta.js b/packages/plugins/robot/meta.js index 740dc50506..00f4b06401 100644 --- a/packages/plugins/robot/meta.js +++ b/packages/plugins/robot/meta.js @@ -15,6 +15,16 @@ export default { // 支持通过注册表传入chat和agent模式的实现 // chat: useCustomChatMode // agent: useCustomAgentMode + }, + mcpConfig: { + mcpServers: { + // 支持添加自定义MCP Server服务器 + // 'img-search': { + // type: 'SSE', // 支持SSE和StreamableHttp两种类型 + // icon: 'https://xxx', // 自定义图标 + // url: 'https://xxxx/mcp' // 自定义MCP Server地址 + // } + } } } } diff --git a/packages/plugins/robot/package.json b/packages/plugins/robot/package.json index c72b68f428..54091fd0e8 100644 --- a/packages/plugins/robot/package.json +++ b/packages/plugins/robot/package.json @@ -25,6 +25,7 @@ "license": "MIT", "homepage": "https://opentiny.design/tiny-engine", "dependencies": { + "@modelcontextprotocol/sdk": "^1.20.2", "@opentiny/tiny-engine-common": "workspace:*", "@opentiny/tiny-engine-meta-register": "workspace:*", "@opentiny/tiny-engine-utils": "workspace:*", diff --git a/packages/plugins/robot/src/components/footer-extension/McpServer.vue b/packages/plugins/robot/src/components/footer-extension/McpServer.vue index adb8993333..ca7ae0bea7 100644 --- a/packages/plugins/robot/src/components/footer-extension/McpServer.vue +++ b/packages/plugins/robot/src/components/footer-extension/McpServer.vue @@ -51,12 +51,13 @@ const { inUseMcpServers: installedPlugins, refreshMcpServerTools, updateMcpServerToolStatus, - updateMcpServerStatus + updateMcpServerStatus, + updateMcpServerToggle } = useMcpServer() // 插件状态切换 const handlePluginToggle = (plugin: PluginInfo, enabled: boolean) => { - plugin.enabled = enabled + updateMcpServerToggle(plugin, enabled) } // 插件展开状态变化 diff --git a/packages/plugins/robot/src/composables/features/useMcp.ts b/packages/plugins/robot/src/composables/features/useMcp.ts index c25b346552..2a207b3da3 100644 --- a/packages/plugins/robot/src/composables/features/useMcp.ts +++ b/packages/plugins/robot/src/composables/features/useMcp.ts @@ -3,13 +3,17 @@ import type { PluginInfo, PluginTool } from '@opentiny/tiny-robot' import { getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register' import type { McpTool } from '../../types/mcp.types' import type { RequestTool } from '../../types/chat.types' +import { getRobotServiceOptions } from '../../utils' +import { MCPHost } from '../../services/MCPHost' + +const mcpHost = new MCPHost() const ENGINE_MCP_SERVER: PluginInfo = { id: 'tiny-engine-mcp-server', name: 'Tiny Engine MCP 工具', icon: 'https://res.hc-cdn.com/lowcode-portal/1.1.80.20250515160330/assets/opentiny-tinyengine-logo-4f8a3801.svg', description: '使用TinyEngine设计器能力,如操作画布、编辑页面等', - added: true + addState: 'added' } const inUseMcpServers = ref([{ ...ENGINE_MCP_SERVER, enabled: true, expanded: true, tools: [] }]) @@ -18,6 +22,9 @@ const updateServerTools = (serverId: string, tools: PluginTool[]) => { const mcpServer = inUseMcpServers.value.find((item) => item.id === serverId) if (mcpServer) { mcpServer.tools = tools + if (!mcpHost.getClient(serverId) && serverId === ENGINE_MCP_SERVER.id) { + mcpHost.setClient(serverId, getMetaApi(META_SERVICE.McpService)?.getMcpClient()) + } } } @@ -31,13 +38,14 @@ const updateEngineTools = async () => { enabled: tool.status === 'enabled' })) updateServerTools(ENGINE_MCP_SERVER.id, engineTools) + return engineTools } const convertMCPToOpenAITools = (mcpTools: McpTool[]): RequestTool[] => { return mcpTools.map((tool: McpTool) => ({ type: 'function', function: { - name: tool.name, + name: tool.id || tool.name, description: tool.description || '', parameters: { type: 'object', @@ -50,12 +58,6 @@ const convertMCPToOpenAITools = (mcpTools: McpTool[]): RequestTool[] => { })) as RequestTool[] } -const getEngineServer = () => { - return inUseMcpServers.value.find((item) => item.id === ENGINE_MCP_SERVER.id) -} - -const isToolsEnabled = computed(() => getEngineServer()?.tools?.some((tool) => tool.enabled)) - const updateEngineServerToolStatus = (toolId: string, enabled: boolean) => { getMetaApi(META_SERVICE.McpService)?.updateTool?.(toolId, { enabled }) } @@ -67,15 +69,38 @@ const updateEngineServer = (engineServer: PluginInfo, enabled: boolean) => { }) } +const updateMcpServerToggle = async (server: PluginInfo, enabled: boolean) => { + if (server.id === ENGINE_MCP_SERVER.id) { + server.enabled = enabled + return + } + const inuseServer = inUseMcpServers.value.find((s) => s.id === server.id) + if (!inuseServer) { + return + } + try { + if (enabled) { + const { tools } = await connectMcpServer(inuseServer) // eslint-disable-line + inuseServer.tools = tools.map((tool) => ({ ...tool, enabled: true })) + } else { + await disconnectMcpServer(inuseServer.id) // eslint-disable-line + inuseServer.tools = [] + } + inuseServer.enabled = enabled + } catch (error) { + inuseServer.enabled = false + } +} + const updateMcpServerStatus = async (server: PluginInfo, added: boolean) => { // 市场添加状态修改 - server.added = added + server.addState = added ? 'added' : 'idle' if (added) { const newServer: PluginInfo = { ...server, id: server.id || `mcp-server-${Date.now()}`, enabled: true, - added: true, + addState: 'added', expanded: false, tools: server.tools || [] } @@ -103,33 +128,112 @@ const updateMcpServerToolStatus = (currentServer: PluginInfo, toolId: string, en } } +const updateCustomMcpServers = async () => { + const customMcpServers = getRobotServiceOptions().mcpConfig?.mcpServers || {} + if (Object.keys(customMcpServers).length > 0) { + if ( + Object.values(customMcpServers).some( + (server) => !['streamablehttp', 'sse'].includes(server.type?.toLowerCase()) || !server.url + ) + ) { + return { + result: 'failed', + message: '解析JSON失败,缺少type或url字段' + } + } + for (const [serverName, server] of Object.entries(customMcpServers)) { + if (inUseMcpServers.value.find((s) => s.id === serverName)) { + continue + } + const newServer: PluginInfo = { + id: serverName, + name: serverName, + icon: server.icon, + description: server.description, + enabled: false, + addState: 'added', + expanded: false, + type: server.type, + url: server.url, + tools: [] + } + inUseMcpServers.value.push(newServer) + } + } +} + +const connectMcpServer = (server: PluginInfo) => { + return mcpHost.connectToServer({ + id: server.id, + url: server.url, + type: server.type + }) +} + +const disconnectMcpServer = (serverId: string) => { + return mcpHost.disconnect(serverId) +} + const refreshMcpServerTools = () => { updateEngineTools() + updateCustomMcpServers() } -let llmTools: RequestTool[] | null = null - -const listTools = async (): Promise => { - const mcpTools = await getMetaApi(META_SERVICE.McpService)?.getMcpClient()?.listTools() - return mcpTools?.tools || [] +const toolsMap = computed(() => { + return inUseMcpServers.value + .filter((server) => server.enabled) + .reduce((acc, server) => { + server.tools + .filter((tool) => tool.enabled) + .forEach((tool) => { + acc[tool.id || tool.name] = { + server: server.id, + ...tool + } + }) + return acc + }, {}) +}) + +const callTool = async (toolId: string, args: Record) => { + return mcpHost.getClient(toolsMap.value[toolId]?.server)?.callTool({ name: toolId, arguments: args || {} }) || {} } -const callTool = async (toolId: string, args: Record) => - getMetaApi(META_SERVICE.McpService)?.getMcpClient()?.callTool({ name: toolId, arguments: args }) || {} +const tools = computed(() => { + return convertMCPToOpenAITools( + inUseMcpServers.value + .filter((server) => server.enabled) + .map((server) => server.tools.filter((tool) => tool.enabled)) + .flat() + ) +}) const getLLMTools = async () => { - const mcpTools = await getMetaApi(META_SERVICE.McpService)?.getMcpClient()?.listTools() - llmTools = convertMCPToOpenAITools(mcpTools?.tools || []) - return llmTools + const servers = inUseMcpServers.value.filter((server) => server.enabled && server.tools.length > 0) + const tools = await Promise.all( + servers.map(async (server) => { + const enabledTools = server.tools?.filter((tool) => tool.enabled).map((tool) => tool.id || tool.name) || [] + const client = mcpHost.getClient(server.id) + if (client) { + const listToolResult: { tools: McpTool[] } = await client.listTools() + return listToolResult.tools.filter((tool) => enabledTools.includes(tool.name)) + } + return [] + }) + ) + + return convertMCPToOpenAITools(tools.flat()) } +const isToolsEnabled = computed(() => tools.value.length > 0) + export default function useMcpServer() { return { inUseMcpServers, refreshMcpServerTools, updateMcpServerStatus, updateMcpServerToolStatus, - listTools, + updateMcpServerToggle, callTool, getLLMTools, isToolsEnabled diff --git a/packages/plugins/robot/src/services/MCPHost.ts b/packages/plugins/robot/src/services/MCPHost.ts new file mode 100644 index 0000000000..8f6e2a4f37 --- /dev/null +++ b/packages/plugins/robot/src/services/MCPHost.ts @@ -0,0 +1,134 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' +import type { Tool as MCPTool } from '@modelcontextprotocol/sdk/types.js' +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' + +export type Tool = MCPTool + +interface ServerConfig { + id?: string + url?: string + type: string + [key: string]: unknown +} + +type Transport = SSEClientTransport | StreamableHTTPClientTransport + +export class MCPHost { + private clients: Map = new Map() + private transports: Map = new Map() + + async connectToServer(serverConfig: ServerConfig): Promise<{ name: string; tools: Tool[] }> { + if (!serverConfig || !serverConfig.url) { + throw new Error(`Server configuration not found`) + } + // 未配置名字时使用随机uuid名字 + const serverId = serverConfig.id || crypto.randomUUID() + let transport: Transport + if (serverConfig.type.toLowerCase() === 'sse') { + transport = await this.createSSETransport(serverConfig.url) + } else if (serverConfig.type.toLowerCase() === 'streamablehttp') { + transport = await this.createStreamableHTTPTransport(serverConfig.url) + } else { + throw new Error(`Invalid server type: ${serverConfig.type}`) + } + const client = new Client( + { + name: serverId, + version: '1.0.0' + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {} + } + } + ) + await client.connect(transport) + + this.clients.set(serverId, client) + this.transports.set(serverId, transport) + // 列出可用工具 + const response = await client.listTools() + + return { + id: serverId, + tools: response.tools + } + } + + async listTools(serverId: string): Promise { + const client = this.clients.get(serverId) + if (!client) { + throw new Error(`Server configuration not found for: ${serverId}`) + } + const response = await client.listTools() + return response.tools + } + + setClient(serverId: string, client: Client, transport?: Transport): void { + if (!serverId || !client) return + this.clients.set(serverId, client) + if (transport) { + this.transports.set(serverId, transport) + } + } + + getClient(serverId: string): Client | null { + return this.clients.get(serverId) as Client + } + + async callTool({ + serverId, + toolName, + toolArgs + }: { + serverId: string + toolName: string + toolArgs: any + }): Promise { + const client = this.clients.get(serverId) + if (!client) { + throw new Error(`Server configuration not found for: ${serverId}`) + } + const response = await client.callTool({ + name: toolName, + arguments: toolArgs + }) + return response + } + + private async createSSETransport(url: string): Promise { + return new SSEClientTransport(new URL(url)) + } + private async createStreamableHTTPTransport(url: string): Promise { + return new StreamableHTTPClientTransport(new URL(url)) + } + + async disconnect(serverId: string): Promise { + const client = this.clients.get(serverId) + if (!client) { + throw new Error(`Server configuration not found for: ${serverId}`) + } + // await client.disconnect(); + this.clients.delete(serverId) + const transport = this.transports.get(serverId) + if (transport) { + await transport.close() + this.transports.delete(serverId) + } + } + + async cleanup(): Promise { + for (const transport of Array.from(this.transports.values())) { + await transport.close() + } + this.transports.clear() + this.clients.clear() + } + + hasActiveSessions(): boolean { + return this.clients.size > 0 + } +} From fe7bc529e3faffe1a54c43fa273029de17207cdb Mon Sep 17 00:00:00 2001 From: hexqi Date: Tue, 30 Dec 2025 14:10:10 +0800 Subject: [PATCH 2/2] chore: upgrade robot version to fix autoscroll --- packages/plugins/robot/package.json | 6 +++--- packages/plugins/robot/src/services/MCPHost.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/plugins/robot/package.json b/packages/plugins/robot/package.json index 54091fd0e8..01428f9ea5 100644 --- a/packages/plugins/robot/package.json +++ b/packages/plugins/robot/package.json @@ -29,9 +29,9 @@ "@opentiny/tiny-engine-common": "workspace:*", "@opentiny/tiny-engine-meta-register": "workspace:*", "@opentiny/tiny-engine-utils": "workspace:*", - "@opentiny/tiny-robot": "0.3.0", - "@opentiny/tiny-robot-kit": "0.3.0", - "@opentiny/tiny-robot-svgs": "0.3.0", + "@opentiny/tiny-robot": "0.3.1", + "@opentiny/tiny-robot-kit": "0.3.1", + "@opentiny/tiny-robot-svgs": "0.3.1", "@opentiny/tiny-schema-renderer": "1.0.0-beta.6", "@vueuse/core": "^9.13.0", "dompurify": "^3.0.1", diff --git a/packages/plugins/robot/src/services/MCPHost.ts b/packages/plugins/robot/src/services/MCPHost.ts index 8f6e2a4f37..eb5e08e6ce 100644 --- a/packages/plugins/robot/src/services/MCPHost.ts +++ b/packages/plugins/robot/src/services/MCPHost.ts @@ -18,7 +18,7 @@ export class MCPHost { private clients: Map = new Map() private transports: Map = new Map() - async connectToServer(serverConfig: ServerConfig): Promise<{ name: string; tools: Tool[] }> { + async connectToServer(serverConfig: ServerConfig): Promise<{ id: string; tools: Tool[] }> { if (!serverConfig || !serverConfig.url) { throw new Error(`Server configuration not found`) }