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 // Register MCP handlers
ipcMain.on('mcp:servers-from-renderer', (_event, servers) => {
mcpService.setServers(servers)
})
ipcMain.handle('mcp:list-servers', async () => { ipcMain.handle('mcp:list-servers', async () => {
return mcpService.listAvailableServices() return mcpService.listAvailableServices()
}) })
@ -247,6 +251,11 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
return mcpService.cleanup() 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 // Clean up MCP services when app quits
app.on('before-quit', async () => { app.on('before-quit', async () => {
await mcpService.cleanup() await mcpService.cleanup()

View File

@ -1,12 +1,12 @@
import { MCPServer, MCPTool } from '@types' import { MCPServer, MCPTool } from '@types'
import log from 'electron-log' import log from 'electron-log'
import Store from 'electron-store'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
const store = new Store() import { windowService } from './WindowService'
export default class MCPService extends EventEmitter { export default class MCPService extends EventEmitter {
private servers: MCPServer[] = []
private activeServers: Map<string, any> = new Map() private activeServers: Map<string, any> = new Map()
private clients: { [key: string]: any } = {} private clients: { [key: string]: any } = {}
private Client: any private Client: any
@ -14,15 +14,73 @@ export default class MCPService extends EventEmitter {
private sseTransport: any private sseTransport: any
private initialized = false private initialized = false
private initPromise: Promise<void> | null = null private initPromise: Promise<void> | null = null
private serversLoaded = false
private serversLoadedPromise: Promise<void> | null = null
private serversLoadedResolve: (() => void) | null = null
constructor() { constructor() {
super() 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() { public async init() {
@ -35,6 +93,9 @@ export default class MCPService extends EventEmitter {
// Create and store the initialization promise // Create and store the initialization promise
this.initPromise = (async () => { this.initPromise = (async () => {
try { try {
// Wait for servers to be loaded from Redux
await this.waitForServers()
log.info('[MCP] Starting initialization') log.info('[MCP] Starting initialization')
this.Client = await this.importClient() this.Client = await this.importClient()
this.stoioTransport = await this.importStdioClientTransport() 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 // Mark as initialized before loading servers to prevent recursive initialization
this.initialized = true this.initialized = true
await this.load(this.getServersFromStore()) await this.load(this.getServers())
log.info('[MCP] Initialization completed successfully') log.info('[MCP] Initialization completed successfully')
} catch (err) { } catch (err) {
this.initialized = false // Reset flag on error this.initialized = false // Reset flag on error
@ -89,7 +150,7 @@ export default class MCPService extends EventEmitter {
public async listAvailableServices(): Promise<MCPServer[]> { public async listAvailableServices(): Promise<MCPServer[]> {
await this.ensureInitialized() await this.ensureInitialized()
return this.getServersFromStore() return this.getServers()
} }
private async ensureInitialized() { private async ensureInitialized() {
@ -102,13 +163,13 @@ export default class MCPService extends EventEmitter {
public async addServer(server: MCPServer): Promise<void> { public async addServer(server: MCPServer): Promise<void> {
await this.ensureInitialized() await this.ensureInitialized()
try { try {
const servers = this.getServersFromStore() const servers = this.getServers()
if (servers.some((s) => s.name === server.name)) { if (servers.some((s) => s.name === server.name)) {
throw new Error(`Server with name ${server.name} already exists`) throw new Error(`Server with name ${server.name} already exists`)
} }
servers.push(server) servers.push(server)
store.set('mcp.servers', servers) this.notifyReduxServersChanged(servers)
if (server.isActive) { if (server.isActive) {
await this.activate(server) await this.activate(server)
@ -122,7 +183,7 @@ export default class MCPService extends EventEmitter {
public async updateServer(server: MCPServer): Promise<void> { public async updateServer(server: MCPServer): Promise<void> {
await this.ensureInitialized() await this.ensureInitialized()
try { try {
const servers = this.getServersFromStore() const servers = this.getServers()
const index = servers.findIndex((s) => s.name === server.name) const index = servers.findIndex((s) => s.name === server.name)
if (index === -1) { if (index === -1) {
@ -137,7 +198,7 @@ export default class MCPService extends EventEmitter {
} }
servers[index] = server servers[index] = server
store.set('mcp.servers', servers) this.notifyReduxServersChanged(servers)
} catch (error) { } catch (error) {
log.error('Failed to update MCP server:', error) log.error('Failed to update MCP server:', error)
throw error throw error
@ -151,9 +212,10 @@ export default class MCPService extends EventEmitter {
await this.deactivate(serverName) await this.deactivate(serverName)
} }
const servers = this.getServersFromStore() const servers = this.getServers()
const filteredServers = servers.filter((s) => s.name !== serverName) const filteredServers = servers.filter((s) => s.name !== serverName)
store.set('mcp.servers', filteredServers) this.servers = filteredServers
this.notifyReduxServersChanged(filteredServers)
} catch (error) { } catch (error) {
log.error('Failed to delete MCP server:', error) log.error('Failed to delete MCP server:', error)
throw error throw error
@ -164,7 +226,7 @@ export default class MCPService extends EventEmitter {
await this.ensureInitialized() await this.ensureInitialized()
try { try {
const { name, isActive } = params const { name, isActive } = params
const servers = this.getServersFromStore() const servers = this.getServers()
const server = servers.find((s) => s.name === name) const server = servers.find((s) => s.name === name)
if (!server) { if (!server) {
@ -172,7 +234,7 @@ export default class MCPService extends EventEmitter {
} }
server.isActive = isActive server.isActive = isActive
store.set('mcp.servers', servers) this.notifyReduxServersChanged(servers)
if (isActive) { if (isActive) {
await this.activate(server) 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> { public async activate(server: MCPServer): Promise<void> {
await this.ensureInitialized() await this.ensureInitialized()
try { try {

View File

@ -1,30 +1,86 @@
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { import { setMCPServers as _setMCPServers } from '@renderer/store/mcp'
addMCPServer as _addMCPServer,
deleteMCPServer as _deleteMCPServer,
setMCPServerActive as _setMCPServerActive,
updateMCPServer as _updateMCPServer
} from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types' 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 = () => { export const useMCPServers = () => {
const mcpServers = useAppSelector((state) => state.mcp.servers) const mcpServers = useAppSelector((state) => state.mcp.servers)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const addMCPServer = (server: MCPServer) => { // Send servers to main process when they change in Redux
dispatch(_addMCPServer(server)) 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) => { const updateMCPServer = async (server: MCPServer) => {
dispatch(_updateMCPServer(server)) 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) => { const deleteMCPServer = async (name: string) => {
dispatch(_deleteMCPServer(name)) 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) => { const setMCPServerActive = async (name: string, isActive: boolean) => {
dispatch(_setMCPServerActive({ name, isActive })) 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 = () => { const getActiveMCPServers = () => {