From 0a4131379a32186c8b2e1b1ec63c10d3ac4cb4ee Mon Sep 17 00:00:00 2001 From: LiuVaayne <10231735+vaayne@users.noreply.github.com> Date: Sat, 26 Apr 2025 11:09:58 +0800 Subject: [PATCH] feat: Add MCP server installation via URL protocol (#5351) * Add MCP server installation via URL protocol Implement handler for cherrystudio://mcp/install URLs to add MCP servers from encoded configuration data. Supports multiple server configuration formats and adds a new IPC channel for server addition. * feat: Enhance MCP protocol handling and navigation - Implemented navigation to the '/settings/mcp' page using executeJavaScript in the MCP protocol URL handler. - Updated NavigationService to expose the navigate function globally for easier access in the application. - Added NavigateFunction type to the global environment for improved type safety in navigation operations. --------- Co-authored-by: kangfenmao --- packages/shared/IpcChannel.ts | 1 + src/main/services/ProtocolClient.ts | 7 ++ src/main/services/urlschema/mcp-install.ts | 76 +++++++++++++++++++ src/renderer/src/env.d.ts | 2 + src/renderer/src/hooks/useMCPServers.ts | 3 + .../src/services/NavigationService.ts | 1 + 6 files changed, 90 insertions(+) create mode 100644 src/main/services/urlschema/mcp-install.ts diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 5f8949116b..63c340e7a7 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -38,6 +38,7 @@ export enum IpcChannel { MiniWindow_SetPin = 'miniwindow:set-pin', // Mcp + Mcp_AddServer = 'mcp:add-server', Mcp_RemoveServer = 'mcp:remove-server', Mcp_RestartServer = 'mcp:restart-server', Mcp_StopServer = 'mcp:stop-server', diff --git a/src/main/services/ProtocolClient.ts b/src/main/services/ProtocolClient.ts index e742a4e917..84a8c007c5 100644 --- a/src/main/services/ProtocolClient.ts +++ b/src/main/services/ProtocolClient.ts @@ -1,3 +1,4 @@ +import { handleMcpProtocolUrl } from './urlschema/mcp-install' import { windowService } from './WindowService' export const CHERRY_STUDIO_PROTOCOL = 'cherrystudio' @@ -22,6 +23,12 @@ export function handleProtocolUrl(url: string) { const urlObj = new URL(url) const params = new URLSearchParams(urlObj.search) + switch (urlObj.hostname.toLowerCase()) { + case 'mcp': + handleMcpProtocolUrl(urlObj) + return + } + // You can send the data to your renderer process const mainWindow = windowService.getMainWindow() diff --git a/src/main/services/urlschema/mcp-install.ts b/src/main/services/urlschema/mcp-install.ts new file mode 100644 index 0000000000..e5f0a76501 --- /dev/null +++ b/src/main/services/urlschema/mcp-install.ts @@ -0,0 +1,76 @@ +import { nanoid } from '@reduxjs/toolkit' +import { IpcChannel } from '@shared/IpcChannel' +import { MCPServer } from '@types' +import Logger from 'electron-log' + +import { windowService } from '../WindowService' + +function installMCPServer(server: MCPServer) { + const mainWindow = windowService.getMainWindow() + + if (!server.id) { + server.id = nanoid() + } + + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send(IpcChannel.Mcp_AddServer, server) + } +} + +function installMCPServers(servers: Record) { + for (const name in servers) { + const server = servers[name] + if (!server.name) { + server.name = name + } + installMCPServer(server) + } +} + +export function handleMcpProtocolUrl(url: URL) { + const params = new URLSearchParams(url.search) + switch (url.pathname) { + case '/install': { + // jsonConfig example: + // { + // "mcpServers": { + // "everything": { + // "command": "npx", + // "args": [ + // "-y", + // "@modelcontextprotocol/server-everything" + // ] + // } + // } + // } + // cherrystudio://mcp/install?servers={base64Encode(JSON.stringify(jsonConfig))} + const data = params.get('servers') + if (data) { + const stringify = Buffer.from(data, 'base64').toString('utf8') + Logger.info('install MCP servers from urlschema: ', stringify) + const jsonConfig = JSON.parse(stringify) + Logger.info('install MCP servers from urlschema: ', jsonConfig) + + // support both {mcpServers: [servers]}, [servers] and {server} + if (jsonConfig.mcpServers) { + installMCPServers(jsonConfig.mcpServers) + } else if (Array.isArray(jsonConfig)) { + for (const server of jsonConfig) { + installMCPServer(server) + } + } else { + installMCPServer(jsonConfig) + } + } + + const mainWindow = windowService.getMainWindow() + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.executeJavaScript("window.navigate('/settings/mcp')") + } + break + } + default: + console.error(`Unknown MCP protocol URL: ${url}`) + break + } +} diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts index ec6be6b337..2ef23faf77 100644 --- a/src/renderer/src/env.d.ts +++ b/src/renderer/src/env.d.ts @@ -3,6 +3,7 @@ import type KeyvStorage from '@kangfenmao/keyv-storage' import { MessageInstance } from 'antd/es/message/interface' import { HookAPI } from 'antd/es/modal/useModal' +import { NavigateFunction } from 'react-router-dom' interface ImportMetaEnv { VITE_RENDERER_INTEGRATED_MODEL: string @@ -20,5 +21,6 @@ declare global { keyv: KeyvStorage mermaid: any store: any + navigate: NavigateFunction } } diff --git a/src/renderer/src/hooks/useMCPServers.ts b/src/renderer/src/hooks/useMCPServers.ts index 316f89d9dd..90d6e6fec4 100644 --- a/src/renderer/src/hooks/useMCPServers.ts +++ b/src/renderer/src/hooks/useMCPServers.ts @@ -10,6 +10,9 @@ const ipcRenderer = window.electron.ipcRenderer ipcRenderer.on(IpcChannel.Mcp_ServersChanged, (_event, servers) => { store.dispatch(setMCPServers(servers)) }) +ipcRenderer.on(IpcChannel.Mcp_AddServer, (_event, server: MCPServer) => { + store.dispatch(addMCPServer(server)) +}) export const useMCPServers = () => { const mcpServers = useAppSelector((state) => state.mcp.servers) diff --git a/src/renderer/src/services/NavigationService.ts b/src/renderer/src/services/NavigationService.ts index 67e6f67c02..dc678825f4 100644 --- a/src/renderer/src/services/NavigationService.ts +++ b/src/renderer/src/services/NavigationService.ts @@ -10,6 +10,7 @@ const NavigationService: INavigationService = { setNavigate: (navigateFunc: NavigateFunction): void => { NavigationService.navigate = navigateFunc + window.navigate = NavigationService.navigate } }