diff --git a/src/main/ipc.ts b/src/main/ipc.ts index f918ea18..fe603091 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -213,6 +213,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ) // Register MCP handlers + ipcMain.on('mcp:servers-from-renderer', (_event, servers) => { + mcpService.setServers(servers) + }) + ipcMain.handle('mcp:list-servers', async () => { return mcpService.listAvailableServices() }) @@ -247,6 +251,11 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { return mcpService.cleanup() }) + // Listen for changes in MCP servers and notify renderer + mcpService.on('servers-updated', (servers) => { + mainWindow?.webContents.send('mcp:servers-updated', servers) + }) + // Clean up MCP services when app quits app.on('before-quit', async () => { await mcpService.cleanup() diff --git a/src/main/services/mcp.ts b/src/main/services/mcp.ts index f362ba10..26bebd7b 100644 --- a/src/main/services/mcp.ts +++ b/src/main/services/mcp.ts @@ -1,12 +1,12 @@ import { MCPServer, MCPTool } from '@types' import log from 'electron-log' -import Store from 'electron-store' import { EventEmitter } from 'events' import { v4 as uuidv4 } from 'uuid' -const store = new Store() +import { windowService } from './WindowService' export default class MCPService extends EventEmitter { + private servers: MCPServer[] = [] private activeServers: Map = new Map() private clients: { [key: string]: any } = {} private Client: any @@ -14,15 +14,73 @@ export default class MCPService extends EventEmitter { private sseTransport: any private initialized = false private initPromise: Promise | null = null + private serversLoaded = false + private serversLoadedPromise: Promise | null = null + private serversLoadedResolve: (() => void) | null = null constructor() { super() - this.init().catch((err) => { - log.error('[MCP] Failed to initialize MCP service:', err) + + // Create a promise that will be resolved when servers are loaded from Redux + this.serversLoadedPromise = new Promise((resolve) => { + this.serversLoadedResolve = resolve }) + + // Request servers from Redux on initialization + this.requestServers() } - private getServersFromStore(): MCPServer[] { - return store.get('mcp.servers', []) as MCPServer[] + + /** + * Request server data from renderer process Redux + */ + public requestServers(): void { + const mainWindow = windowService.getMainWindow() + if (mainWindow) { + log.info('[MCP] Requesting servers from Redux') + mainWindow.webContents.send('mcp:request-servers') + } else { + log.warn('[MCP] Main window not available, cannot request servers') + } + } + + /** + * Set servers received from Redux + */ + public setServers(servers: MCPServer[]): void { + log.info(`[MCP] Received ${servers.length} servers from Redux`) + this.servers = servers + this.serversLoaded = true + + // Resolve the promise to unlock initialization + if (this.serversLoadedResolve) { + this.serversLoadedResolve() + this.serversLoadedResolve = null + } + + // Initialize if not already initialized + if (!this.initialized) { + this.init().catch((err) => { + log.error('[MCP] Failed to initialize MCP service:', err) + }) + } + } + + /** + * Get the current servers + */ + private getServers(): MCPServer[] { + return this.servers + } + + /** + * Wait for servers to be loaded from Redux + */ + private async waitForServers(): Promise { + if (!this.serversLoaded && this.serversLoadedPromise) { + log.info('[MCP] Waiting for servers data from Redux...') + await this.serversLoadedPromise + log.info('[MCP] Servers received, continuing initialization') + } } public async init() { @@ -35,6 +93,9 @@ export default class MCPService extends EventEmitter { // Create and store the initialization promise this.initPromise = (async () => { try { + // Wait for servers to be loaded from Redux + await this.waitForServers() + log.info('[MCP] Starting initialization') this.Client = await this.importClient() this.stoioTransport = await this.importStdioClientTransport() @@ -43,7 +104,7 @@ export default class MCPService extends EventEmitter { // Mark as initialized before loading servers to prevent recursive initialization this.initialized = true - await this.load(this.getServersFromStore()) + await this.load(this.getServers()) log.info('[MCP] Initialization completed successfully') } catch (err) { this.initialized = false // Reset flag on error @@ -89,7 +150,7 @@ export default class MCPService extends EventEmitter { public async listAvailableServices(): Promise { await this.ensureInitialized() - return this.getServersFromStore() + return this.getServers() } private async ensureInitialized() { @@ -102,13 +163,13 @@ export default class MCPService extends EventEmitter { public async addServer(server: MCPServer): Promise { await this.ensureInitialized() try { - const servers = this.getServersFromStore() + const servers = this.getServers() if (servers.some((s) => s.name === server.name)) { throw new Error(`Server with name ${server.name} already exists`) } servers.push(server) - store.set('mcp.servers', servers) + this.notifyReduxServersChanged(servers) if (server.isActive) { await this.activate(server) @@ -122,7 +183,7 @@ export default class MCPService extends EventEmitter { public async updateServer(server: MCPServer): Promise { await this.ensureInitialized() try { - const servers = this.getServersFromStore() + const servers = this.getServers() const index = servers.findIndex((s) => s.name === server.name) if (index === -1) { @@ -137,7 +198,7 @@ export default class MCPService extends EventEmitter { } servers[index] = server - store.set('mcp.servers', servers) + this.notifyReduxServersChanged(servers) } catch (error) { log.error('Failed to update MCP server:', error) throw error @@ -151,9 +212,10 @@ export default class MCPService extends EventEmitter { await this.deactivate(serverName) } - const servers = this.getServersFromStore() + const servers = this.getServers() const filteredServers = servers.filter((s) => s.name !== serverName) - store.set('mcp.servers', filteredServers) + this.servers = filteredServers + this.notifyReduxServersChanged(filteredServers) } catch (error) { log.error('Failed to delete MCP server:', error) throw error @@ -164,7 +226,7 @@ export default class MCPService extends EventEmitter { await this.ensureInitialized() try { const { name, isActive } = params - const servers = this.getServersFromStore() + const servers = this.getServers() const server = servers.find((s) => s.name === name) if (!server) { @@ -172,7 +234,7 @@ export default class MCPService extends EventEmitter { } server.isActive = isActive - store.set('mcp.servers', servers) + this.notifyReduxServersChanged(servers) if (isActive) { await this.activate(server) @@ -185,6 +247,16 @@ export default class MCPService extends EventEmitter { } } + /** + * Notify Redux in the renderer process about server changes + */ + private notifyReduxServersChanged(servers: MCPServer[]): void { + const mainWindow = windowService.getMainWindow() + if (mainWindow) { + mainWindow.webContents.send('mcp:servers-changed', servers) + } + } + public async activate(server: MCPServer): Promise { await this.ensureInitialized() try { diff --git a/src/renderer/src/hooks/useMCPServers.ts b/src/renderer/src/hooks/useMCPServers.ts index 21b90c1b..b6a02379 100644 --- a/src/renderer/src/hooks/useMCPServers.ts +++ b/src/renderer/src/hooks/useMCPServers.ts @@ -1,30 +1,86 @@ import { useAppDispatch, useAppSelector } from '@renderer/store' -import { - addMCPServer as _addMCPServer, - deleteMCPServer as _deleteMCPServer, - setMCPServerActive as _setMCPServerActive, - updateMCPServer as _updateMCPServer -} from '@renderer/store/mcp' +import { setMCPServers as _setMCPServers } from '@renderer/store/mcp' import { MCPServer } from '@renderer/types' +import { useEffect } from 'react' + +const ipcRenderer = window.electron.ipcRenderer + +// Set up IPC listener for main process requests +ipcRenderer.on('mcp:request-servers', () => { + // This needs to access Redux outside of a hook, so we use the store directly + const { store } = require('@renderer/store') + const servers = store.getState().mcp.servers + ipcRenderer.send('mcp:servers-from-renderer', servers) +}) + +// Listen for server changes from main process +ipcRenderer.on('mcp:servers-changed', (_event, servers) => { + // This needs to dispatch outside of a hook, so we use the store directly + const { store } = require('@renderer/store') + store.dispatch(_setMCPServers(servers)) +}) export const useMCPServers = () => { const mcpServers = useAppSelector((state) => state.mcp.servers) const dispatch = useAppDispatch() - const addMCPServer = (server: MCPServer) => { - dispatch(_addMCPServer(server)) + // Send servers to main process when they change in Redux + useEffect(() => { + ipcRenderer.send('mcp:servers-from-renderer', mcpServers) + }, [mcpServers]) + + // Initial load of MCP servers from main process + useEffect(() => { + const loadServers = async () => { + try { + const servers = await ipcRenderer.invoke('mcp:list-servers') + dispatch(_setMCPServers(servers)) + } catch (error) { + console.error('Failed to load MCP servers:', error) + } + } + + loadServers() + }, [dispatch]) + + const addMCPServer = async (server: MCPServer) => { + try { + await ipcRenderer.invoke('mcp:add-server', server) + // Main process will send back updated servers via mcp:servers-changed + } catch (error) { + console.error('Failed to add MCP server:', error) + throw error + } } - const updateMCPServer = (server: MCPServer) => { - dispatch(_updateMCPServer(server)) + const updateMCPServer = async (server: MCPServer) => { + try { + await ipcRenderer.invoke('mcp:update-server', server) + // Main process will send back updated servers via mcp:servers-changed + } catch (error) { + console.error('Failed to update MCP server:', error) + throw error + } } - const deleteMCPServer = (name: string) => { - dispatch(_deleteMCPServer(name)) + const deleteMCPServer = async (name: string) => { + try { + await ipcRenderer.invoke('mcp:delete-server', name) + // Main process will send back updated servers via mcp:servers-changed + } catch (error) { + console.error('Failed to delete MCP server:', error) + throw error + } } - const setMCPServerActive = (name: string, isActive: boolean) => { - dispatch(_setMCPServerActive({ name, isActive })) + const setMCPServerActive = async (name: string, isActive: boolean) => { + try { + await ipcRenderer.invoke('mcp:set-server-active', { name, isActive }) + // Main process will send back updated servers via mcp:servers-changed + } catch (error) { + console.error('Failed to set MCP server active status:', error) + throw error + } } const getActiveMCPServers = () => {