feat(MCPService): Implement IPC communication for server management and updates
This commit is contained in:
parent
75eb6680d8
commit
4ca2d7f9dc
@ -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()
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) => {
|
this.init().catch((err) => {
|
||||||
log.error('[MCP] Failed to initialize MCP service:', err)
|
log.error('[MCP] Failed to initialize MCP service:', err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
private getServersFromStore(): MCPServer[] {
|
}
|
||||||
return store.get('mcp.servers', []) as MCPServer[]
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateMCPServer = (server: MCPServer) => {
|
loadServers()
|
||||||
dispatch(_updateMCPServer(server))
|
}, [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 deleteMCPServer = (name: string) => {
|
const updateMCPServer = async (server: MCPServer) => {
|
||||||
dispatch(_deleteMCPServer(name))
|
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 setMCPServerActive = (name: string, isActive: boolean) => {
|
const deleteMCPServer = async (name: string) => {
|
||||||
dispatch(_setMCPServerActive({ name, isActive }))
|
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 = 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 = () => {
|
const getActiveMCPServers = () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user