feat(MCPService): Implement IPC communication for server management and updates

This commit is contained in:
Vaayne 2025-03-10 23:50:00 +08:00 committed by 亢奋猫
parent 75eb6680d8
commit 4ca2d7f9dc
3 changed files with 167 additions and 30 deletions

View File

@ -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()

View File

@ -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<string, any> = 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<void> | null = null
private serversLoaded = false
private serversLoadedPromise: Promise<void> | 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<void> {
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<MCPServer[]> {
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<void> {
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<void> {
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<void> {
await this.ensureInitialized()
try {

View File

@ -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 = () => {