mirror of
https://github.com/voideditor/void.git
synced 2026-10-12 02:24:20 +00:00
update state
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+34
-65
@@ -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<MCPServerOfName>({});
|
||||
const [mcpConfigError, setMCPConfigError] = useState<string | null>(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 (
|
||||
<div className="text-white rounded-md py-4">
|
||||
<div>
|
||||
{!mcpConfigError && Object.entries(mcpServers).map(([name, server]) => (
|
||||
{!mcpServiceState.error && Object.entries(mcpServiceState.mcpServerOfName).map(([name, server]) => (
|
||||
<div className="py-2" key={name}>
|
||||
<MCPServer name={name} server={server} />
|
||||
</div>
|
||||
))}
|
||||
{mcpConfigError && (
|
||||
{mcpServiceState.error && (
|
||||
<div className="text-red-500 text-sm font-medium">
|
||||
{mcpConfigError}
|
||||
{mcpServiceState.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<void>;
|
||||
getMCPServerOfName(): MCPServerOfName;
|
||||
|
||||
getCurrentTools(): InternalToolInfo[];
|
||||
|
||||
toggleServer(serverName: string, isOn: boolean): Promise<void>;
|
||||
|
||||
onDidAddServer: Event<MCPAddResponse>;
|
||||
onDidUpdateServer: Event<MCPUpdateResponse>;
|
||||
onDidDeleteServer: Event<MCPDeleteResponse>;
|
||||
readonly state: MCPState;
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
// onLoadingServers: Event<MCPServerEventLoadingParam>;
|
||||
onConfigParsingError: Event<MCPConfigFileParseErrorResponse>;
|
||||
getCurrentMCPTools(): InternalToolInfo[];
|
||||
}
|
||||
|
||||
export const IMCPService = createDecorator<IMCPService>('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<MCPAddResponse>();
|
||||
private readonly _onDidUpdateServer = new Emitter<MCPUpdateResponse>();
|
||||
private readonly _onDidDeleteServer = new Emitter<MCPDeleteResponse>();
|
||||
public readonly onDidAddServer = this._onDidAddServer.event;
|
||||
public readonly onDidUpdateServer = this._onDidUpdateServer.event;
|
||||
public readonly onDidDeleteServer = this._onDidDeleteServer.event;
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
public readonly onDidChangeState = this._onDidChangeState.event;
|
||||
|
||||
// private readonly _onLoadingServers = new Emitter<MCPServerEventLoadingParam>();
|
||||
// public readonly onLoadingServers = this._onLoadingServers.event;
|
||||
|
||||
private readonly _onConfigParsingError = new Emitter<MCPConfigFileParseErrorResponse>();
|
||||
public readonly onConfigParsingError = this._onConfigParsingError.event;
|
||||
// private readonly _onLoadingServersChange = new Emitter<MCPServerEventLoadingParam>();
|
||||
// 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<MCPAddResponse>)(e => this._onServerEvent(e)));
|
||||
this._register((this.channel.listen('onUpdate_server') satisfies Event<MCPUpdateResponse>)(e => this._onServerEvent(e)));
|
||||
this._register((this.channel.listen('onDelete_server') satisfies Event<MCPDeleteResponse>)(e => this._onServerEvent(e)));
|
||||
this._register((this.channel.listen('onAdd_server') satisfies Event<MCPAddServerResponse>)(e => this._onGetServerEvent(e)));
|
||||
this._register((this.channel.listen('onUpdate_server') satisfies Event<MCPUpdateServerResponse>)(e => this._onGetServerEvent(e)));
|
||||
this._register((this.channel.listen('onDelete_server') satisfies Event<MCPDeleteServerResponse>)(e => this._onGetServerEvent(e)));
|
||||
|
||||
// this._register((this.channel.listen('onLoading_server') satisfies Event<MCPServerEventLoadingParam>)(e => this._onServerEvent(e)));
|
||||
// Initialize the service
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
// This method is called when the service is disposed
|
||||
override async dispose(): Promise<void> {
|
||||
// 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<void> {
|
||||
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<MCPConfigFileType | null> {
|
||||
// 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<void> {
|
||||
private async _addMCPConfigFileWatcher(): Promise<void> {
|
||||
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<void> {
|
||||
if (this.mcpFileWatcher) {
|
||||
this.mcpFileWatcher.dispose();
|
||||
this.mcpFileWatcher = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Client-side functions
|
||||
|
||||
public async revealMCPConfigFile(): Promise<void> {
|
||||
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<URI> {
|
||||
// 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<boolean> {
|
||||
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<MCPServerStateOfName> {
|
||||
// Get the server states from Void Settings Service
|
||||
const savedServerStates = this.voidSettingsService.state.mcpServerStateOfName;
|
||||
private async _updateStateWithCurrentConfigFile(): Promise<void> {
|
||||
|
||||
// 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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ export type EventMCPServerSetupOnError = Parameters<MCPServerSetupParams['onErro
|
||||
export type MCPServerModel = MCPServerSuccessModel | MCPServerErrorModel;
|
||||
|
||||
|
||||
export type MCPEventType = {
|
||||
export type MCPServerEventType = {
|
||||
type: 'add';
|
||||
name: string;
|
||||
prevServer?: undefined;
|
||||
@@ -188,13 +188,13 @@ export type MCPEventType = {
|
||||
}
|
||||
|
||||
// Response types
|
||||
export type MCPAddResponse = { response: MCPEventType & { type: 'add' } }
|
||||
export type MCPUpdateResponse = { response: MCPEventType & { type: 'update' } }
|
||||
export type MCPDeleteResponse = { response: MCPEventType & { type: 'delete' } }
|
||||
// export type MCPLoadingResponse = { response: MCPEventType & { type: 'loading' } }
|
||||
export type MCPAddServerResponse = { response: MCPServerEventType & { type: 'add' } }
|
||||
export type MCPUpdateServerResponse = { response: MCPServerEventType & { type: 'update' } }
|
||||
export type MCPDeleteServerResponse = { response: MCPServerEventType & { type: 'delete' } }
|
||||
// export type MCPLoadingChangeResponse = { response: MCPEventType & { type: 'loading' } }
|
||||
|
||||
|
||||
export type MCPEventResponse = { response: MCPEventType }
|
||||
export type MCPServerEventResponse = { response: MCPServerEventType }
|
||||
|
||||
export interface MCPConfigFileParseErrorResponse {
|
||||
response: {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { MCPConfigFileType, MCPConfigFileServerType, MCPServerErrorModel, MCPServerModel, MCPAddResponse, MCPUpdateResponse, MCPDeleteResponse } from '../common/mcpServiceTypes.js';
|
||||
import { MCPConfigFileType, MCPConfigFileServerType, MCPServerErrorModel, MCPServerModel, MCPAddServerResponse, MCPUpdateServerResponse, MCPDeleteServerResponse } from '../common/mcpServiceTypes.js';
|
||||
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||
import { equals } from '../../../../base/common/objects.js';
|
||||
import { MCPServerStateOfName } from '../common/voidSettingsTypes.js';
|
||||
@@ -52,10 +52,12 @@ export class MCPChannel implements IServerChannel {
|
||||
// mcp emitters
|
||||
private readonly mcpEmitters = {
|
||||
serverEvent: {
|
||||
onAdd: new Emitter<MCPAddResponse>(),
|
||||
onUpdate: new Emitter<MCPUpdateResponse>(),
|
||||
onDelete: new Emitter<MCPDeleteResponse>(),
|
||||
// onChangeLoading: new Emitter<MCPLoadingResponse>(),
|
||||
onAdd: new Emitter<MCPAddServerResponse>(),
|
||||
onUpdate: new Emitter<MCPUpdateServerResponse>(),
|
||||
onDelete: new Emitter<MCPDeleteServerResponse>(),
|
||||
// onResult: new Emitter<>(),
|
||||
// onError: new Emitter<>(),
|
||||
// onChangeLoading: new Emitter<MCPLoadingResponse>(), // really onStart
|
||||
}
|
||||
// toolCall: {
|
||||
// success: new Emitter<void>(),
|
||||
@@ -63,9 +65,9 @@ export class MCPChannel implements IServerChannel {
|
||||
// },
|
||||
} satisfies {
|
||||
serverEvent: {
|
||||
onAdd: Emitter<MCPAddResponse>,
|
||||
onUpdate: Emitter<MCPUpdateResponse>,
|
||||
onDelete: Emitter<MCPDeleteResponse>,
|
||||
onAdd: Emitter<MCPAddServerResponse>,
|
||||
onUpdate: Emitter<MCPUpdateServerResponse>,
|
||||
onDelete: Emitter<MCPDeleteServerResponse>,
|
||||
// onChangeLoading: Emitter<MCPLoadingResponse>,
|
||||
}
|
||||
}
|
||||
@@ -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<any> {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user