This commit is contained in:
Andrew Pareles
2025-05-21 00:56:14 -07:00
parent 47b926d040
commit d18cf23c5b
7 changed files with 315 additions and 320 deletions
@@ -1,5 +1,5 @@
import { VoidSwitch } from '../util/inputs.js';
import { MCPConfigParseError, MCPServerEventParam, MCPServerObject, MCPServers } from '../../../../common/mcpServiceTypes.js';
import { MCPConfigFileParseErrorResponse, MCPEventType, MCPEventResponse, MCPServerObject, MCPServerOfName } from '../../../../common/mcpServiceTypes.js';
import { useEffect, useState } from 'react';
import { useAccessor } from '../util/services.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
@@ -98,14 +98,14 @@ const MCPServersList = () => {
const accessor = useAccessor();
const mcpService = accessor.get('IMCPService');
const [mcpServers, setMCPServers] = useState<MCPServers>({});
const [mcpServers, setMCPServers] = useState<MCPServerOfName>({});
const [mcpConfigError, setMCPConfigError] = useState<string | null>(null);
// Get all servers from MCPConfigService
useEffect(() => {
console.log('RUNNING MCPServersList EFFECT');
// Initial fetch
const servers = mcpService.getMCPServers();
const servers = mcpService.getMCPServerOfName();
if (servers) {
// Do something with the servers
console.log('MCP Servers:', servers);
@@ -114,10 +114,12 @@ const MCPServersList = () => {
// 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.onLoadingServers(handleListeners));
disposables.push(mcpService.onConfigParsingError(handleListeners));
// Clean up subscription when component unmounts
@@ -128,14 +130,14 @@ const MCPServersList = () => {
}, [mcpService]);
const handleListeners = (e: MCPServerEventParam | MCPConfigParseError) => {
if (e.response.event === 'config-error') {
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.event === 'add' || e.response.event === 'update' || e.response.event === 'loading') {
if (e.response.type === 'add' || e.response.type === 'update' || e.response.type === 'loading') {
// Handle the add event
const { name, newServer } = e.response;
setMCPServers(prevServers => ({
@@ -144,7 +146,7 @@ const MCPServersList = () => {
}));
return;
}
if (e.response.event === 'delete') {
if (e.response.type === 'delete') {
// Handle the delete event
const { name, prevServer } = e.response;
setMCPServers(prevServers => {
@@ -1249,7 +1249,7 @@ Alternatively, place a \`.voidrules\` file in the root of your workspace.
<div className='mt-12 max-w-[600px]'>
<div className='flex items-center justify-between mb-2'>
<h2 className={`text-3xl`}>MCP</h2>
<VoidButtonBgDarken className='px-4 py-1' onClick={async () => { await mcpService.openMCPConfigFile() }}>
<VoidButtonBgDarken className='px-4 py-1' onClick={async () => { await mcpService.revealMCPConfigFile() }}>
Add MCP Server
</VoidButtonBgDarken>
</div>
@@ -15,23 +15,27 @@ 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 { MCPServers, MCPConfig, MCPServerEventParam, MCPServerEventAddParam, MCPServerEventUpdateParam, MCPServerEventDeleteParam, MCPServerEventLoadingParam, MCPConfigParseError } from './mcpServiceTypes.js';
import { MCPServerOfName, MCPConfigFileType, MCPConfigFileParseErrorResponse, MCPAddResponse, MCPUpdateResponse, MCPDeleteResponse, MCPEventResponse } from './mcpServiceTypes.js';
import { Event, Emitter } from '../../../../base/common/event.js';
import { InternalToolInfo } from './prompt/prompts.js';
import { IVoidSettingsService } from './voidSettingsService.js';
import { MCPServerStates } from './voidSettingsTypes.js';
import { MCPServerStateOfName } from './voidSettingsTypes.js';
export interface IMCPService {
readonly _serviceBrand: undefined;
openMCPConfigFile(): Promise<void>;
getMCPServers(): MCPServers;
getAllToolsFormatted(): InternalToolInfo[];
revealMCPConfigFile(): Promise<void>;
getMCPServerOfName(): MCPServerOfName;
getCurrentTools(): InternalToolInfo[];
toggleServer(serverName: string, isOn: boolean): Promise<void>;
onDidAddServer: Event<MCPServerEventAddParam>;
onDidUpdateServer: Event<MCPServerEventUpdateParam>;
onDidDeleteServer: Event<MCPServerEventDeleteParam>;
onLoadingServers: Event<MCPServerEventLoadingParam>;
onConfigParsingError: Event<MCPConfigParseError>;
onDidAddServer: Event<MCPAddResponse>;
onDidUpdateServer: Event<MCPUpdateResponse>;
onDidDeleteServer: Event<MCPDeleteResponse>;
// onLoadingServers: Event<MCPServerEventLoadingParam>;
onConfigParsingError: Event<MCPConfigFileParseErrorResponse>;
}
export const IMCPService = createDecorator<IMCPService>('mcpConfigService');
@@ -48,18 +52,20 @@ class MCPService extends Disposable implements IMCPService {
private readonly channel: IChannel // MCPChannel
// list of MCP servers pulled from mcpChannel
private mcpServers: MCPServers = {}
private readonly mcpServers: MCPServerOfName = {}
// Emitters for server events
private readonly _onDidAddServer = new Emitter<MCPServerEventAddParam>();
private readonly _onDidUpdateServer = new Emitter<MCPServerEventUpdateParam>();
private readonly _onDidDeleteServer = new Emitter<MCPServerEventDeleteParam>();
private readonly _onLoadingServers = new Emitter<MCPServerEventLoadingParam>();
private readonly _onConfigParsingError = new Emitter<MCPConfigParseError>();
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;
public readonly onLoadingServers = this._onLoadingServers.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;
constructor(
@@ -74,10 +80,11 @@ 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<MCPServerEventAddParam>)(e => this._onServerEvent(e)));
this._register((this.channel.listen('onUpdate_server') satisfies Event<MCPServerEventUpdateParam>)(e => this._onServerEvent(e)));
this._register((this.channel.listen('onDelete_server') satisfies Event<MCPServerEventDeleteParam>)(e => this._onServerEvent(e)));
this._register((this.channel.listen('onLoading_server') satisfies Event<MCPServerEventLoadingParam>)(e => this._onServerEvent(e)));
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('onLoading_server') satisfies Event<MCPServerEventLoadingParam>)(e => this._onServerEvent(e)));
// Initialize the service
this._initialize();
}
@@ -104,12 +111,11 @@ class MCPService extends Disposable implements IMCPService {
private async _initialize() {
try {
// Get the MCP config file path
const mcpConfigUri = await this._getMCPConfigPath();
const mcpConfigUri = await this._getMCPConfigFilePath();
// Check if the file exists
// create file if it doesn't exist
const fileExists = await this._configFileExists(mcpConfigUri);
if (!fileExists) {
// Create the file if it doesn't exist
await this._createMCPConfigFile(mcpConfigUri);
console.log('MCP Config file created:', mcpConfigUri.toString());
}
@@ -117,15 +123,16 @@ class MCPService extends Disposable implements IMCPService {
// Wait for VoidSettingsService to initialize before proceeding
await this.voidSettingsService.waitForInitState;
// Parse the MCP config file
// 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`);
if (mcpConfig && mcpConfig.mcpServers) {
const updatedServerStates = await this._handleServerStateChange(mcpConfig);
// update state based on config file
const updatedServerStates = await this._handleServerStateChange(mcpConfig);
// Setup the server list
this.channel.call('setupServers', { mcpConfig, serverStates: updatedServerStates })
}
// Setup the server list
this.channel.call('setupServers', { mcpConfig, serverStates: updatedServerStates })
// Add a watcher to the MCP config file
@@ -136,59 +143,48 @@ class MCPService extends Disposable implements IMCPService {
}
}
private async _onServerEvent(e: MCPServerEventParam) {
if (e.response.event === 'add') {
// Add to the mcpServers list
this.mcpServers[e.response.name] = e.response.newServer;
// Fire the event to notify browser
this._onDidAddServer.fire(e as MCPServerEventAddParam);
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 (e.response.event === 'update') {
// Update the mcpServers list
this.mcpServers[e.response.name] = e.response.newServer;
// Fire the event to notify browser
this._onDidUpdateServer.fire(e as MCPServerEventUpdateParam);
if (r.type === 'update') {
this.mcpServers[r.name] = r.newServer;
this._onDidUpdateServer.fire(e as MCPUpdateResponse);
}
if (e.response.event === 'delete') {
// Remove from the mcpServers list
delete this.mcpServers[e.response.name];
// Fire the event to notify browser
this._onDidDeleteServer.fire(e as MCPServerEventDeleteParam);
}
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 as MCPServerEventLoadingParam);
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);
// }
}
// Create the file/directory if it doesn't exist
private async _createMCPConfigFile(mcpConfigUri: URI): Promise<void> {
// Create the directory if it doesn't exist
await this.fileService.createFile(mcpConfigUri.with({ path: mcpConfigUri.path }));
// Create the MCP config file with default content
const buffer = VSBuffer.fromString(this.MCP_CONFIG_SAMPLE_STRING);
await this.fileService.writeFile(mcpConfigUri, buffer);
}
private async _parseMCPConfigFile(): Promise<MCPConfig | null> {
// Remove any previous config parsing error
// This isn't super intuitive, but it works
// 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: {
event: 'config-error',
type: 'config-file-error',
error: null
}
});
// Process config file
const mcpConfigUri = await this._getMCPConfigPath();
const mcpConfigUri = await this._getMCPConfigFilePath();
try {
const fileContent = await this.fileService.readFile(mcpConfigUri);
@@ -197,13 +193,13 @@ class MCPService extends Disposable implements IMCPService {
if (!configJson.mcpServers) {
throw new Error('Invalid MCP config file: missing mcpServers property');
}
return configJson as MCPConfig;
return configJson as MCPConfigFileType;
} catch (error) {
const fullError = `Error parsing MCP config file: ${error}`;
console.error(fullError);
this._onConfigParsingError.fire({
response: {
event: 'config-error',
type: 'config-file-error',
error: fullError
}
});
@@ -212,7 +208,7 @@ class MCPService extends Disposable implements IMCPService {
}
private async _setMCPConfigFileWatch(): Promise<void> {
const mcpConfigUri = await this._getMCPConfigPath();
const mcpConfigUri = await this._getMCPConfigFilePath();
// Watch the file for changes
this.mcpFileWatcher = this.fileService.watch(mcpConfigUri);
@@ -241,10 +237,10 @@ class MCPService extends Disposable implements IMCPService {
// Client-side functions
public async openMCPConfigFile(): Promise<void> {
public async revealMCPConfigFile(): Promise<void> {
try {
// Get the MCP config file path
const mcpConfigUri = await this._getMCPConfigPath();
const mcpConfigUri = await this._getMCPConfigFilePath();
// Open the MCP config file in the editor
await this.editorService.openEditor({
@@ -260,12 +256,12 @@ class MCPService extends Disposable implements IMCPService {
}
}
public getMCPServers(): MCPServers {
public getMCPServerOfName(): MCPServerOfName {
// Call the getMCPServers method in the main process
return this.mcpServers;
}
public getAllToolsFormatted(): InternalToolInfo[] {
public getCurrentTools(): InternalToolInfo[] {
const allTools = Object.values(this.mcpServers).flatMap(server => {
return server.tools.map(tool => {
// Convert JsonSchema to the expected format
@@ -293,15 +289,14 @@ class MCPService extends Disposable implements IMCPService {
public async toggleServer(serverName: string, isOn: boolean): Promise<void> {
this.channel.call('toggleServer', { serverName, isOn })
// Update the server state in the local mcpServers list
await this.voidSettingsService.updateMCPServerState(serverName, isOn);
await this.voidSettingsService.setMCPServerState(serverName, isOn);
}
// utility functions
private async _getMCPConfigPath(): Promise<URI> {
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);
return URI.file(mcpConfigPath);
@@ -319,9 +314,9 @@ class MCPService extends Disposable implements IMCPService {
}
// Handle server state changes
private async _handleServerStateChange(mcpConfig: MCPConfig): Promise<MCPServerStates> {
private async _handleServerStateChange(mcpConfig: MCPConfigFileType): Promise<MCPServerStateOfName> {
// Get the server states from Void Settings Service
const savedServerStates = this.voidSettingsService.state.mcpServerStates;
const savedServerStates = this.voidSettingsService.state.mcpServerStateOfName;
// Parse the MCP config file for servers
const availableServers = Object.keys(mcpConfig.mcpServers);
@@ -331,7 +326,7 @@ class MCPService extends Disposable implements IMCPService {
const addedServersObject = addedServers.reduce((acc, serverName) => {
acc[serverName] = { isOn: true };
return acc;
}, {} as MCPServerStates);
}, {} as MCPServerStateOfName);
await this.voidSettingsService.addMCPServers(addedServersObject);
// Handle removed servers
@@ -344,7 +339,7 @@ class MCPService extends Disposable implements IMCPService {
acc[serverName] = savedServerStates[serverName];
}
return acc;
}, {} as MCPServerStates);
}, {} as MCPServerStateOfName);
return updatedServers;
}
@@ -23,103 +23,100 @@
/* Core JSON‑RPC envelope */
/* -------------------------------------------------- */
export interface JsonRpcSuccess<T> {
/** JSON‑RPC version – always '2.0' */
jsonrpc: '2.0';
/** Request identifier echoed back by the server */
id: string | number | null;
/** The successful result payload */
result: T;
}
// export interface JsonRpcSuccess<T> {
// /** JSON‑RPC version – always '2.0' */
// jsonrpc: '2.0';
// /** Request identifier echoed back by the server */
// id: string | number | null;
// /** The successful result payload */
// result: T;
// }
/* -------------------------------------------------- */
/* Utility: pagination */
/* -------------------------------------------------- */
export interface Paginated {
/** Opaque cursor for fetching the next page */
nextCursor?: string;
}
// export interface Paginated {
// /** Opaque cursor for fetching the next page */
// nextCursor?: string;
// }
/* -------------------------------------------------- */
/* 1. tools/list */
/* -------------------------------------------------- */
/** Minimal JSON‑Schema placeholder – adapt if you need stricter typing */
export type JsonSchema = Record<string, unknown>;
export interface Tool {
export interface MCPTool {
/** Unique tool identifier */
name: string;
/** Human‑readable description */
description?: string;
/** JSON schema describing expected arguments */
inputSchema?: JsonSchema;
inputSchema?: Record<string, unknown>;
/** Free‑form annotations describing behaviour, security, etc. */
annotations?: Record<string, unknown>;
}
export interface ToolsListResult extends Paginated {
tools: Tool[];
}
// export interface ToolsListResult extends Paginated {
// tools: MCPTool[];
// }
export type ToolsListResponse = JsonRpcSuccess<ToolsListResult>;
// export type ToolsListResponse = JsonRpcSuccess<ToolsListResult>;
/* -------------------------------------------------- */
/* 2. prompts/list */
/* -------------------------------------------------- */
export interface PromptArgument {
name: string;
description?: string;
/** Whether the argument is required */
required?: boolean;
}
// export interface PromptArgument {
// name: string;
// description?: string;
// /** Whether the argument is required */
// required?: boolean;
// }
export interface Prompt {
name: string;
description?: string;
arguments?: PromptArgument[];
}
// export interface Prompt {
// name: string;
// description?: string;
// arguments?: PromptArgument[];
// }
export interface PromptsListResult extends Paginated {
prompts: Prompt[];
}
// export interface PromptsListResult extends Paginated {
// prompts: Prompt[];
// }
export type PromptsListResponse = JsonRpcSuccess<PromptsListResult>;
// export type PromptsListResponse = JsonRpcSuccess<PromptsListResult>;
/* -------------------------------------------------- */
/* 3. tools/call */
/* -------------------------------------------------- */
/** Additional resource structure that can be embedded in tool results */
export interface Resource {
uri: string;
mimeType: string;
/** Either plain‑text or base64‑encoded binary data */
text?: string;
data?: string;
}
// export interface Resource {
// uri: string;
// mimeType: string;
// /** Either plain‑text or base64‑encoded binary data */
// text?: string;
// data?: string;
// }
/** Individual content items returned by a tool */
export type ToolContent =
| { type: 'text'; text: string }
| { type: 'image'; data: string; mimeType: string }
| { type: 'audio'; data: string; mimeType: string }
| { type: 'resource'; resource: Resource };
// export type ToolContent =
// | { type: 'text'; text: string }
// | { type: 'image'; data: string; mimeType: string }
// | { type: 'audio'; data: string; mimeType: string }
// | { type: 'resource'; resource: Resource };
export interface ToolCallResult {
/** List of content parts (text, images, resources, etc.) */
content: ToolContent[];
/** True if the tool itself encountered a domain‑level error */
isError?: boolean;
}
// export interface ToolCallResult {
// /** List of content parts (text, images, resources, etc.) */
// content: ToolContent[];
// /** True if the tool itself encountered a domain‑level error */
// isError?: boolean;
// }
export type ToolCallResponse = JsonRpcSuccess<ToolCallResult>;
// export type ToolCallResponse = JsonRpcSuccess<ToolCallResult>;
// MCP SERVER CONFIG FILE TYPES -----------------------------
export interface MCPServerConfig {
export interface MCPConfigFileServerType {
// Command-based server properties
command?: string;
args?: string[];
@@ -130,30 +127,23 @@ export interface MCPServerConfig {
headers?: Record<string, string>;
}
export interface MCPConfig {
mcpServers: Record<string, MCPServerConfig>;
export interface MCPConfigFileType {
mcpServers: Record<string, MCPConfigFileServerType>;
}
export interface MCPConfigParseError {
// Error message
response: {
event: 'config-error';
error: string | null;
}
}
// SERVER EVENT TYPES ------------------------------------------
export interface MCPServerObject {
// Command-based server properties
tools: Tool[],
tools: MCPTool[],
status: 'loading' | 'error' | 'success' | 'offline',
isOn: boolean,
command?: string,
error?: string,
}
export interface MCPServers {
export interface MCPServerOfName {
[serverName: string]: MCPServerObject;
}
@@ -162,63 +152,67 @@ export type MCPServerSuccessModel = MCPServerObject;
export type MCPServerErrorModel = Omit<MCPServerObject, 'error'> & { error: string };
export type MCPServerSetupParams<serverResponse> = {
export type MCPServerSetupParams = {
serverName: string;
onSuccess: (param: { model: MCPServerSuccessModel & { serverName: string } }) => void;
onError: (param: { model: MCPServerErrorModel & { serverName: string } }) => void;
}
// Listener event types
export type EventMCPServerSetupOnSuccess<serverResponse> = Parameters<MCPServerSetupParams<serverResponse>['onSuccess']>[0]
export type EventMCPServerSetupOnError<serverResponse> = Parameters<MCPServerSetupParams<serverResponse>['onError']>[0]
type MCPServerEventType = 'add' | 'update' | 'delete' | 'loading';
export type EventMCPServerSetupOnSuccess = Parameters<MCPServerSetupParams['onSuccess']>[0]
export type EventMCPServerSetupOnError = Parameters<MCPServerSetupParams['onError']>[0]
export type MCPServerModel = MCPServerSuccessModel | MCPServerErrorModel;
interface MCPServerResponseBase {
export type MCPEventType = {
type: 'add';
name: string;
event: MCPServerEventType;
newServer?: MCPServerModel;
prevServer?: MCPServerModel;
prevServer?: undefined;
newServer: MCPServerModel;
} | {
type: 'update';
name: string;
prevServer: MCPServerModel;
newServer: MCPServerModel;
} | {
type: 'delete';
name: string;
newServer?: undefined;
prevServer: MCPServerModel;
} | {
type: 'loading';
name: string;
prevServer?: undefined;
newServer: MCPServerModel;
}
type EventTypeConstraints = {
'add': {
prevServer?: never;
newServer: MCPServerModel;
};
'update': {
prevServer: MCPServerModel;
newServer: MCPServerModel;
};
'delete': {
newServer?: never;
prevServer: MCPServerModel;
};
'loading': {
prevServer?: never;
newServer: MCPServerModel;
// 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 MCPEventResponse = { response: MCPEventType }
export interface MCPConfigFileParseErrorResponse {
response: {
type: 'config-file-error';
error: string | null;
}
}
type MCPEventResponse<T extends MCPServerEventType> = Omit<MCPServerResponseBase, 'event' | keyof EventTypeConstraints> & EventTypeConstraints[T] & { event: T };
// Response types
export type MCPAddResponse = MCPEventResponse<'add'>;
export type MCPUpdateResponse = MCPEventResponse<'update'>;
export type MCPDeleteResponse = MCPEventResponse<'delete'>;
export type MCPLoadingResponse = MCPEventResponse<'loading'>;
export type MCPServerResponse = MCPAddResponse | MCPUpdateResponse | MCPDeleteResponse | MCPLoadingResponse;
// export type MCPServerResponse = MCPAddResponse | MCPUpdateResponse | MCPDeleteResponse | MCPLoadingResponse;
// Event parameter types
export type MCPServerEventAddParam = { response: MCPAddResponse };
export type MCPServerEventUpdateParam = { response: MCPUpdateResponse };
export type MCPServerEventDeleteParam = { response: MCPDeleteResponse };
export type MCPServerEventLoadingParam = { response: MCPLoadingResponse };
// export type MCPServerEventAddParam = { response: MCPAddResponse };
// export type MCPServerEventUpdateParam = { response: MCPUpdateResponse };
// export type MCPServerEventDeleteParam = { response: MCPDeleteResponse };
// export type MCPServerEventLoadingParam = { response: MCPLoadingResponse };
// Event Param union type
export type MCPServerEventParam = MCPServerEventAddParam | MCPServerEventUpdateParam | MCPServerEventDeleteParam | MCPServerEventLoadingParam;
// export type MCPServerEventParam = MCPServerEventAddParam | MCPServerEventUpdateParam | MCPServerEventDeleteParam | MCPServerEventLoadingParam;
@@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { IMetricsService } from './metricsService.js';
import { defaultProviderSettings, getModelCapabilities, ModelOverrides } from './modelCapabilities.js';
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPServerStates } from './voidSettingsTypes.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPServerStateOfName } from './voidSettingsTypes.js';
// name is the name in the dropdown
@@ -43,7 +43,7 @@ export type VoidSettingsState = {
readonly optionsOfModelSelection: OptionsOfModelSelection;
readonly overridesOfModel: OverridesOfModel;
readonly globalSettings: GlobalSettings;
readonly mcpServerStates: MCPServerStates;
readonly mcpServerStateOfName: MCPServerStateOfName;
readonly _modelOptions: ModelOption[] // computed based on the two above items
}
@@ -63,7 +63,7 @@ export interface IVoidSettingsService {
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
setOptionsOfModelSelection: SetOptionsOfModelSelection;
setGlobalSetting: SetGlobalSettingFn;
setMCPServerStates: (newStates: MCPServerStates) => Promise<void>;
// setMCPServerStates: (newStates: MCPServerStates) => Promise<void>;
// setting to undefined CLEARS it, unlike others:
setOverridesOfModel(providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined): Promise<void>;
@@ -75,9 +75,10 @@ export interface IVoidSettingsService {
toggleModelHidden(providerName: ProviderName, modelName: string): void;
addModel(providerName: ProviderName, modelName: string): void;
deleteModel(providerName: ProviderName, modelName: string): boolean;
addMCPServers(newMCPStates: MCPServerStates): Promise<void>;
addMCPServers(newMCPStates: MCPServerStateOfName): Promise<void>;
removeMCPServers(serverNames: string[]): Promise<void>;
updateMCPServerState(serverName: string, newIsOn: boolean): Promise<void>;
setMCPServerState(serverName: string, newIsOn: boolean): Promise<void>;
}
@@ -217,7 +218,7 @@ const defaultState = () => {
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
overridesOfModel: deepClone(defaultOverridesOfModel),
_modelOptions: [], // computed later
mcpServerStates: {},
mcpServerStateOfName: {},
}
return d
}
@@ -367,7 +368,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const newGlobalSettings = this.state.globalSettings
const newOverridesOfModel = this.state.overridesOfModel
const newMCPServerStates = this.state.mcpServerStates
const newMCPServerStateOfName = this.state.mcpServerStateOfName
const newState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
@@ -375,7 +376,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
settingsOfProvider: newSettingsOfProvider,
globalSettings: newGlobalSettings,
overridesOfModel: newOverridesOfModel,
mcpServerStates: newMCPServerStates,
mcpServerStateOfName: newMCPServerStateOfName,
}
this.state = _validatedModelState(newState)
@@ -494,21 +495,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
this._metricsService.capture('Autodetect Models', { providerName, newModels: newModels, ...logging })
}
}
setMCPServerStates = async (newStates: MCPServerStates) => {
const newState: VoidSettingsState = {
...this.state,
mcpServerStates: {
...this.state.mcpServerStates,
...newStates
}
};
this.state = _validatedModelState(newState);
await this._storeState();
// this._onDidChangeState.fire();
this._metricsService.capture('Set MCP Server States', { newStates });
}
toggleModelHidden(providerName: ProviderName, modelName: string) {
@@ -555,19 +541,32 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
// MCP Server State
private _setMCPServerStates = async (newStates: MCPServerStateOfName) => {
const newState: VoidSettingsState = {
...this.state,
mcpServerStateOfName: {
...this.state.mcpServerStateOfName,
...newStates
}
};
this.state = _validatedModelState(newState);
await this._storeState();
this._onDidChangeState.fire();
this._metricsService.capture('Set MCP Server States', { newStates });
}
addMCPServers = async (newMCPStates: MCPServerStates) => {
const { mcpServerStates } = this.state
addMCPServers = async (newMCPStates: MCPServerStateOfName) => {
const { mcpServerStateOfName: mcpServerStates } = this.state
const newMCPServerStates = {
...mcpServerStates,
...newMCPStates,
}
await this.setMCPServerStates(newMCPServerStates)
await this._setMCPServerStates(newMCPServerStates)
this._metricsService.capture('Add MCP Server', { servers: Object.keys(newMCPStates).join(', ') });
}
removeMCPServers = async (serverNames: string[]) => {
const { mcpServerStates } = this.state
const { mcpServerStateOfName: mcpServerStates } = this.state
const newMCPServerStates = {
...mcpServerStates,
}
@@ -576,12 +575,12 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
delete newMCPServerStates[serverName]
}
})
await this.setMCPServerStates(newMCPServerStates)
await this._setMCPServerStates(newMCPServerStates)
this._metricsService.capture('Remove MCP Server', { servers: serverNames.join(', ') });
}
updateMCPServerState = async (serverName: string, newIsOn: boolean) => {
const { mcpServerStates } = this.state
setMCPServerState = async (serverName: string, newIsOn: boolean) => {
const { mcpServerStateOfName: mcpServerStates } = this.state
if (!(serverName in mcpServerStates)) return // if not in list, do nothing
const newMCPServerStates = {
...mcpServerStates,
@@ -589,7 +588,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
isOn: newIsOn,
},
}
await this.setMCPServerStates(newMCPServerStates)
await this._setMCPServerStates(newMCPServerStates)
this._metricsService.capture('Update MCP Server State', { serverName, newIsOn });
}
@@ -494,7 +494,7 @@ export const defaultOverridesOfModel = overridesOfModel
export interface MCPServerStates {
export interface MCPServerStateOfName {
[serverName: string]: MCPServerState;
}
@@ -13,41 +13,60 @@ 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 { MCPConfig, MCPServerConfig, MCPServerErrorModel, MCPAddResponse, MCPServerEventAddParam, MCPServerEventUpdateParam, MCPServerEventDeleteParam, MCPUpdateResponse, MCPServerModel, MCPDeleteResponse, MCPServerEventLoadingParam } from '../common/mcpServiceTypes.js';
import { MCPConfigFileType, MCPConfigFileServerType, MCPServerErrorModel, MCPServerModel, MCPAddResponse, MCPUpdateResponse, MCPDeleteResponse } from '../common/mcpServiceTypes.js';
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import { equals } from '../../../../base/common/objects.js';
import { MCPServerStates } from '../common/voidSettingsTypes.js';
import { MCPServerStateOfName } from '../common/voidSettingsTypes.js';
// const getLoadingServerObject = (serverName: string, isOn: boolean | undefined) => {
// return {
// response: {
// event: 'loading',
// name: serverName,
// newServer: {
// status: 'loading',
// isOn,
// tools: [],
// command: '',
// }
// }
// } as const
// }
const getClientConfig = (serverName: string) => {
return {
name: `${serverName}-client`,
version: '0.1.0',
// debug: true,
}
}
export class MCPChannel implements IServerChannel {
// connected clients
private clients: { [clientId: string]: { client?: Client, mcpConfig: MCPServerConfig, formattedServer: MCPServerModel } } = {}
private getClientConfig(serverName: string) {
return {
name: `${serverName}-client`,
version: '0.1.0',
// debug: true,
}
}
private clients: { [clientId: string]: { client?: Client, mcpConfig: MCPConfigFileServerType, formattedServer: MCPServerModel } } = {}
// mcp emitters
private readonly mcpEmitters = {
serverEvent: {
add: new Emitter<MCPServerEventAddParam>(),
update: new Emitter<MCPServerEventUpdateParam>(),
delete: new Emitter<MCPServerEventDeleteParam>(),
loading: new Emitter<MCPServerEventLoadingParam>(),
onAdd: new Emitter<MCPAddResponse>(),
onUpdate: new Emitter<MCPUpdateResponse>(),
onDelete: new Emitter<MCPDeleteResponse>(),
// onChangeLoading: new Emitter<MCPLoadingResponse>(),
}
// toolCall: {
// success: new Emitter<void>(),
// error: new Emitter<void>(),
// },
} satisfies {
[event in 'serverEvent']: {
add: Emitter<MCPServerEventAddParam>,
update: Emitter<MCPServerEventUpdateParam>,
delete: Emitter<MCPServerEventDeleteParam>,
loading: Emitter<MCPServerEventLoadingParam>,
serverEvent: {
onAdd: Emitter<MCPAddResponse>,
onUpdate: Emitter<MCPUpdateResponse>,
onDelete: Emitter<MCPDeleteResponse>,
// onChangeLoading: Emitter<MCPLoadingResponse>,
}
}
@@ -59,10 +78,10 @@ export class MCPChannel implements IServerChannel {
listen(_: unknown, event: string): Event<any> {
// server events
if (event === 'onAdd_server') return this.mcpEmitters.serverEvent.add.event;
else if (event === 'onUpdate_server') return this.mcpEmitters.serverEvent.update.event;
else if (event === 'onDelete_server') return this.mcpEmitters.serverEvent.delete.event;
else if (event === 'onLoading_server') return this.mcpEmitters.serverEvent.loading.event;
if (event === 'onAdd_server') return this.mcpEmitters.serverEvent.onAdd.event;
else if (event === 'onUpdate_server') return this.mcpEmitters.serverEvent.onUpdate.event;
else if (event === 'onDelete_server') return this.mcpEmitters.serverEvent.onDelete.event;
// else if (event === 'onLoading_server') return this.mcpEmitters.serverEvent.onChangeLoading.event;
// handle unknown events
else throw new Error(`Event not found: ${event}`);
@@ -72,29 +91,30 @@ export class MCPChannel implements IServerChannel {
async call(_: unknown, command: string, params: any): Promise<any> {
try {
if (command === 'setupServers') {
await this._callSetupServers(params)
await this._setupServers(params)
}
else if (command === 'closeAllServers') {
await this._callCloseAllServers()
await this._closeAllServers()
}
else if (command === 'toggleServer') {
await this._handleToggleServer(params.serverName, params.isOn)
}
else if (command === 'callTool') {
// TODO: HANDLE THIS
await this._toggleServer(params.serverName, params.isOn)
}
// TODO!!! is this still needed?
// else if (command === 'callTool') {
// // TODO: HANDLE THIS
// }
else {
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
}
}
catch (e) {
console.log('mcp channel: Call Error:', e)
console.error('mcp channel: Call Error:', e)
}
}
// call functions
private async _callSetupServers(params: { mcpConfig: MCPConfig, serverStates: MCPServerStates }) {
private async _setupServers(params: { mcpConfig: MCPConfigFileType, serverStates: MCPServerStateOfName }) {
const { mcpConfig, serverStates } = params
@@ -118,27 +138,27 @@ export class MCPChannel implements IServerChannel {
// Divide the server based on event
const addedServers = serverNames.filter((serverName) => {
const { prevMCPConfig, newMCPConfig } = getPrevAndNewServerConfig(serverName)
const isAdded = !prevMCPConfig && newMCPConfig
if (isAdded) {
this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName, serverStates[serverName]?.isOn))
}
return isAdded
const isNew = !prevMCPConfig && newMCPConfig
// if (isAdded) {
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, serverStates[serverName]?.isOn))
// }
return isNew
})
const updatedServers = serverNames.filter((serverName) => {
const { prevMCPConfig, newMCPConfig } = getPrevAndNewServerConfig(serverName)
const isUpdated = prevMCPConfig && newMCPConfig && !equals(prevMCPConfig, newMCPConfig)
if (isUpdated) {
this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName, serverStates[serverName]?.isOn))
}
return isUpdated
const isNew = prevMCPConfig && newMCPConfig && !equals(prevMCPConfig, newMCPConfig)
// if (isUpdated) {
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, serverStates[serverName]?.isOn))
// }
return isNew
})
const deletedServers = Object.keys(prevServers).filter((serverName) => {
const { prevMCPConfig, newMCPConfig } = getPrevAndNewServerConfig(serverName)
const isDeleted = prevMCPConfig && !newMCPConfig
if (isDeleted) {
this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName, serverStates[serverName]?.isOn))
}
return isDeleted
const isNew = prevMCPConfig && !newMCPConfig
// if (isDeleted) {
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, serverStates[serverName]?.isOn))
// }
return isNew
})
// Check if no changes were made
@@ -148,55 +168,55 @@ export class MCPChannel implements IServerChannel {
}
if (addedServers.length > 0) {
// Handle added servers
const addPromises: Promise<MCPAddResponse>[] = addedServers.map(async (serverName) => {
// emit added servers
const addPromises = addedServers.map(async (serverName) => {
const addedServer = await this._safeSetupServer(mcpServers[serverName], serverName, serverStates[serverName]?.isOn)
return {
event: 'add',
type: 'add',
newServer: addedServer,
name: serverName,
} as MCPAddResponse
} as const
});
const formattedAddedResponses = await Promise.all(addPromises);
formattedAddedResponses.forEach((formattedResponse) => (this.mcpEmitters.serverEvent.add.fire({ response: formattedResponse })));
formattedAddedResponses.forEach((formattedResponse) => (this.mcpEmitters.serverEvent.onAdd.fire({ response: formattedResponse })));
}
if (updatedServers.length > 0) {
// Handle updated servers
const updatePromises: Promise<MCPUpdateResponse>[] = updatedServers.map(async (serverName) => {
// emit updated servers
const updatePromises = updatedServers.map(async (serverName) => {
const prevServer = this.clients[serverName]?.formattedServer;
const newServer = await this._safeSetupServer(mcpServers[serverName], serverName, serverStates[serverName]?.isOn)
return {
type: 'update',
prevServer,
newServer: newServer,
event: 'update',
name: serverName,
} as MCPUpdateResponse
} as const
});
const formattedUpdatedResponses = await Promise.all(updatePromises);
formattedUpdatedResponses.forEach((formattedResponse) => (this.mcpEmitters.serverEvent.update.fire({ response: formattedResponse })));
formattedUpdatedResponses.forEach((formattedResponse) => (this.mcpEmitters.serverEvent.onUpdate.fire({ response: formattedResponse })));
}
if (deletedServers.length > 0) {
// Handle deleted servers
const deletePromises: Promise<MCPDeleteResponse>[] = deletedServers.map(async (serverName) => {
// emit deleted servers
const deletePromises = deletedServers.map(async (serverName) => {
const prevServer = this.clients[serverName]?.formattedServer;
await this._callCloseServer(serverName)
this._callRemoveServer(serverName)
await this._closeServer(serverName)
this._removeServer(serverName)
return {
event: 'delete',
type: 'delete',
prevServer,
name: serverName,
} as MCPDeleteResponse
} as const
});
const formattedDeletedResponses = await Promise.all(deletePromises);
formattedDeletedResponses.forEach((formattedResponse) => (this.mcpEmitters.serverEvent.delete.fire({ response: formattedResponse })));
formattedDeletedResponses.forEach((formattedResponse) => (this.mcpEmitters.serverEvent.onDelete.fire({ response: formattedResponse })));
}
}
private async _callSetupServer(server: MCPServerConfig, serverName: string, isOn = true) {
private async _callSetupServer(server: MCPConfigFileServerType, serverName: string, isOn = true) {
const clientConfig = this.getClientConfig(serverName)
const clientConfig = getClientConfig(serverName)
const client = new Client(clientConfig)
let transport: Transport;
let formattedServer: MCPServerModel;
@@ -263,7 +283,7 @@ export class MCPChannel implements IServerChannel {
}
// Helper function to safely setup a server
private async _safeSetupServer(serverConfig: MCPServerConfig, serverName: string, isOn = true) {
private async _safeSetupServer(serverConfig: MCPConfigFileServerType, serverName: string, isOn = true) {
try {
return await this._callSetupServer(serverConfig, serverName, isOn)
} catch (err) {
@@ -293,15 +313,15 @@ export class MCPChannel implements IServerChannel {
}
}
private async _callCloseAllServers() {
private async _closeAllServers() {
for (const serverName in this.clients) {
await this._callCloseServer(serverName)
this._callRemoveServer(serverName)
await this._closeServer(serverName)
this._removeServer(serverName)
}
console.log('Closed all MCP servers');
}
private async _callCloseServer(serverName: string) {
private async _closeServer(serverName: string) {
const server = this.clients[serverName]
if (server) {
const { client } = server
@@ -314,22 +334,22 @@ export class MCPChannel implements IServerChannel {
}
}
private _callRemoveServer(serverName: string) {
private _removeServer(serverName: string) {
if (this.clients[serverName]) {
delete this.clients[serverName]
console.log(`Removed MCP server ${serverName}`);
}
}
private async _handleToggleServer(serverName: string, isOn: boolean) {
private async _toggleServer(serverName: string, isOn: boolean) {
const prevServer = this.clients[serverName]?.formattedServer
if (isOn) {
// Handle turning on the server
this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName, isOn))
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn))
const formattedServer = await this._callSetupServer(this.clients[serverName].mcpConfig, serverName)
this.mcpEmitters.serverEvent.update.fire({
this.mcpEmitters.serverEvent.onUpdate.fire({
response: {
event: 'update',
type: 'update',
name: serverName,
newServer: formattedServer,
prevServer: prevServer,
@@ -337,11 +357,11 @@ export class MCPChannel implements IServerChannel {
})
} else {
// Handle turning off the server
this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName, isOn))
this._callCloseServer(serverName)
this.mcpEmitters.serverEvent.update.fire({
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn))
this._closeServer(serverName)
this.mcpEmitters.serverEvent.onUpdate.fire({
response: {
event: 'update',
type: 'update',
name: serverName,
newServer: {
status: 'offline',
@@ -358,21 +378,6 @@ export class MCPChannel implements IServerChannel {
}
}
// Util functions
private _getLoadingServerObject(serverName: string, isOn = true): MCPServerEventLoadingParam {
return {
response: {
event: 'loading',
name: serverName,
newServer: {
status: 'loading',
isOn,
tools: [],
command: '',
}
}
}
}
}