From 13654389c6fc6157c5104841aa342a38d83817e4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 01:38:15 -0700 Subject: [PATCH] update state --- .../void/browser/react/src/util/services.tsx | 25 +- .../src/void-settings-tsx/MCPServersList.tsx | 99 +++----- .../contrib/void/common/mcpService.ts | 234 +++++++----------- .../contrib/void/common/mcpServiceTypes.ts | 12 +- .../contrib/void/electron-main/mcpChannel.ts | 30 +-- 5 files changed, 165 insertions(+), 235 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 3d71f2ea..de5ff82a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ import React, { useState, useEffect, useCallback } from 'react' -import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js' +import { MCPServerState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js' import { IDisposable } from '../../../../../../../base/common/lifecycle.js' import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js' import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js' @@ -79,6 +79,8 @@ const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => const commandBarURIStateListeners: Set<(uri: URI) => void> = new Set(); const activeURIListeners: Set<(uri: URI | null) => void> = new Set(); +const mcpListeners: Set<() => void> = new Set() + // must call this before you can use any of the hooks below // this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it! @@ -96,9 +98,10 @@ export const _registerServices = (accessor: ServicesAccessor) => { editCodeService: accessor.get(IEditCodeService), voidCommandBarService: accessor.get(IVoidCommandBarService), modelService: accessor.get(IModelService), + mcpService: accessor.get(IMCPService), } - const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices + const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService, mcpService } = stateServices @@ -165,6 +168,11 @@ export const _registerServices = (accessor: ServicesAccessor) => { }) ) + disposables.push( + mcpService.onDidChangeState(() => { + mcpListeners.forEach(l => l()) + }) + ) return disposables @@ -378,3 +386,16 @@ export const useActiveURI = () => { + +export const useMCPServiceState = () => { + const accessor = useAccessor() + const mcpService = accessor.get('IMCPService') + const [s, ss] = useState(mcpService.state) + useEffect(() => { + const listener = () => { ss(mcpService.state) } + mcpListeners.add(listener); + return () => { mcpListeners.delete(listener) }; + }, []); + return s +} + diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/MCPServersList.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/MCPServersList.tsx index 4bc78e6a..95ac299b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/MCPServersList.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/MCPServersList.tsx @@ -1,7 +1,7 @@ import { VoidSwitch } from '../util/inputs.js'; -import { MCPConfigFileParseErrorResponse, MCPEventType, MCPEventResponse, MCPServerObject, MCPServerOfName } from '../../../../common/mcpServiceTypes.js'; +import { MCPConfigFileParseErrorResponse, MCPServerEventType, MCPServerEventResponse, MCPServerObject, MCPServerOfName } from '../../../../common/mcpServiceTypes.js'; import { useEffect, useState } from 'react'; -import { useAccessor } from '../util/services.js'; +import { useAccessor, useMCPServiceState } from '../util/services.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; // Command display component @@ -96,80 +96,49 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) // Main component that renders the list of servers const MCPServersList = () => { - const accessor = useAccessor(); - const mcpService = accessor.get('IMCPService'); - const [mcpServers, setMCPServers] = useState({}); - const [mcpConfigError, setMCPConfigError] = useState(null); + const mcpServiceState = useMCPServiceState() - // Get all servers from MCPConfigService - useEffect(() => { - console.log('RUNNING MCPServersList EFFECT'); - // Initial fetch - const servers = mcpService.getMCPServerOfName(); - if (servers) { - // Do something with the servers - console.log('MCP Servers:', servers); - setMCPServers(servers); - } + // const handleListeners = (e: MCPServerEventResponse | MCPConfigFileParseErrorResponse) => { + // if (e.response.type === 'config-file-error') { + // // Handle the config error event + // const { error } = e.response; + // setMCPConfigError(error); + // return; + // } + // if (e.response.type === 'add' || e.response.type === 'update' || e.response.type === 'loading') { + // // Handle the add event + // const { name, newServer } = e.response; + // setMCPServers(prevServers => ({ + // ...prevServers, + // [name]: newServer + // })); + // return; + // } + // if (e.response.type === 'delete') { + // // Handle the delete event + // const { name, prevServer } = e.response; + // setMCPServers(prevServers => { + // const newServers = { ...prevServers }; + // delete newServers[name]; + // return newServers; + // }); + // return; + // } + // throw new Error('Event not handled'); + // } - // Set up listeners for server events - const disposables: IDisposable[] = [] - - disposables.push(mcpService.onDidAddServer(handleListeners)); - disposables.push(mcpService.onDidDeleteServer(handleListeners)); - disposables.push(mcpService.onDidUpdateServer(handleListeners)); - - // disposables.push(mcpService.onLoadingServers(handleListeners)); - disposables.push(mcpService.onConfigParsingError(handleListeners)); - - // Clean up subscription when component unmounts - return () => { - console.log('Cleaning up subscriptions'); - disposables.forEach(disposable => disposable.dispose()); - }; - - }, [mcpService]); - - const handleListeners = (e: MCPEventResponse | MCPConfigFileParseErrorResponse) => { - if (e.response.type === 'config-file-error') { - // Handle the config error event - const { error } = e.response; - setMCPConfigError(error); - return; - } - if (e.response.type === 'add' || e.response.type === 'update' || e.response.type === 'loading') { - // Handle the add event - const { name, newServer } = e.response; - setMCPServers(prevServers => ({ - ...prevServers, - [name]: newServer - })); - return; - } - if (e.response.type === 'delete') { - // Handle the delete event - const { name, prevServer } = e.response; - setMCPServers(prevServers => { - const newServers = { ...prevServers }; - delete newServers[name]; - return newServers; - }); - return; - } - throw new Error('Event not handled'); - } return (
- {!mcpConfigError && Object.entries(mcpServers).map(([name, server]) => ( + {!mcpServiceState.error && Object.entries(mcpServiceState.mcpServerOfName).map(([name, server]) => (
))} - {mcpConfigError && ( + {mcpServiceState.error && (
- {mcpConfigError} + {mcpServiceState.error}
)}
diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index dd628e1a..45d33569 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ import { URI } from '../../../../base/common/uri.js'; -import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IFileService } from '../../../../platform/files/common/files.js'; @@ -15,58 +15,55 @@ import { IProductService } from '../../../../platform/product/common/productServ import { VSBuffer } from '../../../../base/common/buffer.js'; import { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; -import { MCPServerOfName, MCPConfigFileType, MCPConfigFileParseErrorResponse, MCPAddResponse, MCPUpdateResponse, MCPDeleteResponse, MCPEventResponse } from './mcpServiceTypes.js'; +import { MCPServerOfName, MCPConfigFileType, MCPAddServerResponse, MCPUpdateServerResponse, MCPDeleteServerResponse, MCPServerEventResponse, MCPServerObject } from './mcpServiceTypes.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { InternalToolInfo } from './prompt/prompts.js'; import { IVoidSettingsService } from './voidSettingsService.js'; import { MCPServerStateOfName } from './voidSettingsTypes.js'; + +type MCPState = { + mcpServerOfName: MCPServerOfName, + error: string | undefined, +} + export interface IMCPService { readonly _serviceBrand: undefined; revealMCPConfigFile(): Promise; - getMCPServerOfName(): MCPServerOfName; - - getCurrentTools(): InternalToolInfo[]; - toggleServer(serverName: string, isOn: boolean): Promise; - onDidAddServer: Event; - onDidUpdateServer: Event; - onDidDeleteServer: Event; + readonly state: MCPState; + onDidChangeState: Event; - // onLoadingServers: Event; - onConfigParsingError: Event; + getCurrentMCPTools(): InternalToolInfo[]; } export const IMCPService = createDecorator('mcpConfigService'); + + +const MCP_CONFIG_FILE_NAME = 'mcp.json'; +const MCP_CONFIG_SAMPLE = { mcpServers: {} } +const MCP_CONFIG_SAMPLE_STRING = JSON.stringify(MCP_CONFIG_SAMPLE, null, 2); + class MCPService extends Disposable implements IMCPService { _serviceBrand: undefined; - private readonly MCP_CONFIG_FILE_NAME = 'mcp.json'; - private readonly MCP_CONFIG_SAMPLE = { - mcpServers: {}, - } - private readonly MCP_CONFIG_SAMPLE_STRING = JSON.stringify(this.MCP_CONFIG_SAMPLE, null, 2); - private mcpFileWatcher: IDisposable | null = null; + private readonly channel: IChannel // MCPChannel // list of MCP servers pulled from mcpChannel - private readonly mcpServers: MCPServerOfName = {} + state: MCPState = { + mcpServerOfName: {}, + error: undefined + } // Emitters for server events - private readonly _onDidAddServer = new Emitter(); - private readonly _onDidUpdateServer = new Emitter(); - private readonly _onDidDeleteServer = new Emitter(); - public readonly onDidAddServer = this._onDidAddServer.event; - public readonly onDidUpdateServer = this._onDidUpdateServer.event; - public readonly onDidDeleteServer = this._onDidDeleteServer.event; + private readonly _onDidChangeState = new Emitter(); + public readonly onDidChangeState = this._onDidChangeState.event; - // private readonly _onLoadingServers = new Emitter(); - // public readonly onLoadingServers = this._onLoadingServers.event; - - private readonly _onConfigParsingError = new Emitter(); - public readonly onConfigParsingError = this._onConfigParsingError.event; + // private readonly _onLoadingServersChange = new Emitter(); + // public readonly onLoadingServersChange = this._onLoadingServersChange.event; constructor( @IFileService private readonly fileService: IFileService, @@ -80,108 +77,79 @@ class MCPService extends Disposable implements IMCPService { // Register the service with the instantiation service this.channel = this.mainProcessService.getChannel('void-channel-mcp') // Register listeners for the channel - this._register((this.channel.listen('onAdd_server') satisfies Event)(e => this._onServerEvent(e))); - this._register((this.channel.listen('onUpdate_server') satisfies Event)(e => this._onServerEvent(e))); - this._register((this.channel.listen('onDelete_server') satisfies Event)(e => this._onServerEvent(e))); + this._register((this.channel.listen('onAdd_server') satisfies Event)(e => this._onGetServerEvent(e))); + this._register((this.channel.listen('onUpdate_server') satisfies Event)(e => this._onGetServerEvent(e))); + this._register((this.channel.listen('onDelete_server') satisfies Event)(e => this._onGetServerEvent(e))); // this._register((this.channel.listen('onLoading_server') satisfies Event)(e => this._onServerEvent(e))); // Initialize the service this._initialize(); } - // This method is called when the service is disposed - override async dispose(): Promise { - // Custom cleanup logic goes here - console.log('MCPService is being disposed'); - - // Call _removeMCPConfigFileWatch to clean up file watchers - this._removeMCPConfigFileWatch().catch(err => { - console.error('Error removing MCP config file watch:', err); - }); - - // Close all servers in electron main process - await this.channel.call('closeAllServers') - - // Always call the parent class dispose method to ensure proper cleanup - super.dispose(); - } - - private async _initialize() { try { - // Get the MCP config file path - const mcpConfigUri = await this._getMCPConfigFilePath(); + await this.voidSettingsService.waitForInitState; - // create file if it doesn't exist + // Create .mcpConfig if it doesn't exist + const mcpConfigUri = await this._getMCPConfigFilePath(); const fileExists = await this._configFileExists(mcpConfigUri); if (!fileExists) { await this._createMCPConfigFile(mcpConfigUri); console.log('MCP Config file created:', mcpConfigUri.toString()); } - // Wait for VoidSettingsService to initialize before proceeding - await this.voidSettingsService.waitForInitState; - - // read MCP config file - const mcpConfig = await this._parseMCPConfigFile(); - if (!mcpConfig) throw new Error(`MCP config file not found`); - if (!mcpConfig.mcpServers) throw new Error(`MCP config file did not have an 'mcpServers' field`); - - // update state based on config file - const updatedServerStates = await this._handleServerStateChange(mcpConfig); - - // Setup the server list - this.channel.call('setupServers', { mcpConfig, serverStates: updatedServerStates }) - + await this._updateStateWithCurrentConfigFile(); // Add a watcher to the MCP config file - await this._setMCPConfigFileWatch(); + await this._addMCPConfigFileWatcher(); } catch (error) { console.error('Error initializing MCPService:', error); } } - private async _onServerEvent(e: MCPEventResponse) { - const r = e.response - if (r.type === 'add') { - this.mcpServers[r.name] = r.newServer; - this._onDidAddServer.fire(e as MCPAddResponse); - } - if (r.type === 'update') { - this.mcpServers[r.name] = r.newServer; - this._onDidUpdateServer.fire(e as MCPUpdateResponse); - } - if (r.type === 'delete') { - delete this.mcpServers[r.name]; - this._onDidDeleteServer.fire(e as MCPDeleteResponse); - } - // if (e.response.event === 'loading') { - // // Update the mcpServers list - // this.mcpServers[e.response.name] = e.response.newServer; - // // Fire the event to notify browser - // this._onLoadingServers.fire(e); - // } + private async _onGetServerEvent(e: MCPServerEventResponse) { + this._setMCPServer(e.response.name, e.response.newServer) } + + private readonly _setMCPServer = async (serverName: string, newServer: MCPServerObject | undefined) => { + this.state = { + ...this.state, + mcpServerOfName: { + ...this.state.mcpServerOfName, + ...newServer === undefined ? {} : { [serverName]: newServer, } + } + } + this._onDidChangeState.fire(); + } + + private readonly _setHasError = async (hasError: string | undefined) => { + this.state = { + ...this.state, + error: hasError ? `MCP config file not found` : undefined, + } + this._onDidChangeState.fire(); + } + + + // Create the file/directory if it doesn't exist private async _createMCPConfigFile(mcpConfigUri: URI): Promise { await this.fileService.createFile(mcpConfigUri.with({ path: mcpConfigUri.path })); - const buffer = VSBuffer.fromString(this.MCP_CONFIG_SAMPLE_STRING); + const buffer = VSBuffer.fromString(MCP_CONFIG_SAMPLE_STRING); await this.fileService.writeFile(mcpConfigUri, buffer); } - // Remove any previous config parsing error - // This isn't super intuitive, but it works private async _parseMCPConfigFile(): Promise { - // clear error - this._onConfigParsingError.fire({ - response: { - type: 'config-file-error', - error: null - } - }); + // TODO!!!!!!! double check this + // this._onConfigParsingError.fire({ + // response: { + // type: 'config-file-error', + // error: null + // } + // }); // Process config file const mcpConfigUri = await this._getMCPConfigFilePath(); @@ -196,53 +164,28 @@ class MCPService extends Disposable implements IMCPService { return configJson as MCPConfigFileType; } catch (error) { const fullError = `Error parsing MCP config file: ${error}`; - console.error(fullError); - this._onConfigParsingError.fire({ - response: { - type: 'config-file-error', - error: fullError - } - }); + this._setHasError(fullError) return null; } } - private async _setMCPConfigFileWatch(): Promise { + private async _addMCPConfigFileWatcher(): Promise { const mcpConfigUri = await this._getMCPConfigFilePath(); + this._register( + this.fileService.watch(mcpConfigUri) + ) - // Watch the file for changes - this.mcpFileWatcher = this.fileService.watch(mcpConfigUri); - - // Listen for changes this._register(this.fileService.onDidFilesChange(async e => { - // Handle file changes - if (e.contains(mcpConfigUri)) { - const mcpConfig = await this._parseMCPConfigFile(); - if (mcpConfig && mcpConfig.mcpServers) { - const updatedServerStates = await this._handleServerStateChange(mcpConfig); - - // Set up the server list - this.channel.call('setupServers', { mcpConfig, serverStates: updatedServerStates }) - } - } + if (!e.contains(mcpConfigUri)) return + await this._updateStateWithCurrentConfigFile(); })); } - private async _removeMCPConfigFileWatch(): Promise { - if (this.mcpFileWatcher) { - this.mcpFileWatcher.dispose(); - this.mcpFileWatcher = null; - } - } - // Client-side functions public async revealMCPConfigFile(): Promise { try { - // Get the MCP config file path const mcpConfigUri = await this._getMCPConfigFilePath(); - - // Open the MCP config file in the editor await this.editorService.openEditor({ resource: mcpConfigUri, options: { @@ -250,19 +193,13 @@ class MCPService extends Disposable implements IMCPService { revealIfOpened: true, } }); - } catch (error) { console.error('Error opening MCP config file:', error); } } - public getMCPServerOfName(): MCPServerOfName { - // Call the getMCPServers method in the main process - return this.mcpServers; - } - - public getCurrentTools(): InternalToolInfo[] { - const allTools = Object.values(this.mcpServers).flatMap(server => { + public getCurrentMCPTools(): InternalToolInfo[] { + const allTools = Object.values(this.state.mcpServerOfName).flatMap(server => { return server.tools.map(tool => { // Convert JsonSchema to the expected format const convertedParams: { [paramName: string]: { description: string } } = {}; @@ -295,31 +232,32 @@ class MCPService extends Disposable implements IMCPService { // utility functions private async _getMCPConfigFilePath(): Promise { - // Get the appropriate directory based on dev mode const appName = this.productService.dataFolderName const userHome = await this.pathService.userHome(); - const mcpConfigPath = join(userHome.path, appName, this.MCP_CONFIG_FILE_NAME); + const mcpConfigPath = join(userHome.path, appName, MCP_CONFIG_FILE_NAME); return URI.file(mcpConfigPath); } private async _configFileExists(mcpConfigUri: URI): Promise { try { - // Try to get file stats - if it succeeds, the file exists await this.fileService.stat(mcpConfigUri); return true; } catch (error) { - // File doesn't exist or can't be accessed return false; } } // Handle server state changes - private async _handleServerStateChange(mcpConfig: MCPConfigFileType): Promise { - // Get the server states from Void Settings Service - const savedServerStates = this.voidSettingsService.state.mcpServerStateOfName; + private async _updateStateWithCurrentConfigFile(): Promise { - // Parse the MCP config file for servers - const availableServers = Object.keys(mcpConfig.mcpServers); + this._setHasError(undefined) + + const mcpConfigFile = await this._parseMCPConfigFile(); + if (!mcpConfigFile) { console.log(`Not setting state: MCP config file not found`); return } + if (!mcpConfigFile?.mcpServers) { console.log(`Not setting state: MCP config file did not have an 'mcpServers' field`); return } + + const savedServerStates = this.voidSettingsService.state.mcpServerStateOfName; + const availableServers = Object.keys(mcpConfigFile.mcpServers); // Handle added servers const addedServers = availableServers.filter(serverName => !savedServerStates[serverName]); @@ -341,7 +279,7 @@ class MCPService extends Disposable implements IMCPService { return acc; }, {} as MCPServerStateOfName); - return updatedServers; + this.channel.call('refreshMCPServers', { mcpConfig: mcpConfigFile, serverStates: updatedServers }) } } diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index d7e0cfac..50f81d0f 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -165,7 +165,7 @@ export type EventMCPServerSetupOnError = Parameters(), - onUpdate: new Emitter(), - onDelete: new Emitter(), - // onChangeLoading: new Emitter(), + onAdd: new Emitter(), + onUpdate: new Emitter(), + onDelete: new Emitter(), + // onResult: new Emitter<>(), + // onError: new Emitter<>(), + // onChangeLoading: new Emitter(), // really onStart } // toolCall: { // success: new Emitter(), @@ -63,9 +65,9 @@ export class MCPChannel implements IServerChannel { // }, } satisfies { serverEvent: { - onAdd: Emitter, - onUpdate: Emitter, - onDelete: Emitter, + onAdd: Emitter, + onUpdate: Emitter, + onDelete: Emitter, // onChangeLoading: Emitter, } } @@ -90,11 +92,11 @@ export class MCPChannel implements IServerChannel { // browser uses this to call (see this.channel.call() in mcpConfigService.ts for all usages) async call(_: unknown, command: string, params: any): Promise { try { - if (command === 'setupServers') { - await this._setupServers(params) + if (command === 'refreshMCPServers') { + await this._refreshMCPServers(params) } - else if (command === 'closeAllServers') { - await this._closeAllServers() + else if (command === 'closeAllMCPServers') { + await this._closeAllMCPServers() } else if (command === 'toggleServer') { await this._toggleServer(params.serverName, params.isOn) @@ -114,7 +116,7 @@ export class MCPChannel implements IServerChannel { // call functions - private async _setupServers(params: { mcpConfig: MCPConfigFileType, serverStates: MCPServerStateOfName }) { + private async _refreshMCPServers(params: { mcpConfig: MCPConfigFileType, serverStates: MCPServerStateOfName }) { const { mcpConfig, serverStates } = params @@ -313,7 +315,7 @@ export class MCPChannel implements IServerChannel { } } - private async _closeAllServers() { + private async _closeAllMCPServers() { for (const serverName in this.clients) { await this._closeServer(serverName) this._removeServer(serverName)