From 1aeb62d075423ac8061b6953a97baa5c5880ffed Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 13 May 2025 01:13:05 -0400 Subject: [PATCH 01/98] Created mcpConfigService file --- src/vs/workbench/contrib/void/common/mcpConfigService.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/vs/workbench/contrib/void/common/mcpConfigService.ts diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts new file mode 100644 index 00000000..e69de29b From b4dff124e0b52c44a1120a46512992e90082f691 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 13 May 2025 02:00:50 -0400 Subject: [PATCH 02/98] Updated file with class MCPConfigService and functions to check if mcp.json config file exists or not --- .../contrib/void/common/mcpConfigService.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index e69de29b..0b5b99d2 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -0,0 +1,67 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../base/common/uri.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'; +import { IPathService } from '../../../services/path/common/pathService.js'; +import { join } from '../../../../base/common/path.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; + +export interface IMCPConfigService { + readonly _serviceBrand: undefined; + + getMCPConfigPath(): Promise; + // configFileExists(): Promise; +} + +export const IMCPConfigService = createDecorator('mcpConfigService'); + +class MCPConfigService extends Disposable implements IMCPConfigService { + _serviceBrand: undefined; + + constructor( + @IFileService private readonly fileService: IFileService, + @IPathService private readonly pathService: IPathService, + @IProductService private readonly productService: IProductService + ) { + super(); + this._initialize(); + } + + + + private async _initialize() { + // Check logs + const doesMCPExist = await this.configFileExists(); + console.log('MCP Config File Exists:', doesMCPExist); + } + + async getMCPConfigPath(): 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, 'mcp.json'); + return URI.file(mcpConfigPath); + } + + async configFileExists(): Promise { + try { + const mcpConfigUri = await this.getMCPConfigPath(); + + // 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; + } + } +} + +registerSingleton(IMCPConfigService, MCPConfigService, InstantiationType.Delayed); From f82c15401ac7589e7983766bf7712c08f1399efe Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 13 May 2025 02:01:23 -0400 Subject: [PATCH 03/98] Updated services.tsx to include IMCPConfigService --- .../workbench/contrib/void/browser/react/src/util/services.tsx | 2 ++ 1 file changed, 2 insertions(+) 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 82f47954..689c9923 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 @@ -51,6 +51,7 @@ import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService import { ITerminalService } from '../../../../../terminal/browser/terminal.js' import { ISearchService } from '../../../../../../services/search/common/search.js' import { IExtensionManagementService } from '../../../../../../../platform/extensionManagement/common/extensionManagement.js' +import { IMCPConfigService } from '../../../../common/mcpConfigService.js'; // normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes @@ -215,6 +216,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => { ITerminalService: accessor.get(ITerminalService), IExtensionManagementService: accessor.get(IExtensionManagementService), IExtensionTransferService: accessor.get(IExtensionTransferService), + IMCPConfigService: accessor.get(IMCPConfigService), } as const return reactAccessor From 8a12505ffd42a65b7e5a7e8fb4a5d03001a1cca8 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 13 May 2025 02:04:11 -0400 Subject: [PATCH 04/98] Moved mcp.json into a private variable in the class --- src/vs/workbench/contrib/void/common/mcpConfigService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index 0b5b99d2..53aaed7f 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -24,6 +24,7 @@ export const IMCPConfigService = createDecorator('mcpConfigSe class MCPConfigService extends Disposable implements IMCPConfigService { _serviceBrand: undefined; + private readonly MCP_CONFIG_FILE_NAME = 'mcp.json'; constructor( @IFileService private readonly fileService: IFileService, @IPathService private readonly pathService: IPathService, @@ -46,7 +47,7 @@ class MCPConfigService extends Disposable implements IMCPConfigService { const appName = this.productService.dataFolderName const userHome = await this.pathService.userHome(); - const mcpConfigPath = join(userHome.path, appName, 'mcp.json'); + const mcpConfigPath = join(userHome.path, appName, this.MCP_CONFIG_FILE_NAME); return URI.file(mcpConfigPath); } From 1ed79ef9859dc9e674e42245ca036fdbebba4756 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 13 May 2025 02:37:06 -0400 Subject: [PATCH 05/98] Added function to create mcp config file if it doesnt exist --- .../contrib/void/common/mcpConfigService.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index 53aaed7f..39acdfce 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -11,6 +11,7 @@ import { IFileService } from '../../../../platform/files/common/files.js'; import { IPathService } from '../../../services/path/common/pathService.js'; import { join } from '../../../../base/common/path.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; export interface IMCPConfigService { readonly _serviceBrand: undefined; @@ -25,6 +26,11 @@ class MCPConfigService extends Disposable implements IMCPConfigService { _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); + constructor( @IFileService private readonly fileService: IFileService, @IPathService private readonly pathService: IPathService, @@ -38,8 +44,14 @@ class MCPConfigService extends Disposable implements IMCPConfigService { private async _initialize() { // Check logs - const doesMCPExist = await this.configFileExists(); - console.log('MCP Config File Exists:', doesMCPExist); + const mcpExists = await this.configFileExists(); + if (!mcpExists) { + console.log('MCP Config file does not exist. Creating...'); + await this.createMCPConfigFile(); + } else { + console.log('MCP Config file already exists.'); + } + } async getMCPConfigPath(): Promise { @@ -63,6 +75,18 @@ class MCPConfigService extends Disposable implements IMCPConfigService { return false; } } + + async createMCPConfigFile(): Promise { + const mcpConfigUri = await this.getMCPConfigPath(); + + // Create the directory if it doesn't exist + await this.fileService.createFile(mcpConfigUri.with({ path: mcpConfigUri.path })); + + const buffer = VSBuffer.fromString(this.MCP_CONFIG_SAMPLE_STRING); + + // Create the MCP config file with default content + await this.fileService.writeFile(mcpConfigUri, buffer); + } } registerSingleton(IMCPConfigService, MCPConfigService, InstantiationType.Delayed); From 7af9973a8d524b4c8274a3928f97671319c3808d Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 13 May 2025 02:37:54 -0400 Subject: [PATCH 06/98] Fixed the structure of createMCPConfigFile --- src/vs/workbench/contrib/void/common/mcpConfigService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index 39acdfce..f8fe1842 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -82,9 +82,8 @@ class MCPConfigService extends Disposable implements IMCPConfigService { // Create the directory if it doesn't exist await this.fileService.createFile(mcpConfigUri.with({ path: mcpConfigUri.path })); - const buffer = VSBuffer.fromString(this.MCP_CONFIG_SAMPLE_STRING); - // Create the MCP config file with default content + const buffer = VSBuffer.fromString(this.MCP_CONFIG_SAMPLE_STRING); await this.fileService.writeFile(mcpConfigUri, buffer); } } From 01329382b9132e6bc842d745031ba536ce073f51 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 13 May 2025 02:46:48 -0400 Subject: [PATCH 07/98] Made createMCPConfigFile private --- src/vs/workbench/contrib/void/common/mcpConfigService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index f8fe1842..70f6e8af 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -47,7 +47,7 @@ class MCPConfigService extends Disposable implements IMCPConfigService { const mcpExists = await this.configFileExists(); if (!mcpExists) { console.log('MCP Config file does not exist. Creating...'); - await this.createMCPConfigFile(); + await this._createMCPConfigFile(); } else { console.log('MCP Config file already exists.'); } @@ -76,7 +76,7 @@ class MCPConfigService extends Disposable implements IMCPConfigService { } } - async createMCPConfigFile(): Promise { + private async _createMCPConfigFile(): Promise { const mcpConfigUri = await this.getMCPConfigPath(); // Create the directory if it doesn't exist From 2187641cc30b3a31c1992735b6de432eadd4167c Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 13 May 2025 21:07:06 -0400 Subject: [PATCH 08/98] Updated all functions to be private initially --- .../contrib/void/common/mcpConfigService.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index 70f6e8af..da22cedd 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -16,8 +16,8 @@ import { VSBuffer } from '../../../../base/common/buffer.js'; export interface IMCPConfigService { readonly _serviceBrand: undefined; - getMCPConfigPath(): Promise; - // configFileExists(): Promise; + // _getMCPConfigPath(): Promise; + // _configFileExists(): Promise; } export const IMCPConfigService = createDecorator('mcpConfigService'); @@ -44,7 +44,7 @@ class MCPConfigService extends Disposable implements IMCPConfigService { private async _initialize() { // Check logs - const mcpExists = await this.configFileExists(); + const mcpExists = await this._configFileExists(); if (!mcpExists) { console.log('MCP Config file does not exist. Creating...'); await this._createMCPConfigFile(); @@ -54,7 +54,7 @@ class MCPConfigService extends Disposable implements IMCPConfigService { } - async getMCPConfigPath(): Promise { + private async _getMCPConfigPath(): Promise { // Get the appropriate directory based on dev mode const appName = this.productService.dataFolderName @@ -63,9 +63,9 @@ class MCPConfigService extends Disposable implements IMCPConfigService { return URI.file(mcpConfigPath); } - async configFileExists(): Promise { + private async _configFileExists(): Promise { try { - const mcpConfigUri = await this.getMCPConfigPath(); + const mcpConfigUri = await this._getMCPConfigPath(); // Try to get file stats - if it succeeds, the file exists await this.fileService.stat(mcpConfigUri); @@ -77,7 +77,7 @@ class MCPConfigService extends Disposable implements IMCPConfigService { } private async _createMCPConfigFile(): Promise { - const mcpConfigUri = await this.getMCPConfigPath(); + const mcpConfigUri = await this._getMCPConfigPath(); // Create the directory if it doesn't exist await this.fileService.createFile(mcpConfigUri.with({ path: mcpConfigUri.path })); From 661908237bd4481fdc2aa334a5fb02bcc4a3ba44 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 13 May 2025 21:54:42 -0400 Subject: [PATCH 09/98] Added functions to watch, update, and remove watcher for mcpConfigFile --- .../contrib/void/common/mcpConfigService.ts | 116 +++++++++++++++--- 1 file changed, 97 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index da22cedd..7500302c 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -4,20 +4,19 @@ *--------------------------------------------------------------------------------------*/ import { URI } from '../../../../base/common/uri.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, IDisposable } 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'; import { IPathService } from '../../../services/path/common/pathService.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { join } from '../../../../base/common/path.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; export interface IMCPConfigService { readonly _serviceBrand: undefined; - - // _getMCPConfigPath(): Promise; - // _configFileExists(): Promise; + openMCPConfigFile(): Promise; } export const IMCPConfigService = createDecorator('mcpConfigService'); @@ -27,31 +26,55 @@ class MCPConfigService extends Disposable implements IMCPConfigService { private readonly MCP_CONFIG_FILE_NAME = 'mcp.json'; private readonly MCP_CONFIG_SAMPLE = { - mcpServers: [], + mcpServers: {}, } private readonly MCP_CONFIG_SAMPLE_STRING = JSON.stringify(this.MCP_CONFIG_SAMPLE, null, 2); + private mcpFileWatcher: IDisposable | null = null; constructor( @IFileService private readonly fileService: IFileService, @IPathService private readonly pathService: IPathService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IEditorService private readonly editorService: IEditorService, ) { super(); this._initialize(); } + // This method is called when the service is disposed + override dispose(): void { + // Custom cleanup logic goes here + console.log('MCPConfigService is being disposed'); + + // Call _removeMCPConfigFileWatch to clean up file watchers + this._removeMCPConfigFileWatch().catch(err => { + console.error('Error removing MCP config file watch:', err); + }); + + // Always call the parent class dispose method to ensure proper cleanup + super.dispose(); + } + private async _initialize() { - // Check logs - const mcpExists = await this._configFileExists(); - if (!mcpExists) { - console.log('MCP Config file does not exist. Creating...'); - await this._createMCPConfigFile(); - } else { - console.log('MCP Config file already exists.'); - } + try { + // Get the MCP config file path + const mcpConfigUri = await this._getMCPConfigPath(); + // Check if the file exists + if (!this._configFileExists(mcpConfigUri)) { + // Create the file if it doesn't exist + await this._createMCPConfigFile(mcpConfigUri); + console.log('MCP Config file created:', mcpConfigUri.toString()); + } + + // Add a watcher to the MCP config file + await this._setMCPConfigFileWatch(); + + } catch (error) { + console.error('Error initializing MCPConfigService:', error); + } } private async _getMCPConfigPath(): Promise { @@ -63,10 +86,8 @@ class MCPConfigService extends Disposable implements IMCPConfigService { return URI.file(mcpConfigPath); } - private async _configFileExists(): Promise { + private async _configFileExists(mcpConfigUri: URI): Promise { try { - const mcpConfigUri = await this._getMCPConfigPath(); - // Try to get file stats - if it succeeds, the file exists await this.fileService.stat(mcpConfigUri); return true; @@ -76,8 +97,7 @@ class MCPConfigService extends Disposable implements IMCPConfigService { } } - private async _createMCPConfigFile(): Promise { - const mcpConfigUri = await this._getMCPConfigPath(); + private async _createMCPConfigFile(mcpConfigUri: URI): Promise { // Create the directory if it doesn't exist await this.fileService.createFile(mcpConfigUri.with({ path: mcpConfigUri.path })); @@ -86,6 +106,64 @@ class MCPConfigService extends Disposable implements IMCPConfigService { const buffer = VSBuffer.fromString(this.MCP_CONFIG_SAMPLE_STRING); await this.fileService.writeFile(mcpConfigUri, buffer); } + + private async _parseMCPConfigFile(): Promise { + const mcpConfigUri = await this._getMCPConfigPath(); + + try { + const fileContent = await this.fileService.readFile(mcpConfigUri); + const contentString = fileContent.value.toString(); + return JSON.parse(contentString); + } catch (error) { + console.error('Error reading or parsing MCP config file:', error); + return null; + } + } + + private async _setMCPConfigFileWatch(): Promise { + const mcpConfigUri = await this._getMCPConfigPath(); + + // Watch the file for changes + this.mcpFileWatcher = this.fileService.watch(mcpConfigUri); + + // Listen for changes + this._register(this.fileService.onDidFilesChange(e => { + // Handle file changes + if (e.contains(mcpConfigUri)) { + console.log('MCP Config file changed:', JSON.stringify(e, null, 2)); + this._parseMCPConfigFile(); + } + })); + } + + private async _removeMCPConfigFileWatch(): Promise { + if (this.mcpFileWatcher) { + this.mcpFileWatcher.dispose(); + this.mcpFileWatcher = null; + } + } + + public async openMCPConfigFile(): Promise { + try { + // Get the MCP config file path + const mcpConfigUri = await this._getMCPConfigPath(); + + // Check if the file exists + if (!this._configFileExists(mcpConfigUri)) { + // Create the file if it doesn't exist + await this._createMCPConfigFile(mcpConfigUri); + console.log('MCP Config file created:', mcpConfigUri.toString()); + } + + // Open the MCP config file in the editor + await this.editorService.openEditor({ + resource: mcpConfigUri, + }); + + } catch (error) { + console.error('Error opening MCP config file:', error); + } + } } registerSingleton(IMCPConfigService, MCPConfigService, InstantiationType.Delayed); From 63083cec89d6fc1d1801d7308a6b1e7755585751 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 13 May 2025 22:23:49 -0400 Subject: [PATCH 10/98] Added MCP section to void settings --- .../react/src/void-settings-tsx/Settings.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 9fa3e7cb..209cb7fe 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -914,6 +914,7 @@ export const Settings = () => { const voidSettingsService = accessor.get('IVoidSettingsService') const chatThreadsService = accessor.get('IChatThreadService') const notificationService = accessor.get('INotificationService') + const mcpConfigService = accessor.get('IMCPConfigService') const onDownload = (t: 'Chats' | 'Settings') => { let dataStr: string @@ -1249,6 +1250,26 @@ Alternatively, place a \`.voidrules\` file in the root of your workspace. + +
+
+

MCP

+ { await mcpConfigService.openMCPConfigFile() }}> + Add MCP Server + +
+

+ +

+ +
+ {/* TODO: Add MCP server list with a live button */} +
+
+
From 4cca689c94684e1ee3f6603f647dcfefd54ab60c Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 13 May 2025 22:24:32 -0400 Subject: [PATCH 11/98] Moved all watcher and file creation logic to initialize and fixed bug with this._configFileExists --- .../contrib/void/common/mcpConfigService.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index 7500302c..14698bd5 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -63,7 +63,8 @@ class MCPConfigService extends Disposable implements IMCPConfigService { const mcpConfigUri = await this._getMCPConfigPath(); // Check if the file exists - if (!this._configFileExists(mcpConfigUri)) { + 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()); @@ -148,16 +149,13 @@ class MCPConfigService extends Disposable implements IMCPConfigService { // Get the MCP config file path const mcpConfigUri = await this._getMCPConfigPath(); - // Check if the file exists - if (!this._configFileExists(mcpConfigUri)) { - // Create the file if it doesn't exist - await this._createMCPConfigFile(mcpConfigUri); - console.log('MCP Config file created:', mcpConfigUri.toString()); - } - // Open the MCP config file in the editor await this.editorService.openEditor({ resource: mcpConfigUri, + options: { + pinned: true, + revealIfOpened: true, + } }); } catch (error) { From 7b9e5ce517cf98ab6da3bc588ee5a2bfc7675604 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Wed, 14 May 2025 11:08:24 -0400 Subject: [PATCH 12/98] Created mcpService file to handle MCP commands --- src/vs/workbench/contrib/void/common/mcpService.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/vs/workbench/contrib/void/common/mcpService.ts diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts new file mode 100644 index 00000000..e69de29b From 2377c182418031ea573ea520eae8902af106ff05 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Wed, 14 May 2025 11:23:02 -0400 Subject: [PATCH 13/98] Added @modelcontextprotocol/inspector-cli for MCP client interactions and server interactions --- package-lock.json | 50 +++++++++++++++++++++++++++++++++++++++++++---- package.json | 1 + 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5158d409..3000e743 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@mistralai/mistralai": "^1.6.0", + "@modelcontextprotocol/inspector-cli": "^0.12.0", "@modelcontextprotocol/sdk": "^1.10.2", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", @@ -2615,10 +2616,33 @@ "zod": ">= 3" } }, + "node_modules/@modelcontextprotocol/inspector-cli": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-cli/-/inspector-cli-0.12.0.tgz", + "integrity": "sha512-0sMKYqn2Dp3RwJPz/Ukz4FhRN8JNNTniCyOuzuCnb2r5ogLvi6eBaU0CQa2SnE33fFH9HFSOLSI3nHg7SzRJcQ==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.11.0", + "commander": "^13.1.0", + "spawn-rx": "^5.1.2" + }, + "bin": { + "mcp-inspector-cli": "build/cli.js" + } + }, + "node_modules/@modelcontextprotocol/inspector-cli/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz", - "integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz", + "integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==", "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -19493,6 +19517,15 @@ } ] }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -20547,6 +20580,16 @@ "node": ">= 0.10" } }, + "node_modules/spawn-rx": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-5.1.2.tgz", + "integrity": "sha512-/y7tJKALVZ1lPzeZZB9jYnmtrL7d0N2zkorii5a7r7dhHkWIuLTzZpZzMJLK1dmYRgX/NCc4iarTO3F7BS2c/A==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.7", + "rxjs": "^7.8.1" + } + }, "node_modules/spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", @@ -22273,7 +22316,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/tsscmp": { diff --git a/package.json b/package.json index 7a5bc366..30bb4ec9 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@mistralai/mistralai": "^1.6.0", + "@modelcontextprotocol/inspector-cli": "^0.12.0", "@modelcontextprotocol/sdk": "^1.10.2", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", From aed9f31185c8860bb08a9a9523a01cf40f0a28fb Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Wed, 14 May 2025 11:23:35 -0400 Subject: [PATCH 14/98] Added initial scaffolding --- .../contrib/void/common/mcpService.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index e69de29b..ebe3abb4 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -0,0 +1,49 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +export interface IMCPService { + readonly _serviceBrand: undefined; +} + +export const IMCPService = createDecorator('mcpConfigService'); + +class MCPService extends Disposable implements IMCPService { + _serviceBrand: undefined; + + // TODO: ADD MCP VARIABLES AND MEMORY HERE + + constructor( + ) { + super(); + this._initialize(); + } + + // This method is called when the service is disposed + override dispose(): void { + // Custom cleanup logic goes here + console.log('MCPService is being disposed'); + + // Always call the parent class dispose method to ensure proper cleanup + super.dispose(); + } + + + + private async _initialize() { + try { + console.log('MCPService initialized') + } catch (error) { + console.error('Error initializing MCPService:', error); + } + } + + // TODO: ADD MCP FUNCTIONS HERE +} + +registerSingleton(IMCPService, MCPService, InstantiationType.Delayed); From 716cb8d5915c45887dfe876ab330205283407272 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Wed, 14 May 2025 11:41:22 -0400 Subject: [PATCH 15/98] Replaced @modelcontextprotocol/inspector-cli for @modelcontextprotocol/sdk --- package-lock.json | 46 ++-------------------------------------------- package.json | 3 +-- 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3000e743..9fc60e62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@mistralai/mistralai": "^1.6.0", - "@modelcontextprotocol/inspector-cli": "^0.12.0", - "@modelcontextprotocol/sdk": "^1.10.2", + "@modelcontextprotocol/sdk": "^1.11.2", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", @@ -2616,29 +2615,6 @@ "zod": ">= 3" } }, - "node_modules/@modelcontextprotocol/inspector-cli": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-cli/-/inspector-cli-0.12.0.tgz", - "integrity": "sha512-0sMKYqn2Dp3RwJPz/Ukz4FhRN8JNNTniCyOuzuCnb2r5ogLvi6eBaU0CQa2SnE33fFH9HFSOLSI3nHg7SzRJcQ==", - "license": "MIT", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.0", - "commander": "^13.1.0", - "spawn-rx": "^5.1.2" - }, - "bin": { - "mcp-inspector-cli": "build/cli.js" - } - }, - "node_modules/@modelcontextprotocol/inspector-cli/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.11.2", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz", @@ -19517,15 +19493,6 @@ } ] }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -20580,16 +20547,6 @@ "node": ">= 0.10" } }, - "node_modules/spawn-rx": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-5.1.2.tgz", - "integrity": "sha512-/y7tJKALVZ1lPzeZZB9jYnmtrL7d0N2zkorii5a7r7dhHkWIuLTzZpZzMJLK1dmYRgX/NCc4iarTO3F7BS2c/A==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.7", - "rxjs": "^7.8.1" - } - }, "node_modules/spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", @@ -22316,6 +22273,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, "license": "0BSD" }, "node_modules/tsscmp": { diff --git a/package.json b/package.json index 30bb4ec9..e6341c09 100644 --- a/package.json +++ b/package.json @@ -79,8 +79,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@mistralai/mistralai": "^1.6.0", - "@modelcontextprotocol/inspector-cli": "^0.12.0", - "@modelcontextprotocol/sdk": "^1.10.2", + "@modelcontextprotocol/sdk": "^1.11.2", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", From b18ac4b6c0def5cf5db334ab5a6c0847b1cf0108 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Thu, 15 May 2025 22:26:11 -0400 Subject: [PATCH 16/98] Created mcpChannel to handle all mcp server setup and events in electron-main --- .../contrib/void/electron-main/mcpChannel.ts | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 src/vs/workbench/contrib/void/electron-main/mcpChannel.ts diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts new file mode 100644 index 00000000..83b08e1b --- /dev/null +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -0,0 +1,232 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +// registered in app.ts +// can't make a service responsible for this, because it needs +// to be connected to the main process and node dependencies + +import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +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, EventMCPServerSetupOnError, EventMCPServerSetupOnSuccess, MCPServerSuccessModel, MCPServerErrorModel } from '../common/mcpServiceTypes.js'; +import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; + +export class MCPChannel implements IServerChannel { + + // connected clients + private clients: { [clientId: string]: { client: Client, mcpConfig: MCPServerConfig } } = {} + private getClientConfig(serverName: string) { + return { + name: `${serverName}-client`, + version: '0.1.0', + // debug: true, + } + } + + // mcp emitters + private readonly mcpEmitters = { + serverSetup: { + success: new Emitter>(), + error: new Emitter>(), + }, + // toolCall: { + // success: new Emitter(), + // error: new Emitter(), + // }, + } satisfies { + [task in 'serverSetup']: { + success: Emitter>, + error: Emitter>, + } + } + + constructor( + // private readonly metricsService: IMetricsService, + ) { } + + // browser uses this to listen for changes + listen(_: unknown, event: string): Event { + + // server setup + if (event === 'onSuccess_serverSetup') return this.mcpEmitters.serverSetup.success.event; + else if (event === 'onError_serverSetup') return this.mcpEmitters.serverSetup.error.event; + + // tool call + // else if (event === 'onSuccess_toolCall') return this.mcpEmitters.toolCall.success.event; + // else if (event === 'onError_toolCall') return this.mcpEmitters.toolCall.error.event; + + // handle unknown events + else throw new Error(`Event not found: ${event}`); + } + + // 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._callSetupServers(params) + } + else if (command === 'closeAllServers') { + await this._callCloseAllServers() + } + else if (command === 'toggleServer') { + // TODO: HANDLE THIS + } + 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) + } + } + + // call functions + + private async _callSetupServers(mcpConfig: MCPConfig) { + + // Reset all servers + if (Object.keys(this.clients).length > 0) { + await this._callCloseAllServers() + } + + // Handle config file setup and changes + const { mcpServers } = mcpConfig + const serverNames = Object.keys(mcpServers) + if (serverNames.length === 0) { + // TODO: CHANGE THIS TO AN ERROR EVENT + console.log('No MCP servers found in config file.') + return + } + for (const serverName of serverNames) { + + // Get the server config + const server = mcpServers[serverName] + + if (server) { + // TODO: add a check if server is on or off + try { + await this._callSetupServer(server, serverName) + + } catch (err) { + // catches *any* error (including SSE fallback or Stdio connect) + console.error(`❌ Failed to connect to server "${serverName}":`, err); + // fire error event + // TODO: handle sending back the error + const typedErr = err as Error + console.log('Error Message: ', typedErr.message) + this.mcpEmitters.serverSetup.error.fire({ + model: { + serverName, + isLive: false, + isOn: false, + tools: [], + error: typedErr.message, + } + }) + // and then move on to the next server + continue; + } + } + } + } + + private async _callSetupServer(server: MCPServerConfig, serverName: string) { + + const clientConfig = this.getClientConfig(serverName) + const client = new Client(clientConfig) + let transport: Transport; + + if (server.url) { + // first try HTTP, fall back to SSE + try { + transport = new StreamableHTTPClientTransport(server.url); + await client.connect(transport); + console.log(`Connected via HTTP to ${serverName}`); + } catch (httpErr) { + console.warn(`HTTP failed for ${serverName}, trying SSE…`, httpErr); + transport = new SSEClientTransport(server.url); + await client.connect(transport); + console.log(`Connected via SSE to ${serverName}`); + } + } else if (server.command) { + console.log('ENV DATA: ', server.env) + transport = new StdioClientTransport({ + command: server.command, + args: server.args, + env: { + ...server.env, + ...process.env + } as Record, + }); + + client.onerror = (err) => { + // TODO: HANDLE SENDING AN EVENT BACK TO THE CLIENT + console.error(`Error in MCP client for ${serverName}:`, err); + } + + await client.connect(transport) + + console.log(`Connected via Stdio to ${serverName}`); + + const { tools } = await client.listTools() + + this.mcpEmitters.serverSetup.success.fire({ + model: { + serverName, + isLive: true, + isOn: true, + tools: tools, + } + }) + } else { + console.warn(`No url or command for server ${serverName}`); + return; + } + + // only add to clients map if connect succeeded + this.clients[serverName] = { client, mcpConfig: server }; + } + + private async _callCloseAllServers() { + for (const serverName in this.clients) { + await this._callCloseServer(serverName) + } + console.log('Closed all MCP servers'); + } + + private async _callCloseServer(serverName: string) { + if (this.clients[serverName]) { + const { client } = this.clients[serverName] + await client.close() + delete this.clients[serverName] + console.log(`Closed MCP server ${serverName}`); + } + } + + + // listen functions + + // private _onServerSetupSuccess(serverName: string) { + // this.mcpEmitters.serverSetup.success.fire() + // } + // private _onServerSetupError(error: Error) { + // // this.error = error + // console.log('WHAAAAT') + // console.log('Error in MCPChannel:', error) + // } + // private _onToolCallSuccess(serverName: string) { + // this.mcpEmitters.toolCall.success.fire() + // } + // private _onToolCallError(serverName: string) { + // this.mcpEmitters.toolCall.error.fire() + // } +} + From 4a862641cdc0f984c307efeb1ebbd04d3a34fb74 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Thu, 15 May 2025 22:27:05 -0400 Subject: [PATCH 17/98] Added a types file to handle all MCP Server interfaces and response values --- .../contrib/void/common/mcpServiceTypes.ts | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/vs/workbench/contrib/void/common/mcpServiceTypes.ts diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts new file mode 100644 index 00000000..e627895a --- /dev/null +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -0,0 +1,160 @@ +/** + * mcp-response-types.ts + * -------------------------------------------------- + * **Pure** TypeScript interfaces (no external imports) + * describing the JSON-RPC response shapes for: + * + * 1. tools/list -> ToolsListResponse + * 2. prompts/list -> PromptsListResponse + * 3. tools/call -> ToolCallResponse + * + * They are distilled directly from the official MCP + * 2025‑03‑26 specification: + * • Tools list response examples + * • Prompts list response examples + * • Tool call response examples + * + * Use them to get full IntelliSense when working with + * @modelcontextprotocol/inspector‑cli responses. + */ + +/* -------------------------------------------------- */ +/* Core JSON‑RPC envelope */ +/* -------------------------------------------------- */ + +export interface JsonRpcSuccess { + /** 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; +} + +/* -------------------------------------------------- */ +/* 1. tools/list */ +/* -------------------------------------------------- */ + +/** Minimal JSON‑Schema placeholder – adapt if you need stricter typing */ +export type JsonSchema = Record; + +export interface Tool { + /** Unique tool identifier */ + name: string; + /** Human‑readable description */ + description?: string; + /** JSON schema describing expected arguments */ + inputSchema?: JsonSchema; + /** Free‑form annotations describing behaviour, security, etc. */ + annotations?: Record; +} + +export interface ToolsListResult extends Paginated { + tools: Tool[]; +} + +export type ToolsListResponse = JsonRpcSuccess; + +/* -------------------------------------------------- */ +/* 2. prompts/list */ +/* -------------------------------------------------- */ + +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 PromptsListResult extends Paginated { + prompts: Prompt[]; +} + +export type PromptsListResponse = JsonRpcSuccess; + +/* -------------------------------------------------- */ +/* 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; +} + +/** 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 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; + +// Interface for mcpConfigObjects +export interface MCPServerConfig { + // Command-based server properties + command?: string; + args?: string[]; + env?: Record; + + // URL-based server properties + url?: URL; + headers?: Record; +} + +export interface MCPConfig { + mcpServers: Record; +} + +// Interface for mcpServerObjects in browser +export interface MCPServerObject { + // Command-based server properties + tools: Tool[], + isLive: boolean, + isOn: boolean, + error?: string, +} + +export interface MCPServers { + [serverName: string]: MCPServerObject; +} + +// Create separate types for success and error cases +export type MCPServerSuccessModel = MCPServerObject; +export type MCPServerErrorModel = Omit & { error: string }; + +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 = Parameters['onSuccess']>[0] +export type EventMCPServerSetupOnError = Parameters['onError']>[0] From 0074189191d2a03f4fff1b7894ee6b8fc567f82e Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Thu, 15 May 2025 22:28:14 -0400 Subject: [PATCH 18/98] Removed mcpService from common because it was supposed to handle setting up the servers but can't because of a lack of access to node dependencies --- .../contrib/void/common/mcpService.ts | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 src/vs/workbench/contrib/void/common/mcpService.ts diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts deleted file mode 100644 index ebe3abb4..00000000 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ /dev/null @@ -1,49 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; - -export interface IMCPService { - readonly _serviceBrand: undefined; -} - -export const IMCPService = createDecorator('mcpConfigService'); - -class MCPService extends Disposable implements IMCPService { - _serviceBrand: undefined; - - // TODO: ADD MCP VARIABLES AND MEMORY HERE - - constructor( - ) { - super(); - this._initialize(); - } - - // This method is called when the service is disposed - override dispose(): void { - // Custom cleanup logic goes here - console.log('MCPService is being disposed'); - - // Always call the parent class dispose method to ensure proper cleanup - super.dispose(); - } - - - - private async _initialize() { - try { - console.log('MCPService initialized') - } catch (error) { - console.error('Error initializing MCPService:', error); - } - } - - // TODO: ADD MCP FUNCTIONS HERE -} - -registerSingleton(IMCPService, MCPService, InstantiationType.Delayed); From a6297366834cd04a301151f658dc401fe1614686 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Thu, 15 May 2025 22:28:48 -0400 Subject: [PATCH 19/98] Registered mcpChannel --- src/vs/code/electron-main/app.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 4feafb34..1c103b1f 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -130,6 +130,7 @@ import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpda import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js'; import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js'; import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/sendLLMMessageChannel.js'; +import { MCPChannel } from '../../workbench/contrib/void/electron-main/mcpChannel.js'; /** * The main VS Code application. There will only ever be one instance, @@ -1243,6 +1244,9 @@ export class CodeApplication extends Disposable { const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService)); mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel); + const mcpChannel = new MCPChannel(); + mainProcessElectronServer.registerChannel('void-channel-mcp', mcpChannel); + // Extension Host Debug Broadcasting const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService)); mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel); From ca51bcaecc929c8883f39b6cf8b84ccf0b114c29 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Thu, 15 May 2025 22:29:22 -0400 Subject: [PATCH 20/98] Added @modelcontextprotocol/sdk to the allowed list of imports to avoid typescript error --- eslint.config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index 7acc50f2..4f014565 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -842,7 +842,9 @@ export default tseslint.config( '@xterm/xterm', 'yauzl', 'yazl', - 'zlib' + 'zlib', + // Void added this + '@modelcontextprotocol/sdk/**' ] }, { From 30655678fe71435643242e7f60e3bc177a037c24 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Thu, 15 May 2025 22:30:15 -0400 Subject: [PATCH 21/98] Connected this service to the mcpChannel for interaction with the servers --- .../contrib/void/common/mcpConfigService.ts | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index 14698bd5..0053cf71 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -13,6 +13,10 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { join } from '../../../../base/common/path.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; 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, EventMCPServerSetupOnSuccess, MCPServerSuccessModel } from './mcpServiceTypes.js'; +import { Event } from '../../../../base/common/event.js'; export interface IMCPConfigService { readonly _serviceBrand: undefined; @@ -30,19 +34,39 @@ class MCPConfigService extends Disposable implements IMCPConfigService { } 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 mcpServers: MCPServers = {} constructor( @IFileService private readonly fileService: IFileService, @IPathService private readonly pathService: IPathService, @IProductService private readonly productService: IProductService, @IEditorService private readonly editorService: IEditorService, + @IMainProcessService private readonly mainProcessService: IMainProcessService, ) { super(); + // 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('onSuccess_serverSetup') satisfies Event & { serverName: string }>)(e => { + // Handle successful server setup + const { model } = e; + const { serverName, isLive, isOn, tools } = model; + this.mcpServers[serverName] = { + isLive, + isOn, + tools + } + console.log('MCP Server setup successful:', serverName, JSON.stringify(model, null, 2)); + })); + // Initialize the service this._initialize(); } // This method is called when the service is disposed - override dispose(): void { + override async dispose(): Promise { // Custom cleanup logic goes here console.log('MCPConfigService is being disposed'); @@ -51,6 +75,9 @@ class MCPConfigService extends Disposable implements IMCPConfigService { 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(); } @@ -70,6 +97,15 @@ class MCPConfigService extends Disposable implements IMCPConfigService { console.log('MCP Config file created:', mcpConfigUri.toString()); } + // Parse the MCP config file + const mcpConfig = await this._parseMCPConfigFile(); + if (mcpConfig) { + // Process the MCP config file + console.log('MCP Config file parsed:', JSON.stringify(mcpConfig, null, 2)); + this.channel.call('setupServers', mcpConfig) + } + + // Add a watcher to the MCP config file await this._setMCPConfigFileWatch(); @@ -108,7 +144,7 @@ class MCPConfigService extends Disposable implements IMCPConfigService { await this.fileService.writeFile(mcpConfigUri, buffer); } - private async _parseMCPConfigFile(): Promise { + private async _parseMCPConfigFile(): Promise { const mcpConfigUri = await this._getMCPConfigPath(); try { @@ -128,11 +164,15 @@ class MCPConfigService extends Disposable implements IMCPConfigService { this.mcpFileWatcher = this.fileService.watch(mcpConfigUri); // Listen for changes - this._register(this.fileService.onDidFilesChange(e => { + this._register(this.fileService.onDidFilesChange(async e => { // Handle file changes if (e.contains(mcpConfigUri)) { console.log('MCP Config file changed:', JSON.stringify(e, null, 2)); - this._parseMCPConfigFile(); + const mcpConfig = await this._parseMCPConfigFile(); + if (mcpConfig && mcpConfig.mcpServers) { + // Call the setupServers method in the main process + this.channel.call('setupServers', mcpConfig) + } } })); } @@ -162,6 +202,11 @@ class MCPConfigService extends Disposable implements IMCPConfigService { console.error('Error opening MCP config file:', error); } } + + public getMCPServers(): MCPServers { + // Call the getMCPServers method in the main process + return this.mcpServers; + } } registerSingleton(IMCPConfigService, MCPConfigService, InstantiationType.Delayed); From 05e1e61f080195650b8c4e47f5f2ec1562328646 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Thu, 15 May 2025 22:47:24 -0400 Subject: [PATCH 22/98] Added error channel event listener and a utility function to handle all server events --- .../contrib/void/common/mcpConfigService.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index 0053cf71..ed6c9519 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -15,7 +15,7 @@ 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, EventMCPServerSetupOnSuccess, MCPServerSuccessModel } from './mcpServiceTypes.js'; +import { MCPServers, MCPConfig, EventMCPServerSetupOnSuccess, MCPServerSuccessModel, MCPServerErrorModel, EventMCPServerSetupOnError, MCPServerObject } from './mcpServiceTypes.js'; import { Event } from '../../../../base/common/event.js'; export interface IMCPConfigService { @@ -50,17 +50,8 @@ class MCPConfigService extends Disposable implements IMCPConfigService { // 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('onSuccess_serverSetup') satisfies Event & { serverName: string }>)(e => { - // Handle successful server setup - const { model } = e; - const { serverName, isLive, isOn, tools } = model; - this.mcpServers[serverName] = { - isLive, - isOn, - tools - } - console.log('MCP Server setup successful:', serverName, JSON.stringify(model, null, 2)); - })); + this._register((this.channel.listen('onSuccess_serverSetup') satisfies Event & { serverName: string }>)(e => this._onServerEvent(e, 'success'))); + this._register((this.channel.listen('onError_serverSetup') satisfies Event & { serverName: string }>)(e => this._onServerEvent(e, 'error'))); // Initialize the service this._initialize(); } @@ -114,6 +105,19 @@ class MCPConfigService extends Disposable implements IMCPConfigService { } } + private async _onServerEvent(e: EventMCPServerSetupOnSuccess | EventMCPServerSetupOnError, eventType: 'success' | 'error') { + const { model } = e; + const { serverName, isLive, isOn, tools, error } = model; + const serverObject: MCPServerObject = { + isLive, + isOn, + tools, + error: eventType === 'error' ? error : undefined, + }; + this.mcpServers[serverName] = serverObject; + console.log(`MCP Server Setup ${eventType}:`, serverName, error); + } + private async _getMCPConfigPath(): Promise { // Get the appropriate directory based on dev mode const appName = this.productService.dataFolderName From 2817cdd8a6c74506b372894032c73d84d10b8333 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Fri, 16 May 2025 00:38:09 -0400 Subject: [PATCH 23/98] Created function for getting all tools from mcp servers formatted for void --- .../contrib/void/common/mcpConfigService.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index ed6c9519..ac0d16d3 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -17,10 +17,13 @@ import { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; import { MCPServers, MCPConfig, EventMCPServerSetupOnSuccess, MCPServerSuccessModel, MCPServerErrorModel, EventMCPServerSetupOnError, MCPServerObject } from './mcpServiceTypes.js'; import { Event } from '../../../../base/common/event.js'; +import { InternalToolInfo } from './prompt/prompts.js'; export interface IMCPConfigService { readonly _serviceBrand: undefined; openMCPConfigFile(): Promise; + getMCPServers(): MCPServers; + getAllToolsFormatted(): InternalToolInfo[]; } export const IMCPConfigService = createDecorator('mcpConfigService'); @@ -211,6 +214,32 @@ class MCPConfigService extends Disposable implements IMCPConfigService { // Call the getMCPServers method in the main process return this.mcpServers; } + + public getAllToolsFormatted(): InternalToolInfo[] { + const allTools = Object.values(this.mcpServers).flatMap(server => { + return server.tools.map(tool => { + // Convert JsonSchema to the expected format + const convertedParams: { [paramName: string]: { description: string } } = {}; + + // Assuming tool.inputSchema has a 'properties' field that contains parameter definitions + if (tool.inputSchema && tool.inputSchema.properties) { + Object.entries(tool.inputSchema.properties).forEach(([paramName, paramSchema]: [string, any]) => { + convertedParams[paramName] = { + description: paramSchema.description || '' + }; + }); + } + + return { + description: tool.description || '', + params: convertedParams, + name: tool.name, + }; + }); + }); + return allTools; + } + } registerSingleton(IMCPConfigService, MCPConfigService, InstantiationType.Delayed); From 243349a90fb97995306d707135f924d21bc298c9 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Fri, 16 May 2025 23:23:35 -0400 Subject: [PATCH 24/98] Created a server list UI and hooked it up to Config Service --- .../src/void-settings-tsx/MCPServersList.tsx | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/MCPServersList.tsx 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 new file mode 100644 index 00000000..507cb415 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/MCPServersList.tsx @@ -0,0 +1,145 @@ +import { VoidSwitch, VoidToggleBgDarken } from '../util/inputs.js'; +import { MCPServerObject, MCPServers } from '../../../../common/mcpServiceTypes.js'; +import { use, useEffect, useState } from 'react'; +import { useAccessor } from '../util/services.js'; + +export interface Tool { + /** Unique tool identifier */ + name: string; + /** Human‑readable description */ + description?: string; + /** JSON schema describing expected arguments */ + inputSchema?: any; + /** Free‑form annotations describing behaviour, security, etc. */ + annotations?: Record; +} + +// Command display component +const CommandDisplay = ({ command }: {command: string}) => { + return ( +
+ {command} +
+ ); +}; + + +interface MCPServerProps { + name: string; + server: MCPServerObject; +} + +// MCP Server component +const MCPServer = ({ name, server }: MCPServerProps) => { + + return ( +
+
+ {/* Status indicator */} +
+ + {/* Server name */} +
{name}
+ + {/* Power toggle switch */} +
+ { + server.isOn = !server.isOn; + }} + /> +
+
+ + {/* Tools section */} +
+
+ {server.tools.length > 0 ? ( + server.tools.map((tool) => ( + + {tool.name} + + )) + ) : ( + No tools available + )} +
+
+ + {/* Command display */} + {server.command && ( +
+
Command:
+ +
+ )} + + {/* Error message if present */} + {server.error && ( +
+ + + + + + {server.error} +
+ )} +
+ ); +}; + +// Main component that renders the list of servers +const MCPServersList = () => { + // Dummy data for two servers + + + const accessor = useAccessor(); + const mcpConfigService = accessor.get('IMCPConfigService'); + const [mcpServers, setMCPServers] = useState({}); + + // Get all servers from MCPConfigService + useEffect(() => { + // Initial fetch + const servers = mcpConfigService.getMCPServers(); + if (servers) { + // Do something with the servers + console.log('MCP Servers:', servers); + setMCPServers(servers); + } + + // Subscribe to changes + const disposable = mcpConfigService.onDidChangeMCPServers((response) => { + const {serverName, serverObject} = response; + + // Update the server object in the state + setMCPServers(prevServers => ({ + ...prevServers, // Spread all previous servers + [serverName]: serverObject // Override or add the new server + })); + }); + + // Clean up subscription when component unmounts + return () => disposable.dispose(); + }, [mcpConfigService]); + + return ( +
+
+ {Object.entries(mcpServers).map(([name, server]) => ( +
+ +
+ ))} +
+
+ ); +}; + +export default MCPServersList; From a0f9309e02b15c7ab9cd5a8ed42815120ed458c9 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Fri, 16 May 2025 23:24:29 -0400 Subject: [PATCH 25/98] Updated the service to include a service that updates the browser whenever an event from the main-process comes in --- .../contrib/void/common/mcpConfigService.ts | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index ac0d16d3..747b1a27 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -16,7 +16,7 @@ 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, EventMCPServerSetupOnSuccess, MCPServerSuccessModel, MCPServerErrorModel, EventMCPServerSetupOnError, MCPServerObject } from './mcpServiceTypes.js'; -import { Event } from '../../../../base/common/event.js'; +import { Event, Emitter } from '../../../../base/common/event.js'; import { InternalToolInfo } from './prompt/prompts.js'; export interface IMCPConfigService { @@ -24,6 +24,12 @@ export interface IMCPConfigService { openMCPConfigFile(): Promise; getMCPServers(): MCPServers; getAllToolsFormatted(): InternalToolInfo[]; + onDidChangeMCPServers: Event; +} + +interface MCPServerEmitterReturns { + serverName: string; + serverObject: MCPServerObject; } export const IMCPConfigService = createDecorator('mcpConfigService'); @@ -42,6 +48,10 @@ class MCPConfigService extends Disposable implements IMCPConfigService { // list of MCP servers pulled from mcpChannel private mcpServers: MCPServers = {} + // Emitters for client + private readonly _onDidChangeMCPServers = new Emitter(); + public readonly onDidChangeMCPServers = this._onDidChangeMCPServers.event; + constructor( @IFileService private readonly fileService: IFileService, @IPathService private readonly pathService: IPathService, @@ -93,8 +103,12 @@ class MCPConfigService extends Disposable implements IMCPConfigService { // Parse the MCP config file const mcpConfig = await this._parseMCPConfigFile(); + if (mcpConfig) { - // Process the MCP config file + // Create the initial server list + await this._createInitialServerList(mcpConfig); + + // Setup the server list console.log('MCP Config file parsed:', JSON.stringify(mcpConfig, null, 2)); this.channel.call('setupServers', mcpConfig) } @@ -108,17 +122,44 @@ class MCPConfigService extends Disposable implements IMCPConfigService { } } + private async _createInitialServerList(mcpConfig: MCPConfig) { + // Create a list of servers from the MCP config + if (mcpConfig && mcpConfig.mcpServers) { + + const formattedServers: MCPServers = {}; + for (const serverName in mcpConfig.mcpServers) { + const serverConfig = mcpConfig.mcpServers[serverName]; + if (serverConfig) { + const fullCommand = serverConfig.command || serverConfig.args?.join(' ') || undefined; + const serverObject: MCPServerObject = { + status: 'loading', + isOn: false, + tools: [], + command: fullCommand + }; + formattedServers[serverName] = serverObject; + } + } + } else { + this.mcpServers = {}; + } + } + private async _onServerEvent(e: EventMCPServerSetupOnSuccess | EventMCPServerSetupOnError, eventType: 'success' | 'error') { const { model } = e; - const { serverName, isLive, isOn, tools, error } = model; + const { serverName, status, isOn, tools, error, command } = model; const serverObject: MCPServerObject = { - isLive, + status, isOn, tools, + command, error: eventType === 'error' ? error : undefined, }; this.mcpServers[serverName] = serverObject; console.log(`MCP Server Setup ${eventType}:`, serverName, error); + + // Fire the event to notify listeners + this._onDidChangeMCPServers.fire({ serverName, serverObject }); } private async _getMCPConfigPath(): Promise { @@ -177,6 +218,9 @@ class MCPConfigService extends Disposable implements IMCPConfigService { console.log('MCP Config file changed:', JSON.stringify(e, null, 2)); const mcpConfig = await this._parseMCPConfigFile(); if (mcpConfig && mcpConfig.mcpServers) { + // Create the initial server list + await this._createInitialServerList(mcpConfig); + // Call the setupServers method in the main process this.channel.call('setupServers', mcpConfig) } From 8afaec30da4df604b8b1aeac4793b4e1a234ac13 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Fri, 16 May 2025 23:25:01 -0400 Subject: [PATCH 26/98] Updated the types to include commmand for display --- src/vs/workbench/contrib/void/common/mcpServiceTypes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index e627895a..bb0b7f71 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -136,8 +136,9 @@ export interface MCPConfig { export interface MCPServerObject { // Command-based server properties tools: Tool[], - isLive: boolean, + status: 'loading' | 'error' | 'success', isOn: boolean, + command?: string, error?: string, } From 543c6f05509c5ad3d23ef64ef842336a38a048d2 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Fri, 16 May 2025 23:25:31 -0400 Subject: [PATCH 27/98] Sent back command formatted for display --- .../contrib/void/electron-main/mcpChannel.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 83b08e1b..971e9e2c 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -122,13 +122,19 @@ export class MCPChannel implements IServerChannel { // TODO: handle sending back the error const typedErr = err as Error console.log('Error Message: ', typedErr.message) + + // Get the full command string for display + const fullCommand = `${server.command} ${server.args?.join(' ') || ''}` + console.log('Full Command: ', fullCommand) + this.mcpEmitters.serverSetup.error.fire({ model: { serverName, - isLive: false, + status: 'error', isOn: false, tools: [], error: typedErr.message, + command: fullCommand, } }) // and then move on to the next server @@ -178,12 +184,17 @@ export class MCPChannel implements IServerChannel { const { tools } = await client.listTools() + // Create a full command string for display + const fullCommand = `${server.command} ${server.args?.join(' ') || ''}` + console.log('Full Command: ', fullCommand) + this.mcpEmitters.serverSetup.success.fire({ model: { serverName, - isLive: true, + status: 'success', isOn: true, tools: tools, + command: fullCommand, } }) } else { From c669a987c540e4d357cb69e706e791548f757428 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Fri, 16 May 2025 23:28:06 -0400 Subject: [PATCH 28/98] Removed the custom toggle button and added MCPServersList to the main Settings --- .../browser/react/src/void-settings-tsx/MCPServersList.tsx | 2 +- .../void/browser/react/src/void-settings-tsx/Settings.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) 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 507cb415..c2da1aef 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,4 +1,4 @@ -import { VoidSwitch, VoidToggleBgDarken } from '../util/inputs.js'; +import { VoidSwitch } from '../util/inputs.js'; import { MCPServerObject, MCPServers } from '../../../../common/mcpServiceTypes.js'; import { use, useEffect, useState } from 'react'; import { useAccessor } from '../util/services.js'; diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 20409eb0..ff31f4f2 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -19,6 +19,7 @@ import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsSer import Severity from '../../../../../../../base/common/severity.js' import { getModelCapabilities, modelOverrideKeys, ModelOverrides } from '../../../../common/modelCapabilities.js'; import { TransferEditorType, TransferFilesInfo } from '../../../extensionTransferTypes.js'; +import MCPServersList from './MCPServersList.js'; const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => { @@ -1259,9 +1260,7 @@ Edit your MCP configuration file to add or modify server connections. `} chatMessageLocation={undefined} /> -
- {/* TODO: Add MCP server list with a live button */} -
+
From 3c96193b713a938331088c242d4a72bee6508bb2 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Sat, 17 May 2025 00:03:44 -0400 Subject: [PATCH 29/98] Added an event for reinitializing all servers when config file is updated --- .../react/src/void-settings-tsx/MCPServersList.tsx | 10 ++++++++++ .../workbench/contrib/void/common/mcpConfigService.ts | 11 +++++++++++ 2 files changed, 21 insertions(+) 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 c2da1aef..fd555158 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 @@ -125,6 +125,16 @@ const MCPServersList = () => { })); }); + // Subscribe to config file changes + const disposableConfig = mcpConfigService.onDidUpdateConfigFile((response) => { + const {mcpServers} = response; + if (mcpServers) { + // Reset all servers to "loading" state + console.log('MCP Servers:', mcpServers); + setMCPServers(mcpServers); + } + }); + // Clean up subscription when component unmounts return () => disposable.dispose(); }, [mcpConfigService]); diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpConfigService.ts index 747b1a27..2054a157 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpConfigService.ts @@ -25,6 +25,7 @@ export interface IMCPConfigService { getMCPServers(): MCPServers; getAllToolsFormatted(): InternalToolInfo[]; onDidChangeMCPServers: Event; + onDidUpdateConfigFile: Event; } interface MCPServerEmitterReturns { @@ -32,6 +33,10 @@ interface MCPServerEmitterReturns { serverObject: MCPServerObject; } +interface MCPConfigFileUpdateReturns { + mcpServers: MCPServers; +} + export const IMCPConfigService = createDecorator('mcpConfigService'); class MCPConfigService extends Disposable implements IMCPConfigService { @@ -50,7 +55,9 @@ class MCPConfigService extends Disposable implements IMCPConfigService { // Emitters for client private readonly _onDidChangeMCPServers = new Emitter(); + private readonly _onDidUpdateConfigFile = new Emitter(); public readonly onDidChangeMCPServers = this._onDidChangeMCPServers.event; + public readonly onDidUpdateConfigFile = this._onDidUpdateConfigFile.event; constructor( @IFileService private readonly fileService: IFileService, @@ -140,8 +147,12 @@ class MCPConfigService extends Disposable implements IMCPConfigService { formattedServers[serverName] = serverObject; } } + // Fire the event to notify listeners + this._onDidUpdateConfigFile.fire({ mcpServers: formattedServers }); } else { this.mcpServers = {}; + // Fire the event to notify listeners + this._onDidUpdateConfigFile.fire({ mcpServers: {} }); } } From 065470afc1a9d0f4780cc207bc27add0cbdbbdc7 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Sat, 17 May 2025 17:36:37 -0400 Subject: [PATCH 30/98] Updated the channel it include events for add, update, delete, and loading. Fixed a bug related to client.close() throwing and error --- .../contrib/void/electron-main/mcpChannel.ts | 267 ++++++++++++------ 1 file changed, 175 insertions(+), 92 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 971e9e2c..6cda79f5 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -13,13 +13,14 @@ 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, EventMCPServerSetupOnError, EventMCPServerSetupOnSuccess, MCPServerSuccessModel, MCPServerErrorModel } from '../common/mcpServiceTypes.js'; +import { MCPConfig, MCPServerConfig, MCPServerErrorModel, MCPAddResponse, MCPServerEventAddParam, MCPServerEventUpdateParam, MCPServerEventDeleteParam, MCPUpdateResponse, MCPServerModel, MCPDeleteResponse, MCPServerEventLoadingParam } from '../common/mcpServiceTypes.js'; import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +import { equals } from '../../../../base/common/objects.js'; export class MCPChannel implements IServerChannel { // connected clients - private clients: { [clientId: string]: { client: Client, mcpConfig: MCPServerConfig } } = {} + private clients: { [clientId: string]: { client?: Client, mcpConfig: MCPServerConfig, formattedServer: MCPServerModel } } = {} private getClientConfig(serverName: string) { return { name: `${serverName}-client`, @@ -30,18 +31,22 @@ export class MCPChannel implements IServerChannel { // mcp emitters private readonly mcpEmitters = { - serverSetup: { - success: new Emitter>(), - error: new Emitter>(), - }, + serverEvent: { + add: new Emitter(), + update: new Emitter(), + delete: new Emitter(), + loading: new Emitter(), + } // toolCall: { // success: new Emitter(), // error: new Emitter(), // }, } satisfies { - [task in 'serverSetup']: { - success: Emitter>, - error: Emitter>, + [event in 'serverEvent']: { + add: Emitter, + update: Emitter, + delete: Emitter, + loading: Emitter, } } @@ -52,13 +57,11 @@ export class MCPChannel implements IServerChannel { // browser uses this to listen for changes listen(_: unknown, event: string): Event { - // server setup - if (event === 'onSuccess_serverSetup') return this.mcpEmitters.serverSetup.success.event; - else if (event === 'onError_serverSetup') return this.mcpEmitters.serverSetup.error.event; - - // tool call - // else if (event === 'onSuccess_toolCall') return this.mcpEmitters.toolCall.success.event; - // else if (event === 'onError_toolCall') return this.mcpEmitters.toolCall.error.event; + // 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; // handle unknown events else throw new Error(`Event not found: ${event}`); @@ -92,10 +95,8 @@ export class MCPChannel implements IServerChannel { private async _callSetupServers(mcpConfig: MCPConfig) { - // Reset all servers - if (Object.keys(this.clients).length > 0) { - await this._callCloseAllServers() - } + // Get all prevServers + const prevServers = { ...this.clients } // Handle config file setup and changes const { mcpServers } = mcpConfig @@ -105,42 +106,86 @@ export class MCPChannel implements IServerChannel { console.log('No MCP servers found in config file.') return } - for (const serverName of serverNames) { + const getPrevAndNewServerConfig = (serverName: string) => { + const prevMCPConfig = prevServers[serverName]?.mcpConfig + const newMCPConfig = mcpServers[serverName] + return { prevMCPConfig, newMCPConfig } + } - // Get the server config - const server = mcpServers[serverName] - - if (server) { - // TODO: add a check if server is on or off - try { - await this._callSetupServer(server, serverName) - - } catch (err) { - // catches *any* error (including SSE fallback or Stdio connect) - console.error(`❌ Failed to connect to server "${serverName}":`, err); - // fire error event - // TODO: handle sending back the error - const typedErr = err as Error - console.log('Error Message: ', typedErr.message) - - // Get the full command string for display - const fullCommand = `${server.command} ${server.args?.join(' ') || ''}` - console.log('Full Command: ', fullCommand) - - this.mcpEmitters.serverSetup.error.fire({ - model: { - serverName, - status: 'error', - isOn: false, - tools: [], - error: typedErr.message, - command: fullCommand, - } - }) - // and then move on to the next server - continue; - } + // 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)) } + return isAdded + }) + 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)) + } + }) + 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)) + } + return isDeleted + }) + + // Check if no changes were made + if (addedServers.length === 0 && updatedServers.length === 0 && deletedServers.length === 0) { + console.log('No changes to MCP servers found.') + return + } + + if (addedServers.length > 0) { + // Handle added servers + const addPromises: Promise[] = addedServers.map(async (serverName) => { + const addedServer = await this._safeSetupServer(mcpServers[serverName], serverName) + return { + event: 'add', + newServer: addedServer, + name: serverName, + } as MCPAddResponse + }); + const formattedAddedResponses = await Promise.all(addPromises); + formattedAddedResponses.forEach((formattedResponse) => (this.mcpEmitters.serverEvent.add.fire({ response: formattedResponse }))); + } + + if (updatedServers.length > 0) { + // Handle updated servers + const updatePromises: Promise[] = updatedServers.map(async (serverName) => { + const prevServer = this.clients[serverName]?.formattedServer; + const newServer = await this._safeSetupServer(mcpServers[serverName], serverName) + return { + prevServer, + newServer: newServer, + event: 'update', + name: serverName, + } as MCPUpdateResponse + }); + const formattedUpdatedResponses = await Promise.all(updatePromises); + formattedUpdatedResponses.forEach((formattedResponse) => (this.mcpEmitters.serverEvent.update.fire({ response: formattedResponse }))); + } + + if (deletedServers.length > 0) { + // Handle deleted servers + const deletePromises: Promise[] = deletedServers.map(async (serverName) => { + const prevServer = this.clients[serverName]?.formattedServer; + await this._callCloseServer(serverName) + return { + event: 'delete', + prevServer, + name: serverName, + } as MCPDeleteResponse + }); + const formattedDeletedResponses = await Promise.all(deletePromises); + formattedDeletedResponses.forEach((formattedResponse) => (this.mcpEmitters.serverEvent.delete.fire({ response: formattedResponse }))); } } @@ -149,6 +194,7 @@ export class MCPChannel implements IServerChannel { const clientConfig = this.getClientConfig(serverName) const client = new Client(clientConfig) let transport: Transport; + let formattedServer: MCPServerModel; if (server.url) { // first try HTTP, fall back to SSE @@ -156,11 +202,24 @@ export class MCPChannel implements IServerChannel { transport = new StreamableHTTPClientTransport(server.url); await client.connect(transport); console.log(`Connected via HTTP to ${serverName}`); + const { tools } = await client.listTools() + formattedServer = { + status: 'success', + isOn: true, + tools: tools, + command: server.url.toString(), + } } catch (httpErr) { console.warn(`HTTP failed for ${serverName}, trying SSE…`, httpErr); transport = new SSEClientTransport(server.url); await client.connect(transport); console.log(`Connected via SSE to ${serverName}`); + formattedServer = { + status: 'success', + isOn: true, + tools: [], + command: server.url.toString(), + } } } else if (server.command) { console.log('ENV DATA: ', server.env) @@ -173,37 +232,60 @@ export class MCPChannel implements IServerChannel { } as Record, }); - client.onerror = (err) => { - // TODO: HANDLE SENDING AN EVENT BACK TO THE CLIENT - console.error(`Error in MCP client for ${serverName}:`, err); - } - await client.connect(transport) - console.log(`Connected via Stdio to ${serverName}`); - + // Get the tools from the server const { tools } = await client.listTools() // Create a full command string for display const fullCommand = `${server.command} ${server.args?.join(' ') || ''}` - console.log('Full Command: ', fullCommand) - this.mcpEmitters.serverSetup.success.fire({ - model: { - serverName, - status: 'success', - isOn: true, - tools: tools, - command: fullCommand, - } - }) + // Format server object + formattedServer = { + status: 'success', + isOn: true, + tools: tools, + command: fullCommand, + } + } else { - console.warn(`No url or command for server ${serverName}`); - return; + throw new Error(`No url or command for server ${serverName}`); } - // only add to clients map if connect succeeded - this.clients[serverName] = { client, mcpConfig: server }; + + this.clients[serverName] = { client, mcpConfig: server, formattedServer } + return formattedServer; + } + + // Helper function to safely setup a server + private async _safeSetupServer(serverConfig: MCPServerConfig, serverName: string) { + try { + return await this._callSetupServer(serverConfig, serverName) + } catch (err) { + const typedErr = err as Error + console.error(`❌ Failed to connect to server "${serverName}":`, err) + + let fullCommand = '' + if (serverConfig.command) { + fullCommand = `${serverConfig.command} ${serverConfig.args?.join(' ') || ''}` + } + + const formattedError: MCPServerErrorModel = { + status: 'error', + isOn: false, + tools: [], + error: typedErr.message, + command: fullCommand, + } + + // Add the error to the clients object + this.clients[serverName] = { + mcpConfig: serverConfig, + formattedServer: formattedError, + } + + return formattedError + } } private async _callCloseAllServers() { @@ -216,28 +298,29 @@ export class MCPChannel implements IServerChannel { private async _callCloseServer(serverName: string) { if (this.clients[serverName]) { const { client } = this.clients[serverName] - await client.close() + if (client) { + await client.close() + } delete this.clients[serverName] console.log(`Closed MCP server ${serverName}`); } } + // Util functions - // listen functions - - // private _onServerSetupSuccess(serverName: string) { - // this.mcpEmitters.serverSetup.success.fire() - // } - // private _onServerSetupError(error: Error) { - // // this.error = error - // console.log('WHAAAAT') - // console.log('Error in MCPChannel:', error) - // } - // private _onToolCallSuccess(serverName: string) { - // this.mcpEmitters.toolCall.success.fire() - // } - // private _onToolCallError(serverName: string) { - // this.mcpEmitters.toolCall.error.fire() - // } + private _getLoadingServerObject(serverName: string): MCPServerEventLoadingParam { + return { + response: { + event: 'loading', + name: serverName, + newServer: { + status: 'loading', + isOn: false, + tools: [], + command: '', + } + } + } + } } From d3757e81b47f2710491b95c11896ef621bcb8d9d Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Sat, 17 May 2025 17:41:10 -0400 Subject: [PATCH 31/98] Changed mcpConfigService to mcpService and updated events to match channel --- .../void/browser/react/src/util/services.tsx | 4 +- .../{mcpConfigService.ts => mcpService.ts} | 156 ++++++++---------- 2 files changed, 73 insertions(+), 87 deletions(-) rename src/vs/workbench/contrib/void/common/{mcpConfigService.ts => mcpService.ts} (69%) 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 689c9923..3d71f2ea 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 @@ -51,7 +51,7 @@ import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService import { ITerminalService } from '../../../../../terminal/browser/terminal.js' import { ISearchService } from '../../../../../../services/search/common/search.js' import { IExtensionManagementService } from '../../../../../../../platform/extensionManagement/common/extensionManagement.js' -import { IMCPConfigService } from '../../../../common/mcpConfigService.js'; +import { IMCPService } from '../../../../common/mcpService.js'; // normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes @@ -216,7 +216,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => { ITerminalService: accessor.get(ITerminalService), IExtensionManagementService: accessor.get(IExtensionManagementService), IExtensionTransferService: accessor.get(IExtensionTransferService), - IMCPConfigService: accessor.get(IMCPConfigService), + IMCPService: accessor.get(IMCPService), } as const return reactAccessor diff --git a/src/vs/workbench/contrib/void/common/mcpConfigService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts similarity index 69% rename from src/vs/workbench/contrib/void/common/mcpConfigService.ts rename to src/vs/workbench/contrib/void/common/mcpService.ts index 2054a157..804abfe8 100644 --- a/src/vs/workbench/contrib/void/common/mcpConfigService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -15,31 +15,24 @@ 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, EventMCPServerSetupOnSuccess, MCPServerSuccessModel, MCPServerErrorModel, EventMCPServerSetupOnError, MCPServerObject } from './mcpServiceTypes.js'; +import { MCPServers, MCPConfig, MCPServerEventParam, MCPServerEventAddParam, MCPServerEventUpdateParam, MCPServerEventDeleteParam, MCPServerEventLoadingParam } from './mcpServiceTypes.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { InternalToolInfo } from './prompt/prompts.js'; -export interface IMCPConfigService { +export interface IMCPService { readonly _serviceBrand: undefined; openMCPConfigFile(): Promise; getMCPServers(): MCPServers; getAllToolsFormatted(): InternalToolInfo[]; - onDidChangeMCPServers: Event; - onDidUpdateConfigFile: Event; + onDidAddServer: Event; + onDidUpdateServer: Event; + onDidDeleteServer: Event; + onLoadingServers: Event; } -interface MCPServerEmitterReturns { - serverName: string; - serverObject: MCPServerObject; -} +export const IMCPService = createDecorator('mcpConfigService'); -interface MCPConfigFileUpdateReturns { - mcpServers: MCPServers; -} - -export const IMCPConfigService = createDecorator('mcpConfigService'); - -class MCPConfigService extends Disposable implements IMCPConfigService { +class MCPService extends Disposable implements IMCPService { _serviceBrand: undefined; private readonly MCP_CONFIG_FILE_NAME = 'mcp.json'; @@ -53,11 +46,15 @@ class MCPConfigService extends Disposable implements IMCPConfigService { // list of MCP servers pulled from mcpChannel private mcpServers: MCPServers = {} - // Emitters for client - private readonly _onDidChangeMCPServers = new Emitter(); - private readonly _onDidUpdateConfigFile = new Emitter(); - public readonly onDidChangeMCPServers = this._onDidChangeMCPServers.event; - public readonly onDidUpdateConfigFile = this._onDidUpdateConfigFile.event; + // Emitters for server events + private readonly _onDidAddServer = new Emitter(); + private readonly _onDidUpdateServer = new Emitter(); + private readonly _onDidDeleteServer = new Emitter(); + private readonly _onLoadingServers = new Emitter(); + public readonly onDidAddServer = this._onDidAddServer.event; + public readonly onDidUpdateServer = this._onDidUpdateServer.event; + public readonly onDidDeleteServer = this._onDidDeleteServer.event; + public readonly onLoadingServers = this._onLoadingServers.event; constructor( @IFileService private readonly fileService: IFileService, @@ -70,8 +67,10 @@ class MCPConfigService extends Disposable implements IMCPConfigService { // 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('onSuccess_serverSetup') satisfies Event & { serverName: string }>)(e => this._onServerEvent(e, 'success'))); - this._register((this.channel.listen('onError_serverSetup') satisfies Event & { serverName: string }>)(e => this._onServerEvent(e, 'error'))); + 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('onLoading_server') satisfies Event)(e => this._onServerEvent(e))); // Initialize the service this._initialize(); } @@ -79,7 +78,7 @@ class MCPConfigService extends Disposable implements IMCPConfigService { // This method is called when the service is disposed override async dispose(): Promise { // Custom cleanup logic goes here - console.log('MCPConfigService is being disposed'); + console.log('MCPService is being disposed'); // Call _removeMCPConfigFileWatch to clean up file watchers this._removeMCPConfigFileWatch().catch(err => { @@ -113,7 +112,7 @@ class MCPConfigService extends Disposable implements IMCPConfigService { if (mcpConfig) { // Create the initial server list - await this._createInitialServerList(mcpConfig); + // await this._createInitialServerList(mcpConfig); // Setup the server list console.log('MCP Config file parsed:', JSON.stringify(mcpConfig, null, 2)); @@ -125,71 +124,38 @@ class MCPConfigService extends Disposable implements IMCPConfigService { await this._setMCPConfigFileWatch(); } catch (error) { - console.error('Error initializing MCPConfigService:', error); + console.error('Error initializing MCPService:', error); } } - private async _createInitialServerList(mcpConfig: MCPConfig) { - // Create a list of servers from the MCP config - if (mcpConfig && mcpConfig.mcpServers) { + private async _onServerEvent(e: MCPServerEventParam) { - const formattedServers: MCPServers = {}; - for (const serverName in mcpConfig.mcpServers) { - const serverConfig = mcpConfig.mcpServers[serverName]; - if (serverConfig) { - const fullCommand = serverConfig.command || serverConfig.args?.join(' ') || undefined; - const serverObject: MCPServerObject = { - status: 'loading', - isOn: false, - tools: [], - command: fullCommand - }; - formattedServers[serverName] = serverObject; - } - } - // Fire the event to notify listeners - this._onDidUpdateConfigFile.fire({ mcpServers: formattedServers }); - } else { - this.mcpServers = {}; - // Fire the event to notify listeners - this._onDidUpdateConfigFile.fire({ mcpServers: {} }); + 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: EventMCPServerSetupOnSuccess | EventMCPServerSetupOnError, eventType: 'success' | 'error') { - const { model } = e; - const { serverName, status, isOn, tools, error, command } = model; - const serverObject: MCPServerObject = { - status, - isOn, - tools, - command, - error: eventType === 'error' ? error : undefined, - }; - this.mcpServers[serverName] = serverObject; - console.log(`MCP Server Setup ${eventType}:`, serverName, error); + 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); + } - // Fire the event to notify listeners - this._onDidChangeMCPServers.fire({ serverName, serverObject }); - } + 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); + } - private async _getMCPConfigPath(): 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); - 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; + 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); } } @@ -226,11 +192,8 @@ class MCPConfigService extends Disposable implements IMCPConfigService { this._register(this.fileService.onDidFilesChange(async e => { // Handle file changes if (e.contains(mcpConfigUri)) { - console.log('MCP Config file changed:', JSON.stringify(e, null, 2)); const mcpConfig = await this._parseMCPConfigFile(); if (mcpConfig && mcpConfig.mcpServers) { - // Create the initial server list - await this._createInitialServerList(mcpConfig); // Call the setupServers method in the main process this.channel.call('setupServers', mcpConfig) @@ -246,6 +209,8 @@ class MCPConfigService extends Disposable implements IMCPConfigService { } } + // Client-side functions + public async openMCPConfigFile(): Promise { try { // Get the MCP config file path @@ -295,6 +260,27 @@ class MCPConfigService extends Disposable implements IMCPConfigService { return allTools; } + // utility functions + + private async _getMCPConfigPath(): 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); + 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; + } + } } -registerSingleton(IMCPConfigService, MCPConfigService, InstantiationType.Delayed); +registerSingleton(IMCPService, MCPService, InstantiationType.Eager); From b1b7afe8a0933247449e946712f9a6c96ad27e47 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Sat, 17 May 2025 17:41:46 -0400 Subject: [PATCH 32/98] Updated service types to match new events and organized it better --- .../contrib/void/common/mcpServiceTypes.ts | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index bb0b7f71..ec5f728d 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -18,6 +18,7 @@ * @modelcontextprotocol/inspector‑cli responses. */ + /* -------------------------------------------------- */ /* Core JSON‑RPC envelope */ /* -------------------------------------------------- */ @@ -116,7 +117,8 @@ export interface ToolCallResult { export type ToolCallResponse = JsonRpcSuccess; -// Interface for mcpConfigObjects +// MCP SERVER CONFIG FILE TYPES ----------------------------- + export interface MCPServerConfig { // Command-based server properties command?: string; @@ -132,7 +134,8 @@ export interface MCPConfig { mcpServers: Record; } -// Interface for mcpServerObjects in browser +// SERVER EVENT TYPES ------------------------------------------ + export interface MCPServerObject { // Command-based server properties tools: Tool[], @@ -150,6 +153,7 @@ export interface MCPServers { export type MCPServerSuccessModel = MCPServerObject; export type MCPServerErrorModel = Omit & { error: string }; + export type MCPServerSetupParams = { serverName: string; onSuccess: (param: { model: MCPServerSuccessModel & { serverName: string } }) => void; @@ -159,3 +163,54 @@ export type MCPServerSetupParams = { // Listener event types export type EventMCPServerSetupOnSuccess = Parameters['onSuccess']>[0] export type EventMCPServerSetupOnError = Parameters['onError']>[0] + +type MCPServerEventType = 'add' | 'update' | 'delete' | 'loading'; + +export type MCPServerModel = MCPServerSuccessModel | MCPServerErrorModel; + +interface MCPServerResponseBase { + name: string; + event: MCPServerEventType; + newServer?: MCPServerModel; + prevServer?: MCPServerModel; +} + +type EventTypeConstraints = { + 'add': { + prevServer?: never; + newServer: MCPServerModel; + }; + 'update': { + prevServer: MCPServerModel; + newServer: MCPServerModel; + }; + 'delete': { + newServer?: never; + prevServer: MCPServerModel; + }; + 'loading': { + prevServer?: never; + newServer: MCPServerModel; + } +} + +type MCPEventResponse = Omit & 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; + +// Event parameter types +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; + + From 7f4470674dfc57f413ba243dfc818d0b231070a8 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Sat, 17 May 2025 17:42:28 -0400 Subject: [PATCH 33/98] Updated the client side to use new events and mcpService --- .../src/void-settings-tsx/MCPServersList.tsx | 89 +++++++++++-------- .../react/src/void-settings-tsx/Settings.tsx | 4 +- 2 files changed, 53 insertions(+), 40 deletions(-) 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 fd555158..ef27c58b 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,8 @@ import { VoidSwitch } from '../util/inputs.js'; -import { MCPServerObject, MCPServers } from '../../../../common/mcpServiceTypes.js'; -import { use, useEffect, useState } from 'react'; +import { MCPServerEventParam, MCPServerObject, MCPServers } from '../../../../common/mcpServiceTypes.js'; +import { useEffect, useState } from 'react'; import { useAccessor } from '../util/services.js'; +import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; export interface Tool { /** Unique tool identifier */ @@ -97,59 +98,71 @@ const MCPServer = ({ name, server }: MCPServerProps) => { // Main component that renders the list of servers const MCPServersList = () => { - // Dummy data for two servers - const accessor = useAccessor(); - const mcpConfigService = accessor.get('IMCPConfigService'); + const mcpService = accessor.get('IMCPService'); const [mcpServers, setMCPServers] = useState({}); // Get all servers from MCPConfigService useEffect(() => { + console.log('RUNNING MCPServersList EFFECT'); // Initial fetch - const servers = mcpConfigService.getMCPServers(); + const servers = mcpService.getMCPServers(); if (servers) { // Do something with the servers console.log('MCP Servers:', servers); setMCPServers(servers); } - // Subscribe to changes - const disposable = mcpConfigService.onDidChangeMCPServers((response) => { - const {serverName, serverObject} = response; - - // Update the server object in the state - setMCPServers(prevServers => ({ - ...prevServers, // Spread all previous servers - [serverName]: serverObject // Override or add the new server - })); - }); - - // Subscribe to config file changes - const disposableConfig = mcpConfigService.onDidUpdateConfigFile((response) => { - const {mcpServers} = response; - if (mcpServers) { - // Reset all servers to "loading" state - console.log('MCP Servers:', mcpServers); - setMCPServers(mcpServers); + const handleListeners = (e: MCPServerEventParam) => { + if (e.response.event === 'add' || e.response.event === 'update' || e.response.event === 'loading') { + // Handle the add event + const { name, newServer } = e.response; + setMCPServers(prevServers => ({ + ...prevServers, + [name]: newServer + })); + return; } - }); + if (e.response.event === '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)); // Clean up subscription when component unmounts - return () => disposable.dispose(); - }, [mcpConfigService]); + return () => { + console.log('Cleaning up subscriptions'); + disposables.forEach(disposable => disposable.dispose()); + }; - return ( -
-
- {Object.entries(mcpServers).map(([name, server]) => ( -
- -
- ))} -
-
- ); + }, [mcpService]); + + return ( +
+
+ {Object.entries(mcpServers).map(([name, server]) => ( +
+ +
+ ))} +
+
+ ); }; export default MCPServersList; diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index ff31f4f2..8567df65 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -909,7 +909,7 @@ export const Settings = () => { const voidSettingsService = accessor.get('IVoidSettingsService') const chatThreadsService = accessor.get('IChatThreadService') const notificationService = accessor.get('INotificationService') - const mcpConfigService = accessor.get('IMCPConfigService') + const mcpService = accessor.get('IMCPService') const onDownload = (t: 'Chats' | 'Settings') => { let dataStr: string @@ -1249,7 +1249,7 @@ Alternatively, place a \`.voidrules\` file in the root of your workspace.

MCP

- { await mcpConfigService.openMCPConfigFile() }}> + { await mcpService.openMCPConfigFile() }}> Add MCP Server
From 079ce49d17864b340a3510c4dbdbb8058fb0cab9 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Sat, 17 May 2025 18:21:01 -0400 Subject: [PATCH 34/98] Fixed bug where isUpdated is never returned --- src/vs/workbench/contrib/void/electron-main/mcpChannel.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 6cda79f5..1ff4c5f5 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -127,6 +127,7 @@ export class MCPChannel implements IServerChannel { if (isUpdated) { this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName)) } + return isUpdated }) const deletedServers = Object.keys(prevServers).filter((serverName) => { const { prevMCPConfig, newMCPConfig } = getPrevAndNewServerConfig(serverName) From 59dd7d7eea3a7fa97f68cfd7069667aa01f855a4 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Sat, 17 May 2025 18:21:55 -0400 Subject: [PATCH 35/98] Added proper error handling for config file parsing errors --- .../src/void-settings-tsx/MCPServersList.tsx | 63 +++++++++++-------- .../contrib/void/common/mcpService.ts | 36 ++++++++--- .../contrib/void/common/mcpServiceTypes.ts | 8 +++ 3 files changed, 74 insertions(+), 33 deletions(-) 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 ef27c58b..a4192a37 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,5 +1,5 @@ import { VoidSwitch } from '../util/inputs.js'; -import { MCPServerEventParam, MCPServerObject, MCPServers } from '../../../../common/mcpServiceTypes.js'; +import { MCPConfigParseError, MCPServerEventParam, MCPServerObject, MCPServers } from '../../../../common/mcpServiceTypes.js'; import { useEffect, useState } from 'react'; import { useAccessor } from '../util/services.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; @@ -102,6 +102,7 @@ const MCPServersList = () => { const accessor = useAccessor(); const mcpService = accessor.get('IMCPService'); const [mcpServers, setMCPServers] = useState({}); + const [mcpConfigError, setMCPConfigError] = useState(null); // Get all servers from MCPConfigService useEffect(() => { @@ -114,35 +115,13 @@ const MCPServersList = () => { setMCPServers(servers); } - const handleListeners = (e: MCPServerEventParam) => { - if (e.response.event === 'add' || e.response.event === 'update' || e.response.event === 'loading') { - // Handle the add event - const { name, newServer } = e.response; - setMCPServers(prevServers => ({ - ...prevServers, - [name]: newServer - })); - return; - } - if (e.response.event === '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 () => { @@ -152,14 +131,48 @@ const MCPServersList = () => { }, [mcpService]); + const handleListeners = (e: MCPServerEventParam | MCPConfigParseError) => { + if (e.response.event === 'config-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') { + // Handle the add event + const { name, newServer } = e.response; + setMCPServers(prevServers => ({ + ...prevServers, + [name]: newServer + })); + return; + } + if (e.response.event === '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 (
- {Object.entries(mcpServers).map(([name, server]) => ( + {!mcpConfigError && Object.entries(mcpServers).map(([name, server]) => (
))} + {mcpConfigError && ( +
+ {mcpConfigError} +
+ )}
); diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 804abfe8..043a60c5 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -15,7 +15,7 @@ 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 } from './mcpServiceTypes.js'; +import { MCPServers, MCPConfig, MCPServerEventParam, MCPServerEventAddParam, MCPServerEventUpdateParam, MCPServerEventDeleteParam, MCPServerEventLoadingParam, MCPConfigParseError } from './mcpServiceTypes.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { InternalToolInfo } from './prompt/prompts.js'; @@ -28,6 +28,7 @@ export interface IMCPService { onDidUpdateServer: Event; onDidDeleteServer: Event; onLoadingServers: Event; + onConfigParsingError: Event; } export const IMCPService = createDecorator('mcpConfigService'); @@ -51,10 +52,12 @@ class MCPService extends Disposable implements IMCPService { private readonly _onDidUpdateServer = new Emitter(); private readonly _onDidDeleteServer = new Emitter(); private readonly _onLoadingServers = new Emitter(); + private readonly _onConfigParsingError = new Emitter(); public readonly onDidAddServer = this._onDidAddServer.event; public readonly onDidUpdateServer = this._onDidUpdateServer.event; public readonly onDidDeleteServer = this._onDidDeleteServer.event; public readonly onLoadingServers = this._onLoadingServers.event; + public readonly onConfigParsingError = this._onConfigParsingError.event; constructor( @IFileService private readonly fileService: IFileService, @@ -110,12 +113,9 @@ class MCPService extends Disposable implements IMCPService { // Parse the MCP config file const mcpConfig = await this._parseMCPConfigFile(); - if (mcpConfig) { - // Create the initial server list - // await this._createInitialServerList(mcpConfig); + if (mcpConfig && mcpConfig.mcpServers) { // Setup the server list - console.log('MCP Config file parsed:', JSON.stringify(mcpConfig, null, 2)); this.channel.call('setupServers', mcpConfig) } @@ -170,14 +170,35 @@ class MCPService extends Disposable implements IMCPService { } private async _parseMCPConfigFile(): Promise { + // Remove any previous config parsing error + // This isn't super intuitive, but it works + this._onConfigParsingError.fire({ + response: { + event: 'config-error', + error: null + } + }); + + // Process config file const mcpConfigUri = await this._getMCPConfigPath(); try { const fileContent = await this.fileService.readFile(mcpConfigUri); const contentString = fileContent.value.toString(); - return JSON.parse(contentString); + const configJson = JSON.parse(contentString); + if (!configJson.mcpServers) { + throw new Error('Invalid MCP config file: missing mcpServers property'); + } + return configJson as MCPConfig; } catch (error) { - console.error('Error reading or parsing MCP config file:', error); + const fullError = `Error parsing MCP config file: ${error}`; + console.error(fullError); + this._onConfigParsingError.fire({ + response: { + event: 'config-error', + error: fullError + } + }); return null; } } @@ -194,7 +215,6 @@ class MCPService extends Disposable implements IMCPService { if (e.contains(mcpConfigUri)) { const mcpConfig = await this._parseMCPConfigFile(); if (mcpConfig && mcpConfig.mcpServers) { - // Call the setupServers method in the main process this.channel.call('setupServers', mcpConfig) } diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index ec5f728d..0b411bac 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -134,6 +134,14 @@ export interface MCPConfig { mcpServers: Record; } +export interface MCPConfigParseError { + // Error message + response: { + event: 'config-error'; + error: string | null; + } +} + // SERVER EVENT TYPES ------------------------------------------ export interface MCPServerObject { From b68d8fd8e8f03a880111cea968b9c34df3d1a915 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Sat, 17 May 2025 19:23:14 -0400 Subject: [PATCH 36/98] Added toggle service to turn mcp servers on and off --- .../src/void-settings-tsx/MCPServersList.tsx | 29 +++++++--- .../contrib/void/common/mcpService.ts | 7 ++- .../contrib/void/common/mcpServiceTypes.ts | 2 +- .../contrib/void/electron-main/mcpChannel.ts | 53 +++++++++++++++++-- 4 files changed, 79 insertions(+), 12 deletions(-) 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 a4192a37..eab9e876 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 @@ -3,6 +3,7 @@ import { MCPConfigParseError, MCPServerEventParam, MCPServerObject, MCPServers } import { useEffect, useState } from 'react'; import { useAccessor } from '../util/services.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { off } from 'process'; export interface Tool { /** Unique tool identifier */ @@ -33,11 +34,29 @@ interface MCPServerProps { // MCP Server component const MCPServer = ({ name, server }: MCPServerProps) => { + const accessor = useAccessor(); + const mcpService = accessor.get('IMCPService'); + + const handleChangeEvent = (e: boolean) => { + // Handle the change event + mcpService.toggleServer(name, e); + } + + // Needs to be raw CSS because + // Tailwind won't compile the colors + // dynamically + const serverStatusColorStyles = { + success: { backgroundColor: '#10B981' }, // green-500 equivalent + error: { backgroundColor: '#EF4444' }, // red-500 equivalent + loading: { backgroundColor: '#F59E0B' }, // yellow-500 equivalent + offline: { backgroundColor: '#6B7280' } // gray-500 equivalent + }; + return (
{/* Status indicator */} -
+
{/* Server name */}
{name}
@@ -47,9 +66,7 @@ const MCPServer = ({ name, server }: MCPServerProps) => { { - server.isOn = !server.isOn; - }} + onChange={handleChangeEvent} />
@@ -57,7 +74,7 @@ const MCPServer = ({ name, server }: MCPServerProps) => { {/* Tools section */}
- {server.tools.length > 0 ? ( + {server.isOn && server.tools.length > 0 ? ( server.tools.map((tool) => ( {
{/* Command display */} - {server.command && ( + {server.isOn && server.command && (
Command:
diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 043a60c5..1e0beaa6 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -24,6 +24,7 @@ export interface IMCPService { openMCPConfigFile(): Promise; getMCPServers(): MCPServers; getAllToolsFormatted(): InternalToolInfo[]; + toggleServer(serverName: string, isOn: boolean): Promise; onDidAddServer: Event; onDidUpdateServer: Event; onDidDeleteServer: Event; @@ -215,7 +216,7 @@ class MCPService extends Disposable implements IMCPService { if (e.contains(mcpConfigUri)) { const mcpConfig = await this._parseMCPConfigFile(); if (mcpConfig && mcpConfig.mcpServers) { - // Call the setupServers method in the main process + // Set up the server list this.channel.call('setupServers', mcpConfig) } } @@ -280,6 +281,10 @@ class MCPService extends Disposable implements IMCPService { return allTools; } + public async toggleServer(serverName: string, isOn: boolean): Promise { + this.channel.call('toggleServer', { serverName, isOn }) + } + // utility functions private async _getMCPConfigPath(): Promise { diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index 0b411bac..fea0d34b 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -147,7 +147,7 @@ export interface MCPConfigParseError { export interface MCPServerObject { // Command-based server properties tools: Tool[], - status: 'loading' | 'error' | 'success', + status: 'loading' | 'error' | 'success' | 'offline', isOn: boolean, command?: string, error?: string, diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 1ff4c5f5..9055e329 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -77,7 +77,7 @@ export class MCPChannel implements IServerChannel { await this._callCloseAllServers() } else if (command === 'toggleServer') { - // TODO: HANDLE THIS + await this._handleToggleServer(params.serverName, params.isOn) } else if (command === 'callTool') { // TODO: HANDLE THIS @@ -179,6 +179,7 @@ export class MCPChannel implements IServerChannel { const deletePromises: Promise[] = deletedServers.map(async (serverName) => { const prevServer = this.clients[serverName]?.formattedServer; await this._callCloseServer(serverName) + this._callRemoveServer(serverName) return { event: 'delete', prevServer, @@ -292,6 +293,7 @@ export class MCPChannel implements IServerChannel { private async _callCloseAllServers() { for (const serverName in this.clients) { await this._callCloseServer(serverName) + this._callRemoveServer(serverName) } console.log('Closed all MCP servers'); } @@ -302,21 +304,64 @@ export class MCPChannel implements IServerChannel { if (client) { await client.close() } - delete this.clients[serverName] console.log(`Closed MCP server ${serverName}`); } } + private _callRemoveServer(serverName: string) { + if (this.clients[serverName]) { + delete this.clients[serverName] + console.log(`Removed MCP server ${serverName}`); + } + } + + private async _handleToggleServer(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)) + const formattedServer = await this._callSetupServer(this.clients[serverName].mcpConfig, serverName) + this.mcpEmitters.serverEvent.update.fire({ + response: { + event: 'update', + name: serverName, + newServer: formattedServer, + prevServer: prevServer, + } + }) + } else { + // Handle turning off the server + this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName, isOn)) + this._callCloseServer(serverName) + this.mcpEmitters.serverEvent.update.fire({ + response: { + event: 'update', + name: serverName, + newServer: { + status: 'offline', + isOn, + tools: [], + command: '', + // Explicitly set error to undefined + // to reset the error state + error: undefined, + }, + prevServer: prevServer, + } + }) + } + } + // Util functions - private _getLoadingServerObject(serverName: string): MCPServerEventLoadingParam { + private _getLoadingServerObject(serverName: string, isOn = true): MCPServerEventLoadingParam { return { response: { event: 'loading', name: serverName, newServer: { status: 'loading', - isOn: false, + isOn, tools: [], command: '', } From 2d910835b7b4739d255c282d19dffbd83b865a9d Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Sun, 18 May 2025 04:04:49 -0400 Subject: [PATCH 37/98] Updated the void settings to include mcpServerState --- .../void/common/voidSettingsService.ts | 64 ++++++++++++++++++- .../contrib/void/common/voidSettingsTypes.ts | 10 +++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 73c9eceb..dd1b1f36 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -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 } from './voidSettingsTypes.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'; // name is the name in the dropdown @@ -43,6 +43,7 @@ export type VoidSettingsState = { readonly optionsOfModelSelection: OptionsOfModelSelection; readonly overridesOfModel: OverridesOfModel; readonly globalSettings: GlobalSettings; + readonly mcpServerStates: MCPServerStates; readonly _modelOptions: ModelOption[] // computed based on the two above items } @@ -62,6 +63,7 @@ export interface IVoidSettingsService { setModelSelectionOfFeature: SetModelSelectionOfFeatureFn; setOptionsOfModelSelection: SetOptionsOfModelSelection; setGlobalSetting: SetGlobalSettingFn; + setMCPServerStates: (newStates: MCPServerStates) => Promise; // setting to undefined CLEARS it, unlike others: setOverridesOfModel(providerName: ProviderName, modelName: string, overrides: Partial | undefined): Promise; @@ -73,6 +75,9 @@ 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; + removeMCPServers(serverNames: string[]): Promise; + updateMCPServerState(serverName: string, newIsOn: boolean): Promise; } @@ -212,6 +217,7 @@ const defaultState = () => { optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} }, overridesOfModel: deepClone(defaultOverridesOfModel), _modelOptions: [], // computed later + mcpServerStates: {}, } return d } @@ -361,6 +367,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const newGlobalSettings = this.state.globalSettings const newOverridesOfModel = this.state.overridesOfModel + const newMCPServerStates = this.state.mcpServerStates const newState = { modelSelectionOfFeature: newModelSelectionOfFeature, @@ -368,6 +375,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { settingsOfProvider: newSettingsOfProvider, globalSettings: newGlobalSettings, overridesOfModel: newOverridesOfModel, + mcpServerStates: newMCPServerStates, } this.state = _validatedModelState(newState) @@ -486,6 +494,21 @@ 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) { @@ -531,6 +554,45 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { return true } + // MCP Server State + + addMCPServers = async (newMCPStates: MCPServerStates) => { + const { mcpServerStates } = this.state + const newMCPServerStates = { + ...mcpServerStates, + ...newMCPStates, + } + await this.setMCPServerStates(newMCPServerStates) + this._metricsService.capture('Add MCP Server', { servers: Object.keys(newMCPStates).join(', ') }); + } + + removeMCPServers = async (serverNames: string[]) => { + const { mcpServerStates } = this.state + const newMCPServerStates = { + ...mcpServerStates, + } + serverNames.forEach(serverName => { + if (serverName in newMCPServerStates) { + delete newMCPServerStates[serverName] + } + }) + await this.setMCPServerStates(newMCPServerStates) + this._metricsService.capture('Remove MCP Server', { servers: serverNames.join(', ') }); + } + + updateMCPServerState = async (serverName: string, newIsOn: boolean) => { + const { mcpServerStates } = this.state + if (!(serverName in mcpServerStates)) return // if not in list, do nothing + const newMCPServerStates = { + ...mcpServerStates, + [serverName]: { + isOn: newIsOn, + }, + } + await this.setMCPServerStates(newMCPServerStates) + this._metricsService.capture('Update MCP Server State', { serverName, newIsOn }); + } + } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index a911dfe6..f6195133 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -491,3 +491,13 @@ export type OverridesOfModel = { const overridesOfModel = {} as OverridesOfModel for (const providerName of providerNames) { overridesOfModel[providerName] = {} } export const defaultOverridesOfModel = overridesOfModel + + + +export interface MCPServerStates { + [serverName: string]: MCPServerState; +} + +export interface MCPServerState { + isOn: boolean; +} From 807086f7b613e655386ae74e7b38aded7bfccc0c Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Sun, 18 May 2025 04:05:41 -0400 Subject: [PATCH 38/98] Added persistence between sessions for mcpServerState --- .../contrib/void/common/mcpService.ts | 46 ++++++++++++++++++- .../contrib/void/electron-main/mcpChannel.ts | 40 +++++++++------- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 1e0beaa6..e5690521 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -18,6 +18,8 @@ import { IMainProcessService } from '../../../../platform/ipc/common/mainProcess import { MCPServers, MCPConfig, MCPServerEventParam, MCPServerEventAddParam, MCPServerEventUpdateParam, MCPServerEventDeleteParam, MCPServerEventLoadingParam, MCPConfigParseError } 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'; export interface IMCPService { readonly _serviceBrand: undefined; @@ -66,6 +68,7 @@ class MCPService extends Disposable implements IMCPService { @IProductService private readonly productService: IProductService, @IEditorService private readonly editorService: IEditorService, @IMainProcessService private readonly mainProcessService: IMainProcessService, + @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, ) { super(); // Register the service with the instantiation service @@ -111,13 +114,17 @@ class MCPService extends Disposable implements IMCPService { console.log('MCP Config file created:', mcpConfigUri.toString()); } + // Wait for VoidSettingsService to initialize before proceeding + await this.voidSettingsService.waitForInitState; + // Parse the MCP config file const mcpConfig = await this._parseMCPConfigFile(); if (mcpConfig && mcpConfig.mcpServers) { + const updatedServerStates = await this._handleServerStateChange(mcpConfig); // Setup the server list - this.channel.call('setupServers', mcpConfig) + this.channel.call('setupServers', { mcpConfig, serverStates: updatedServerStates }) } @@ -216,8 +223,10 @@ class MCPService extends Disposable implements IMCPService { 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) + this.channel.call('setupServers', { mcpConfig, serverStates: updatedServerStates }) } } })); @@ -283,6 +292,8 @@ class MCPService extends Disposable implements IMCPService { public async toggleServer(serverName: string, isOn: boolean): Promise { this.channel.call('toggleServer', { serverName, isOn }) + // Update the server state in the local mcpServers list + await this.voidSettingsService.updateMCPServerState(serverName, isOn); } // utility functions @@ -306,6 +317,37 @@ class MCPService extends Disposable implements IMCPService { return false; } } + + // Handle server state changes + private async _handleServerStateChange(mcpConfig: MCPConfig): Promise { + // Get the server states from Void Settings Service + const savedServerStates = this.voidSettingsService.state.mcpServerStates; + + // Parse the MCP config file for servers + const availableServers = Object.keys(mcpConfig.mcpServers); + + // Handle added servers + const addedServers = availableServers.filter(serverName => !savedServerStates[serverName]); + const addedServersObject = addedServers.reduce((acc, serverName) => { + acc[serverName] = { isOn: true }; + return acc; + }, {} as MCPServerStates); + await this.voidSettingsService.addMCPServers(addedServersObject); + + // Handle removed servers + const removedServers = Object.keys(savedServerStates).filter(serverName => availableServers.indexOf(serverName) === -1); + await this.voidSettingsService.removeMCPServers(removedServers); + + // Compile the updated server list as MCPServerStates + const updatedServers = Object.keys(savedServerStates).reduce((acc, serverName) => { + if (availableServers.includes(serverName)) { + acc[serverName] = savedServerStates[serverName]; + } + return acc; + }, {} as MCPServerStates); + + return updatedServers; + } } registerSingleton(IMCPService, MCPService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 9055e329..c1474b8d 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -16,6 +16,7 @@ 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 { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import { equals } from '../../../../base/common/objects.js'; +import { MCPServerStates } from '../common/voidSettingsTypes.js'; export class MCPChannel implements IServerChannel { @@ -93,7 +94,9 @@ export class MCPChannel implements IServerChannel { // call functions - private async _callSetupServers(mcpConfig: MCPConfig) { + private async _callSetupServers(params: { mcpConfig: MCPConfig, serverStates: MCPServerStates }) { + + const { mcpConfig, serverStates } = params // Get all prevServers const prevServers = { ...this.clients } @@ -117,7 +120,7 @@ export class MCPChannel implements IServerChannel { const { prevMCPConfig, newMCPConfig } = getPrevAndNewServerConfig(serverName) const isAdded = !prevMCPConfig && newMCPConfig if (isAdded) { - this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName)) + this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName, serverStates[serverName]?.isOn)) } return isAdded }) @@ -125,7 +128,7 @@ export class MCPChannel implements IServerChannel { const { prevMCPConfig, newMCPConfig } = getPrevAndNewServerConfig(serverName) const isUpdated = prevMCPConfig && newMCPConfig && !equals(prevMCPConfig, newMCPConfig) if (isUpdated) { - this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName)) + this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName, serverStates[serverName]?.isOn)) } return isUpdated }) @@ -133,7 +136,7 @@ export class MCPChannel implements IServerChannel { const { prevMCPConfig, newMCPConfig } = getPrevAndNewServerConfig(serverName) const isDeleted = prevMCPConfig && !newMCPConfig if (isDeleted) { - this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName)) + this.mcpEmitters.serverEvent.loading.fire(this._getLoadingServerObject(serverName, serverStates[serverName]?.isOn)) } return isDeleted }) @@ -147,7 +150,7 @@ export class MCPChannel implements IServerChannel { if (addedServers.length > 0) { // Handle added servers const addPromises: Promise[] = addedServers.map(async (serverName) => { - const addedServer = await this._safeSetupServer(mcpServers[serverName], serverName) + const addedServer = await this._safeSetupServer(mcpServers[serverName], serverName, serverStates[serverName]?.isOn) return { event: 'add', newServer: addedServer, @@ -162,7 +165,7 @@ export class MCPChannel implements IServerChannel { // Handle updated servers const updatePromises: Promise[] = updatedServers.map(async (serverName) => { const prevServer = this.clients[serverName]?.formattedServer; - const newServer = await this._safeSetupServer(mcpServers[serverName], serverName) + const newServer = await this._safeSetupServer(mcpServers[serverName], serverName, serverStates[serverName]?.isOn) return { prevServer, newServer: newServer, @@ -191,7 +194,7 @@ export class MCPChannel implements IServerChannel { } } - private async _callSetupServer(server: MCPServerConfig, serverName: string) { + private async _callSetupServer(server: MCPServerConfig, serverName: string, isOn = true) { const clientConfig = this.getClientConfig(serverName) const client = new Client(clientConfig) @@ -206,8 +209,8 @@ export class MCPChannel implements IServerChannel { console.log(`Connected via HTTP to ${serverName}`); const { tools } = await client.listTools() formattedServer = { - status: 'success', - isOn: true, + status: isOn ? 'success' : 'offline', + isOn, tools: tools, command: server.url.toString(), } @@ -217,8 +220,8 @@ export class MCPChannel implements IServerChannel { await client.connect(transport); console.log(`Connected via SSE to ${serverName}`); formattedServer = { - status: 'success', - isOn: true, + status: isOn ? 'success' : 'offline', + isOn, tools: [], command: server.url.toString(), } @@ -244,8 +247,8 @@ export class MCPChannel implements IServerChannel { // Format server object formattedServer = { - status: 'success', - isOn: true, + status: isOn ? 'success' : 'offline', + isOn, tools: tools, command: fullCommand, } @@ -260,9 +263,9 @@ export class MCPChannel implements IServerChannel { } // Helper function to safely setup a server - private async _safeSetupServer(serverConfig: MCPServerConfig, serverName: string) { + private async _safeSetupServer(serverConfig: MCPServerConfig, serverName: string, isOn = true) { try { - return await this._callSetupServer(serverConfig, serverName) + return await this._callSetupServer(serverConfig, serverName, isOn) } catch (err) { const typedErr = err as Error console.error(`❌ Failed to connect to server "${serverName}":`, err) @@ -299,11 +302,14 @@ export class MCPChannel implements IServerChannel { } private async _callCloseServer(serverName: string) { - if (this.clients[serverName]) { - const { client } = this.clients[serverName] + const server = this.clients[serverName] + if (server) { + const { client } = server if (client) { await client.close() } + // Remove the client from the clients object + delete this.clients[serverName].client console.log(`Closed MCP server ${serverName}`); } } From ae0068ecb3d59b76fa15e66aa9163f5021d32731 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Mon, 19 May 2025 15:01:56 -0400 Subject: [PATCH 39/98] Added callTool function to handle server tool calls --- .../contrib/void/electron-main/mcpChannel.ts | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index c1474b8d..8dcc6f7d 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -17,6 +17,7 @@ import { MCPConfig, MCPServerConfig, MCPServerErrorModel, MCPAddResponse, MCPSer import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import { equals } from '../../../../base/common/objects.js'; import { MCPServerStates } from '../common/voidSettingsTypes.js'; +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; export class MCPChannel implements IServerChannel { @@ -64,6 +65,8 @@ export class MCPChannel implements IServerChannel { else if (event === 'onDelete_server') return this.mcpEmitters.serverEvent.delete.event; else if (event === 'onLoading_server') return this.mcpEmitters.serverEvent.loading.event; + // tool call events + // handle unknown events else throw new Error(`Event not found: ${event}`); } @@ -81,7 +84,7 @@ export class MCPChannel implements IServerChannel { await this._handleToggleServer(params.serverName, params.isOn) } else if (command === 'callTool') { - // TODO: HANDLE THIS + await this._safeCallTool(params.serverName, params.toolName, params.params) } else { throw new Error(`Void sendLLM: command "${command}" not recognized.`) @@ -92,7 +95,7 @@ export class MCPChannel implements IServerChannel { } } - // call functions + // server functions private async _callSetupServers(params: { mcpConfig: MCPConfig, serverStates: MCPServerStates }) { @@ -262,7 +265,8 @@ export class MCPChannel implements IServerChannel { return formattedServer; } - // Helper function to safely setup a server + // Error wrapper around _callSetupServer + // to handle errors and return a formatted error object private async _safeSetupServer(serverConfig: MCPServerConfig, serverName: string, isOn = true) { try { return await this._callSetupServer(serverConfig, serverName, isOn) @@ -358,7 +362,58 @@ export class MCPChannel implements IServerChannel { } } - // Util functions + // tool call functions + + private async _callTool(serverName: string, toolName: string, params: any) { + const server = this.clients[serverName] + if (!server) throw new Error(`Server ${serverName} not found`) + const { client } = server + if (!client) throw new Error(`Client for server ${serverName} not found`) + + // Call the tool with the provided parameters + const response = await client.callTool({ + name: toolName, + arguments: params + }) + const { content } = response as CallToolResult + const returnValue = content[0] + + if (returnValue.type === 'text') { + // handle text response + + if (response.isError) { + throw new Error(`Tool call error: ${response.content}`) + // handle error + } + } + + if (returnValue.type === 'audio') { + // handle audio response + } + + if (returnValue.type === 'image') { + // handle image response + } + + if (returnValue.type === 'resource') { + // handle resource response + } + } + + // tool call error wrapper + private async _safeCallTool(serverName: string, toolName: string, params: any) { + try { + const response = await this._callTool(serverName, toolName, params) + return response + } + catch (err) { + const typedErr = err as Error + console.error(`❌ Failed to call tool "${toolName}" on server "${serverName}":`, err) + return typedErr.message + } + } + + // util functions private _getLoadingServerObject(serverName: string, isOn = true): MCPServerEventLoadingParam { return { From b70ee20b881f9dfd2582b1c9b2c18042af67a3d2 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Mon, 19 May 2025 16:00:07 -0400 Subject: [PATCH 40/98] Added an error to handle unsupported response types --- .../contrib/void/electron-main/mcpChannel.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 8dcc6f7d..1c1c374f 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -387,17 +387,19 @@ export class MCPChannel implements IServerChannel { } } - if (returnValue.type === 'audio') { - // handle audio response - } + // if (returnValue.type === 'audio') { + // // handle audio response + // } - if (returnValue.type === 'image') { - // handle image response - } + // if (returnValue.type === 'image') { + // // handle image response + // } - if (returnValue.type === 'resource') { - // handle resource response - } + // if (returnValue.type === 'resource') { + // // handle resource response + // } + + throw new Error(`Tool call error: We don\'t support ${returnValue.type} tool response yet for server ${serverName} and tool ${toolName}`) } // tool call error wrapper From 2b97326eff378731012d6e67735a15f61eba1bea Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Mon, 19 May 2025 16:00:30 -0400 Subject: [PATCH 41/98] Added types to MCP tool call response --- .../contrib/void/common/mcpServiceTypes.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index fea0d34b..0619170a 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -221,4 +221,53 @@ export type MCPServerEventLoadingParam = { response: MCPLoadingResponse }; // Event Param union type export type MCPServerEventParam = MCPServerEventAddParam | MCPServerEventUpdateParam | MCPServerEventDeleteParam | MCPServerEventLoadingParam; +// TOOL CALL EVENT TYPES ------------------------------------------ +type MCPToolResponseType = 'text' | 'image' | 'audio' | 'resource' | 'error'; + +type ResponseImageTypes = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp' | 'image/svg+xml' | 'image/bmp' | 'image/tiff' | 'image/vnd.microsoft.icon'; + +interface ImageData { + data: string; + mimeType: ResponseImageTypes; +} + +interface MCPToolResponseBase { + toolName: string; + event: MCPToolResponseType; + text?: string; + image?: ImageData; +} + +type MCPToolResponseConstraints = { + 'text': { + image?: never; + text: string; + }; + 'error': { + image?: never; + text: string; + }; + 'image': { + text?: never; + image: ImageData; + }; + 'audio': { + text?: never; + image?: never; + }; + 'resource': { + text?: never; + image?: never; + } +} + +type MCPToolEventResponse = Omit & MCPToolResponseConstraints[T] & { event: T }; + +// Response types +export type MCPToolTextResponse = MCPToolEventResponse<'text'>; +export type MCPToolErrorResponse = MCPToolEventResponse<'error'>; +export type MCPToolImageResponse = MCPToolEventResponse<'image'>; +export type MCPToolAudioResponse = MCPToolEventResponse<'audio'>; +export type MCPToolResourceResponse = MCPToolEventResponse<'resource'>; +export type MCPGenericToolResponse = MCPToolTextResponse | MCPToolErrorResponse | MCPToolImageResponse | MCPToolAudioResponse | MCPToolResourceResponse; From f1c9e29f61855788328db657c23fb4237600f619 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Mon, 19 May 2025 16:16:48 -0400 Subject: [PATCH 42/98] Updated types and hooked up return value to callTool and safeCallTool functions --- .../contrib/void/common/mcpServiceTypes.ts | 1 + .../contrib/void/electron-main/mcpChannel.ts | 34 +++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index 0619170a..071a088a 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -234,6 +234,7 @@ interface ImageData { interface MCPToolResponseBase { toolName: string; + serverName?: string; event: MCPToolResponseType; text?: string; image?: ImageData; diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 1c1c374f..76a42b83 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -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 { MCPConfig, MCPServerConfig, MCPServerErrorModel, MCPAddResponse, MCPServerEventAddParam, MCPServerEventUpdateParam, MCPServerEventDeleteParam, MCPUpdateResponse, MCPServerModel, MCPDeleteResponse, MCPServerEventLoadingParam } from '../common/mcpServiceTypes.js'; +import { MCPConfig, MCPServerConfig, MCPServerErrorModel, MCPAddResponse, MCPServerEventAddParam, MCPServerEventUpdateParam, MCPServerEventDeleteParam, MCPUpdateResponse, MCPServerModel, MCPDeleteResponse, MCPServerEventLoadingParam, MCPGenericToolResponse, MCPToolErrorResponse } 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'; @@ -84,7 +84,9 @@ export class MCPChannel implements IServerChannel { await this._handleToggleServer(params.serverName, params.isOn) } else if (command === 'callTool') { - await this._safeCallTool(params.serverName, params.toolName, params.params) + const response = await this._safeCallTool(params.serverName, params.toolName, params.params) + // return tool call value directly + return response } else { throw new Error(`Void sendLLM: command "${command}" not recognized.`) @@ -364,7 +366,7 @@ export class MCPChannel implements IServerChannel { // tool call functions - private async _callTool(serverName: string, toolName: string, params: any) { + private async _callTool(serverName: string, toolName: string, params: any): Promise { const server = this.clients[serverName] if (!server) throw new Error(`Server ${serverName} not found`) const { client } = server @@ -385,6 +387,14 @@ export class MCPChannel implements IServerChannel { throw new Error(`Tool call error: ${response.content}`) // handle error } + + // handle success + return { + event: 'text', + text: returnValue.text, + toolName, + serverName, + } } // if (returnValue.type === 'audio') { @@ -399,19 +409,23 @@ export class MCPChannel implements IServerChannel { // // handle resource response // } - throw new Error(`Tool call error: We don\'t support ${returnValue.type} tool response yet for server ${serverName} and tool ${toolName}`) + throw new Error(`Tool call error: We don\'t support ${returnValue.type} tool response yet for tool ${toolName} on server ${serverName}`) } // tool call error wrapper - private async _safeCallTool(serverName: string, toolName: string, params: any) { + private async _safeCallTool(serverName: string, toolName: string, params: any): Promise { try { const response = await this._callTool(serverName, toolName, params) return response - } - catch (err) { - const typedErr = err as Error - console.error(`❌ Failed to call tool "${toolName}" on server "${serverName}":`, err) - return typedErr.message + } catch (err) { + const fullErrorMessage = `❌ Failed to call tool "${toolName}" on server "${serverName}": ${typeof err === 'string' ? err : JSON.stringify(err, null, 2)}` + const errorResponse: MCPToolErrorResponse = { + event: 'error', + text: fullErrorMessage, + toolName, + serverName, + } + return errorResponse } } From 6646bd0993f5f48b46d2bbcfe1d4aba10b878026 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Mon, 19 May 2025 16:52:43 -0400 Subject: [PATCH 43/98] Added function in mcpService to handle calling an MCP server tool --- src/vs/workbench/contrib/void/common/mcpService.ts | 7 ++++++- src/vs/workbench/contrib/void/common/mcpServiceTypes.ts | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index e5690521..e33ffce6 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -15,7 +15,7 @@ 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 { MCPServers, MCPConfig, MCPServerEventParam, MCPServerEventAddParam, MCPServerEventUpdateParam, MCPServerEventDeleteParam, MCPServerEventLoadingParam, MCPConfigParseError, MCPGenericToolResponse, MCPToolCallParams } from './mcpServiceTypes.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { InternalToolInfo } from './prompt/prompts.js'; import { IVoidSettingsService } from './voidSettingsService.js'; @@ -348,6 +348,11 @@ class MCPService extends Disposable implements IMCPService { return updatedServers; } + + public async callMCPTool(toolData: MCPToolCallParams): Promise { + const response = await this.channel.call('callTool', toolData); + return response; + } } registerSingleton(IMCPService, MCPService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index 071a088a..edf10765 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -272,3 +272,9 @@ export type MCPToolImageResponse = MCPToolEventResponse<'image'>; export type MCPToolAudioResponse = MCPToolEventResponse<'audio'>; export type MCPToolResourceResponse = MCPToolEventResponse<'resource'>; export type MCPGenericToolResponse = MCPToolTextResponse | MCPToolErrorResponse | MCPToolImageResponse | MCPToolAudioResponse | MCPToolResourceResponse; + +export interface MCPToolCallParams { + serverName: string; + toolName: string; + params: Record; +} From 5995591296d7464afa2c041fe5ae873f16ac346a Mon Sep 17 00:00:00 2001 From: vrtnis <123119434+vrtnis@users.noreply.github.com> Date: Tue, 20 May 2025 01:36:16 +0000 Subject: [PATCH 44/98] feat(void): add Bedrock provider in onboarding + type updates --- .../src/void-onboarding/VoidOnboarding.tsx | 2 +- .../contrib/void/common/modelCapabilities.ts | 20 +++++++++++++ .../contrib/void/common/voidSettingsTypes.ts | 20 +++++++++++-- .../llmMessage/sendLLMMessage.impl.ts | 29 +++++++++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index d16ebefd..9ac3645c 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -100,7 +100,7 @@ const tabNames = ['Free', 'Paid', 'Local'] as const; type TabName = typeof tabNames[number] | 'Cloud/Other'; // Data for cloud providers tab -const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'openAICompatible']; +const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'awsBedrock', 'openAICompatible']; // Data structures for provider tabs const providerNamesOfTab: Record = { diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 49b38b84..a1785022 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -60,6 +60,12 @@ export const defaultProviderSettings = { apiKey: '', azureApiVersion: '2024-05-01-preview', }, + awsBedrock: { + apiKey: '', + region: 'us-east-1', // add region setting + endpoint: '', // optionally allow overriding default + }, + } as const @@ -136,6 +142,7 @@ export const defaultModelsOfProvider = { openAICompatible: [], // fallback googleVertex: [], microsoftAzure: [], + awsBedrock: [], liteLLM: [], @@ -1028,6 +1035,18 @@ const microsoftAzureSettings: VoidStaticProviderInfo = { }, } +// ---------------- AWS BEDROCK ---------------- +const awsBedrockModelOptions = { +} as const satisfies Record + +const awsBedrockSettings: VoidStaticProviderInfo = { + modelOptions: awsBedrockModelOptions, + modelOptionsFallback: (modelName) => { return null }, + providerReasoningIOSettings: { + input: { includeInPayload: openAICompatIncludeInPayloadReasoning }, + }, +} + // ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ---------------- const ollamaModelOptions = { @@ -1333,6 +1352,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi googleVertex: googleVertexSettings, microsoftAzure: microsoftAzureSettings, + awsBedrock: awsBedrockSettings, } as const diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index a911dfe6..55c4f81e 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -103,6 +103,9 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn else if (providerName === 'microsoftAzure') { return { title: 'Microsoft Azure OpenAI', } } + else if (providerName === 'awsBedrock') { + return { title: 'AWS Bedrock', } + } throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`) } @@ -120,6 +123,7 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => { if (providerName === 'openAICompatible') return `Use any provider that's OpenAI-compatible (use this for llama.cpp and more).` if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).' if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).' + if (providerName === 'awsBedrock') return 'Connect via a LiteLLM proxy or the AWS [Bedrock-Access-Gateway](https://github.com/aws-samples/bedrock-access-gateway). LiteLLM Bedrock setup docs are [here](https://docs.litellm.ai/docs/providers/bedrock).' if (providerName === 'ollama') return 'Read more about custom [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' if (providerName === 'vLLM') return 'Read more about custom [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).' if (providerName === 'lmStudio') return 'Read more about custom [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).' @@ -165,14 +169,16 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'googleVertex' ? 'baseURL' : providerName === 'microsoftAzure' ? 'baseURL' : providerName === 'liteLLM' ? 'baseURL' : - '(never)', + providerName === 'awsBedrock' ? 'Endpoint' : + '(never)', placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint : providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint : providerName === 'openAICompatible' ? 'https://my-website.com/v1' : providerName === 'lmStudio' ? defaultProviderSettings.lmStudio.endpoint : providerName === 'liteLLM' ? 'http://localhost:4000' - : '(never)', + : providerName === 'awsBedrock' ? 'http://localhost:4000/v1' + : '(never)', } @@ -185,7 +191,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName return { title: 'Region', placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region - : '' + : providerName === 'awsBedrock' + ? defaultProviderSettings.awsBedrock.region + : '' } } else if (settingName === 'azureApiVersion') { @@ -340,6 +348,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.microsoftAzure), _didFillInProviderSettings: undefined, }, + awsBedrock: { // aggregator (serves models from multiple providers) + ...defaultCustomSettings, + ...defaultProviderSettings.awsBedrock, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.awsBedrock), + _didFillInProviderSettings: undefined, + }, } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 1190b283..89fb5a87 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -121,6 +121,29 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ const options = { endpoint, apiKey: thisConfig.apiKey, apiVersion }; return new AzureOpenAI({ ...options, ...commonPayloadOpts }); } + else if (providerName === 'awsBedrock') { + /** + * We treat Bedrock as *OpenAI-compatible only through a proxy*: + * • LiteLLM default → http://localhost:4000/v1 + * • Bedrock-Access-Gateway → https://.execute-api..amazonaws.com/openai/ + * + * The native Bedrock runtime endpoint + * https://bedrock-runtime..amazonaws.com + * is **NOT** OpenAI-compatible, so we do *not* fall back to it here. + */ + const { endpoint, apiKey } = settingsOfProvider.awsBedrock + + // ① use the user-supplied proxy if present + // ② otherwise default to local LiteLLM + let baseURL = endpoint || 'http://localhost:4000/v1' + + // Normalize: make sure we end with “/v1” + if (!baseURL.endsWith('/v1')) + baseURL = baseURL.replace(/\/+$/, '') + '/v1' + + return new OpenAI({ baseURL, apiKey, ...commonPayloadOpts }) + } + else if (providerName === 'deepseek') { const thisConfig = settingsOfProvider[providerName] @@ -907,6 +930,12 @@ export const sendLLMMessageToProviderImplementation = { sendFIM: null, list: null, }, + awsBedrock: { + sendChat: (params) => _sendOpenAICompatibleChat(params), + sendFIM: null, + list: null, + }, + } satisfies CallFnOfProvider From 07df20b4de77f33fc302c07b359e57c9e560a9ba Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Mon, 19 May 2025 22:37:26 -0400 Subject: [PATCH 45/98] Added mcpServerName to InternalToolInfo --- src/vs/workbench/contrib/void/common/prompt/prompts.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 37f16c84..2115728e 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -147,6 +147,8 @@ export type InternalToolInfo = { params: { [paramName: string]: { description: string } }, + // Only if the tool is from an MCP server + mcpServerName?: string, } From 9b4c0b8d5a6ed585850d9440747da35ba9ba4f16 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Mon, 19 May 2025 22:38:56 -0400 Subject: [PATCH 46/98] Added functions that return properly formatted CallTool functions and ToolResultToStr functions from MCP tools, then hooked it up --- .../contrib/void/browser/toolsService.ts | 20 ++++-- .../contrib/void/common/mcpService.ts | 65 ++++++++++++++++++- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 02edf047..1bf6c464 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -19,6 +19,7 @@ import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js' import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js' import { IVoidSettingsService } from '../common/voidSettingsService.js' import { generateUuid } from '../../../../base/common/uuid.js' +import { IMCPService, MCPCallTool, MCPToolResultToString } from '../common/mcpService.js' // tool use for AI @@ -30,6 +31,10 @@ type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T] | Promise, interruptTool?: () => void }> } type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited) => string } +// Interfaces that accept both internal tools and MCP tools +export type ToolHandler = CallTool & MCPCallTool; +export type ToolResultToStringHandler = ToolResultToString & MCPToolResultToString + const isFalsy = (u: unknown) => { @@ -111,8 +116,8 @@ const checkIfIsFolder = (uriStr: string) => { export interface IToolsService { readonly _serviceBrand: undefined; validateParams: ValidateParams; - callTool: CallTool; - stringOfResult: ToolResultToString; + callTool: ToolHandler; + stringOfResult: ToolResultToStringHandler; } export const IToolsService = createDecorator('ToolsService'); @@ -122,8 +127,8 @@ export class ToolsService implements IToolsService { readonly _serviceBrand: undefined; public validateParams: ValidateParams; - public callTool: CallTool; - public stringOfResult: ToolResultToString; + public callTool: ToolHandler; + public stringOfResult: ToolResultToStringHandler; constructor( @IFileService fileService: IFileService, @@ -137,6 +142,7 @@ export class ToolsService implements IToolsService { @IDirectoryStrService private readonly directoryStrService: IDirectoryStrService, @IMarkerService private readonly markerService: IMarkerService, @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, + @IMCPService private readonly mcpService: IMCPService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); @@ -446,7 +452,8 @@ export class ToolsService implements IToolsService { await this.terminalToolService.killPersistentTerminal(persistentTerminalId) return { result: {} } }, - + // Returns MCP server call tool functions + ...this.mcpService.getMCPToolFns().callTool, } @@ -550,7 +557,8 @@ export class ToolsService implements IToolsService { kill_persistent_terminal: (params, _result) => { return `Successfully closed terminal "${params.persistentTerminalId}".`; }, - + // All MCP server call tool functions + ...this.mcpService.getMCPToolFns().resultToString, } diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index e33ffce6..51f5a966 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -32,10 +32,25 @@ export interface IMCPService { onDidDeleteServer: Event; onLoadingServers: Event; onConfigParsingError: Event; + getMCPToolFns(): { + callTool: MCPCallTool; + resultToString: MCPToolResultToString + }; } export const IMCPService = createDecorator('mcpConfigService'); +export interface MCPCallTool { + [toolName: string]: (params: any) => Promise<{ + result: any | Promise, + interruptTool?: () => void + }>; +} + +export interface MCPToolResultToString { + [toolName: string]: (params: any, result: any) => string; +} + class MCPService extends Disposable implements IMCPService { _serviceBrand: undefined; @@ -266,8 +281,9 @@ class MCPService extends Disposable implements IMCPService { } public getAllToolsFormatted(): InternalToolInfo[] { - const allTools = Object.values(this.mcpServers).flatMap(server => { - return server.tools.map(tool => { + const allTools = Object.keys(this.mcpServers).flatMap(serverName => { + const server = this.mcpServers[serverName]; + return server.tools.map((tool) => { // Convert JsonSchema to the expected format const convertedParams: { [paramName: string]: { description: string } } = {}; @@ -284,6 +300,7 @@ class MCPService extends Disposable implements IMCPService { description: tool.description || '', params: convertedParams, name: tool.name, + serverName, }; }); }); @@ -353,6 +370,50 @@ class MCPService extends Disposable implements IMCPService { const response = await this.channel.call('callTool', toolData); return response; } + + public getMCPToolFns(): { callTool: MCPCallTool; resultToString: MCPToolResultToString } { + const tools = this.getAllToolsFormatted(); + const toolFns: MCPCallTool = {}; + const toolResultToStringFns: MCPToolResultToString = {}; + + tools.forEach((tool) => { + const name = tool.name; + const serverName = tool.mcpServerName; + + // Define the tool call function + const toolFn = async (params: { + serverName: string, + toolName: string, + args: any + }) => { + const { serverName, toolName, args } = params; + const response = await this.callMCPTool({ + serverName, + toolName, + params: args, + }); + return { + result: response, + }; + }; + + // Define the result-to-string function + const resultToStringFn = (params: any, result: MCPGenericToolResponse): string => { + if (result.event === 'error' || result.event === 'text') { + return result.text; + } + throw new Error(`MCP Server ${serverName} and Tool ${name} returned an unexpected result: ${JSON.stringify(result)}`); + }; + + toolFns[name] = toolFn; + toolResultToStringFns[name] = resultToStringFn; + }); + + return { + callTool: toolFns, + resultToString: toolResultToStringFns + }; + } } registerSingleton(IMCPService, MCPService, InstantiationType.Eager); From 35f23261b0d7fe553abfd122903bbb3d95658095 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 20 May 2025 12:44:29 -0400 Subject: [PATCH 47/98] Fixed the string definition for the mcp resultToString fns --- src/vs/workbench/contrib/void/browser/toolsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 1bf6c464..c0b0e9ca 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -557,7 +557,7 @@ export class ToolsService implements IToolsService { kill_persistent_terminal: (params, _result) => { return `Successfully closed terminal "${params.persistentTerminalId}".`; }, - // All MCP server call tool functions + // All MCP server result to string functions ...this.mcpService.getMCPToolFns().resultToString, } From 03ab6559fc13153f5a7a8a23f7565450491e38d5 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 20 May 2025 23:09:23 -0700 Subject: [PATCH 48/98] misc --- .../src/void-settings-tsx/MCPServersList.tsx | 22 ++++++++----------- .../react/src/void-settings-tsx/Settings.tsx | 5 ++--- 2 files changed, 11 insertions(+), 16 deletions(-) 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 eab9e876..9457d5f2 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 @@ -3,7 +3,6 @@ import { MCPConfigParseError, MCPServerEventParam, MCPServerObject, MCPServers } import { useEffect, useState } from 'react'; import { useAccessor } from '../util/services.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; -import { off } from 'process'; export interface Tool { /** Unique tool identifier */ @@ -42,21 +41,18 @@ const MCPServer = ({ name, server }: MCPServerProps) => { mcpService.toggleServer(name, e); } - // Needs to be raw CSS because - // Tailwind won't compile the colors - // dynamically - const serverStatusColorStyles = { - success: { backgroundColor: '#10B981' }, // green-500 equivalent - error: { backgroundColor: '#EF4444' }, // red-500 equivalent - loading: { backgroundColor: '#F59E0B' }, // yellow-500 equivalent - offline: { backgroundColor: '#6B7280' } // gray-500 equivalent - }; - - return ( +return (
{/* Status indicator */} -
+
{/* Server name */}
{name}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 8567df65..a9e52430 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -1255,9 +1255,8 @@ Alternatively, place a \`.voidrules\` file in the root of your workspace.

+Use Model Context Protocol to provide Agent mode with more tools. + `} chatMessageLocation={undefined} />

From 6d9027f63b887b51eb0e6386bbe903bf03755ede Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 20 May 2025 23:18:46 -0700 Subject: [PATCH 49/98] misc --- .../src/void-settings-tsx/MCPServersList.tsx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) 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 9457d5f2..560282f2 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 @@ -4,17 +4,6 @@ import { useEffect, useState } from 'react'; import { useAccessor } from '../util/services.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; -export interface Tool { - /** Unique tool identifier */ - name: string; - /** Human‑readable description */ - description?: string; - /** JSON schema describing expected arguments */ - inputSchema?: any; - /** Free‑form annotations describing behaviour, security, etc. */ - annotations?: Record; -} - // Command display component const CommandDisplay = ({ command }: {command: string}) => { return ( @@ -25,13 +14,8 @@ const CommandDisplay = ({ command }: {command: string}) => { }; -interface MCPServerProps { - name: string; - server: MCPServerObject; -} - // MCP Server component -const MCPServer = ({ name, server }: MCPServerProps) => { +const MCPServer = ({ name, server }: {name: string, server: MCPServerObject}) => { const accessor = useAccessor(); const mcpService = accessor.get('IMCPService'); From d80fd766f8bef46e620ff3d5b22728d001e85c41 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 20 May 2025 23:18:52 -0700 Subject: [PATCH 50/98] autoformat --- .../src/void-settings-tsx/MCPServersList.tsx | 146 +++++++++--------- 1 file changed, 73 insertions(+), 73 deletions(-) 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 560282f2..52260f4e 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 @@ -5,17 +5,17 @@ import { useAccessor } from '../util/services.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; // Command display component -const CommandDisplay = ({ command }: {command: string}) => { - return ( -
- {command} -
- ); +const CommandDisplay = ({ command }: { command: string }) => { + return ( +
+ {command} +
+ ); }; // MCP Server component -const MCPServer = ({ name, server }: {name: string, server: MCPServerObject}) => { +const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) => { const accessor = useAccessor(); const mcpService = accessor.get('IMCPService'); @@ -25,72 +25,72 @@ const MCPServer = ({ name, server }: {name: string, server: MCPServerObject}) => mcpService.toggleServer(name, e); } -return ( + return (
-
- {/* Status indicator */} -
+ {/* Status indicator */} +
- {/* Server name */} -
{name}
+ {/* Server name */} +
{name}
- {/* Power toggle switch */} -
- + {/* Power toggle switch */} +
+ +
-
- {/* Tools section */} -
-
- {server.isOn && server.tools.length > 0 ? ( - server.tools.map((tool) => ( - - {tool.name} - - )) - ) : ( - No tools available + {/* Tools section */} +
+
+ {server.isOn && server.tools.length > 0 ? ( + server.tools.map((tool) => ( + + {tool.name} + + )) + ) : ( + No tools available + )} +
+
+ + {/* Command display */} + {server.isOn && server.command && ( +
+
Command:
+ +
)} -
-
- {/* Command display */} - {server.isOn && server.command && ( -
-
Command:
- -
- )} - - {/* Error message if present */} - {server.error && ( -
- - - - - - {server.error} + {/* Error message if present */} + {server.error && ( +
+ + + + + + {server.error} +
+ )}
- )} -
- ); + ); }; // Main component that renders the list of servers @@ -159,18 +159,18 @@ const MCPServersList = () => { return (
-
- {!mcpConfigError && Object.entries(mcpServers).map(([name, server]) => ( -
- +
+ {!mcpConfigError && Object.entries(mcpServers).map(([name, server]) => ( +
+ +
+ ))} + {mcpConfigError && ( +
+ {mcpConfigError} +
+ )}
- ))} - {mcpConfigError && ( -
- {mcpConfigError} -
- )} -
); }; From 4d8363026e87b83c0eab335e23fb14315ba664c3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 00:56:14 -0700 Subject: [PATCH 51/98] progress --- .../src/void-settings-tsx/MCPServersList.tsx | 18 +- .../react/src/void-settings-tsx/Settings.tsx | 2 +- .../contrib/void/common/mcpService.ts | 153 +++++++------ .../contrib/void/common/mcpServiceTypes.ts | 204 +++++++++--------- .../void/common/voidSettingsService.ts | 61 +++--- .../contrib/void/common/voidSettingsTypes.ts | 2 +- .../contrib/void/electron-main/mcpChannel.ts | 195 +++++++++-------- 7 files changed, 315 insertions(+), 320 deletions(-) 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 52260f4e..4bc78e6a 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,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({}); + const [mcpServers, setMCPServers] = useState({}); const [mcpConfigError, setMCPConfigError] = useState(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 => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index a9e52430..d5174cbe 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -1249,7 +1249,7 @@ Alternatively, place a \`.voidrules\` file in the root of your workspace.

MCP

- { await mcpService.openMCPConfigFile() }}> + { await mcpService.revealMCPConfigFile() }}> Add MCP Server
diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index e5690521..dd628e1a 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -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; - getMCPServers(): MCPServers; - getAllToolsFormatted(): InternalToolInfo[]; + revealMCPConfigFile(): Promise; + getMCPServerOfName(): MCPServerOfName; + + getCurrentTools(): InternalToolInfo[]; + toggleServer(serverName: string, isOn: boolean): Promise; - onDidAddServer: Event; - onDidUpdateServer: Event; - onDidDeleteServer: Event; - onLoadingServers: Event; - onConfigParsingError: Event; + + onDidAddServer: Event; + onDidUpdateServer: Event; + onDidDeleteServer: Event; + + // onLoadingServers: Event; + onConfigParsingError: Event; } export const IMCPService = createDecorator('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(); - private readonly _onDidUpdateServer = new Emitter(); - private readonly _onDidDeleteServer = new Emitter(); - private readonly _onLoadingServers = new Emitter(); - private readonly _onConfigParsingError = new Emitter(); + 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; - public readonly onLoadingServers = this._onLoadingServers.event; + + // private readonly _onLoadingServers = new Emitter(); + // public readonly onLoadingServers = this._onLoadingServers.event; + + private readonly _onConfigParsingError = new Emitter(); 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)(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('onLoading_server') satisfies Event)(e => this._onServerEvent(e))); + 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('onLoading_server') satisfies Event)(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 { - - // 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 { - // 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 { + // 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 { - 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 { + public async revealMCPConfigFile(): Promise { 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 { 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 { + 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); return URI.file(mcpConfigPath); @@ -319,9 +314,9 @@ class MCPService extends Disposable implements IMCPService { } // Handle server state changes - private async _handleServerStateChange(mcpConfig: MCPConfig): Promise { + private async _handleServerStateChange(mcpConfig: MCPConfigFileType): Promise { // 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; } diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index fea0d34b..d7e0cfac 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -23,103 +23,100 @@ /* Core JSON‑RPC envelope */ /* -------------------------------------------------- */ -export interface JsonRpcSuccess { - /** 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 { +// /** 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; - -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; /** Free‑form annotations describing behaviour, security, etc. */ annotations?: Record; } -export interface ToolsListResult extends Paginated { - tools: Tool[]; -} +// export interface ToolsListResult extends Paginated { +// tools: MCPTool[]; +// } -export type ToolsListResponse = JsonRpcSuccess; +// export type ToolsListResponse = JsonRpcSuccess; /* -------------------------------------------------- */ /* 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; +// export type PromptsListResponse = JsonRpcSuccess; /* -------------------------------------------------- */ /* 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; +// export type ToolCallResponse = JsonRpcSuccess; // 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; } -export interface MCPConfig { - mcpServers: Record; +export interface MCPConfigFileType { + mcpServers: Record; } -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 & { error: string }; -export type MCPServerSetupParams = { +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 = Parameters['onSuccess']>[0] -export type EventMCPServerSetupOnError = Parameters['onError']>[0] - -type MCPServerEventType = 'add' | 'update' | 'delete' | 'loading'; +export type EventMCPServerSetupOnSuccess = Parameters[0] +export type EventMCPServerSetupOnError = Parameters[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 = Omit & 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; diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index dd1b1f36..fe500d3f 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -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; + // setMCPServerStates: (newStates: MCPServerStates) => Promise; // setting to undefined CLEARS it, unlike others: setOverridesOfModel(providerName: ProviderName, modelName: string, overrides: Partial | undefined): Promise; @@ -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; + + addMCPServers(newMCPStates: MCPServerStateOfName): Promise; removeMCPServers(serverNames: string[]): Promise; - updateMCPServerState(serverName: string, newIsOn: boolean): Promise; + setMCPServerState(serverName: string, newIsOn: boolean): Promise; } @@ -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 }); } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index f6195133..6faa40b2 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -494,7 +494,7 @@ export const defaultOverridesOfModel = overridesOfModel -export interface MCPServerStates { +export interface MCPServerStateOfName { [serverName: string]: MCPServerState; } diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index c1474b8d..1984f564 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -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(), - update: new Emitter(), - delete: new Emitter(), - loading: new Emitter(), + onAdd: new Emitter(), + onUpdate: new Emitter(), + onDelete: new Emitter(), + // onChangeLoading: new Emitter(), } // toolCall: { // success: new Emitter(), // error: new Emitter(), // }, } satisfies { - [event in 'serverEvent']: { - add: Emitter, - update: Emitter, - delete: Emitter, - loading: Emitter, + serverEvent: { + onAdd: Emitter, + onUpdate: Emitter, + onDelete: Emitter, + // onChangeLoading: Emitter, } } @@ -59,10 +78,10 @@ export class MCPChannel implements IServerChannel { listen(_: unknown, event: string): Event { // 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 { 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[] = 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[] = 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[] = 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: '', - } - } - } - } } + From 13654389c6fc6157c5104841aa342a38d83817e4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 01:38:15 -0700 Subject: [PATCH 52/98] 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) From 3619a3e6716f51d9a3a1f983aa6a44b257e3c673 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 01:48:37 -0700 Subject: [PATCH 53/98] add loading --- src/vs/workbench/contrib/void/common/mcpService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 45d33569..9bb14da9 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -25,6 +25,7 @@ import { MCPServerStateOfName } from './voidSettingsTypes.js'; type MCPState = { mcpServerOfName: MCPServerOfName, error: string | undefined, + isLoading: boolean, // TODO!!!!!! } export interface IMCPService { @@ -55,7 +56,8 @@ class MCPService extends Disposable implements IMCPService { // list of MCP servers pulled from mcpChannel state: MCPState = { mcpServerOfName: {}, - error: undefined + error: undefined, + isLoading: false, } // Emitters for server events From d141bffae9a6660d0adc1a4515525b46713a728b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 02:02:12 -0700 Subject: [PATCH 54/98] misc --- .../contrib/void/common/mcpService.ts | 25 +++++++++-------- .../void/common/voidSettingsService.ts | 28 +++++++++---------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 9bb14da9..7c40d861 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -101,7 +101,7 @@ class MCPService extends Disposable implements IMCPService { console.log('MCP Config file created:', mcpConfigUri.toString()); } - await this._updateStateWithCurrentConfigFile(); + await this._refreshMCPServers(); // Add a watcher to the MCP config file await this._addMCPConfigFileWatcher(); @@ -179,7 +179,7 @@ class MCPService extends Disposable implements IMCPService { this._register(this.fileService.onDidFilesChange(async e => { if (!e.contains(mcpConfigUri)) return - await this._updateStateWithCurrentConfigFile(); + await this._refreshMCPServers(); })); } @@ -225,10 +225,10 @@ class MCPService extends Disposable implements IMCPService { return allTools; } + // toggle MCP server and update isOn in void settings public async toggleServer(serverName: string, isOn: boolean): Promise { this.channel.call('toggleServer', { serverName, isOn }) - // Update the server state in the local mcpServers list - await this.voidSettingsService.setMCPServerState(serverName, isOn); + await this.voidSettingsService.setMCPServerState(serverName, { isOn }); } // utility functions @@ -250,33 +250,34 @@ class MCPService extends Disposable implements IMCPService { } // Handle server state changes - private async _updateStateWithCurrentConfigFile(): Promise { + private async _refreshMCPServers(): Promise { this._setHasError(undefined) + // TODO!!! set is loading 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 currMCPStateOfName = this.voidSettingsService.state.mcpServerStateOfName; const availableServers = Object.keys(mcpConfigFile.mcpServers); // Handle added servers - const addedServers = availableServers.filter(serverName => !savedServerStates[serverName]); + const addedServers = availableServers.filter(serverName => !currMCPStateOfName[serverName].isOn); const addedServersObject = addedServers.reduce((acc, serverName) => { acc[serverName] = { isOn: true }; return acc; }, {} as MCPServerStateOfName); - await this.voidSettingsService.addMCPServers(addedServersObject); + await this.voidSettingsService.addMCPServerStateOfName(addedServersObject); // Handle removed servers - const removedServers = Object.keys(savedServerStates).filter(serverName => availableServers.indexOf(serverName) === -1); - await this.voidSettingsService.removeMCPServers(removedServers); + const removedServers = Object.keys(currMCPStateOfName).filter(serverName => availableServers.indexOf(serverName) === -1); + await this.voidSettingsService.removeMCPServerStateNames(removedServers); // Compile the updated server list as MCPServerStates - const updatedServers = Object.keys(savedServerStates).reduce((acc, serverName) => { + const updatedServers = Object.keys(currMCPStateOfName).reduce((acc, serverName) => { if (availableServers.includes(serverName)) { - acc[serverName] = savedServerStates[serverName]; + acc[serverName] = currMCPStateOfName[serverName]; } return acc; }, {} as MCPServerStateOfName); diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index fe500d3f..2458b1cd 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -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, MCPServerStateOfName } from './voidSettingsTypes.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPServerStateOfName, MCPServerState } from './voidSettingsTypes.js'; // name is the name in the dropdown @@ -76,9 +76,9 @@ export interface IVoidSettingsService { addModel(providerName: ProviderName, modelName: string): void; deleteModel(providerName: ProviderName, modelName: string): boolean; - addMCPServers(newMCPStates: MCPServerStateOfName): Promise; - removeMCPServers(serverNames: string[]): Promise; - setMCPServerState(serverName: string, newIsOn: boolean): Promise; + addMCPServerStateOfName(serverStateOfName: MCPServerStateOfName): Promise; + removeMCPServerStateNames(serverNames: string[]): Promise; + setMCPServerState(serverName: string, state: MCPServerState): Promise; } @@ -541,7 +541,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } // MCP Server State - private _setMCPServerStates = async (newStates: MCPServerStateOfName) => { + private _setMCPServerStateOfName = async (newStates: MCPServerStateOfName) => { const newState: VoidSettingsState = { ...this.state, mcpServerStateOfName: { @@ -555,17 +555,17 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { this._metricsService.capture('Set MCP Server States', { newStates }); } - addMCPServers = async (newMCPStates: MCPServerStateOfName) => { + addMCPServerStateOfName = async (newMCPStates: MCPServerStateOfName) => { const { mcpServerStateOfName: mcpServerStates } = this.state const newMCPServerStates = { ...mcpServerStates, ...newMCPStates, } - await this._setMCPServerStates(newMCPServerStates) + await this._setMCPServerStateOfName(newMCPServerStates) this._metricsService.capture('Add MCP Server', { servers: Object.keys(newMCPStates).join(', ') }); } - removeMCPServers = async (serverNames: string[]) => { + removeMCPServerStateNames = async (serverNames: string[]) => { const { mcpServerStateOfName: mcpServerStates } = this.state const newMCPServerStates = { ...mcpServerStates, @@ -575,21 +575,19 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { delete newMCPServerStates[serverName] } }) - await this._setMCPServerStates(newMCPServerStates) + await this._setMCPServerStateOfName(newMCPServerStates) this._metricsService.capture('Remove MCP Server', { servers: serverNames.join(', ') }); } - setMCPServerState = async (serverName: string, newIsOn: boolean) => { + setMCPServerState = async (serverName: string, state: MCPServerState) => { const { mcpServerStateOfName: mcpServerStates } = this.state if (!(serverName in mcpServerStates)) return // if not in list, do nothing const newMCPServerStates = { ...mcpServerStates, - [serverName]: { - isOn: newIsOn, - }, + [serverName]: state, } - await this._setMCPServerStates(newMCPServerStates) - this._metricsService.capture('Update MCP Server State', { serverName, newIsOn }); + await this._setMCPServerStateOfName(newMCPServerStates) + this._metricsService.capture('Update MCP Server State', { serverName, state }); } } From 55691624ddecc1ddcb2cd315afc3ebdc88b11503 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 19:47:47 -0700 Subject: [PATCH 55/98] rename --- .../browser/react/src/void-settings-tsx/MCPServersList.tsx | 2 +- src/vs/workbench/contrib/void/common/mcpService.ts | 6 +++--- src/vs/workbench/contrib/void/electron-main/mcpChannel.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) 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 95ac299b..f629fb3a 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 @@ -22,7 +22,7 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) const handleChangeEvent = (e: boolean) => { // Handle the change event - mcpService.toggleServer(name, e); + mcpService.toggleMCPServer(name, e); } return ( diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 4343fb96..96beb4c3 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -31,7 +31,7 @@ type MCPState = { export interface IMCPService { readonly _serviceBrand: undefined; revealMCPConfigFile(): Promise; - toggleServer(serverName: string, isOn: boolean): Promise; + toggleMCPServer(serverName: string, isOn: boolean): Promise; getMCPToolFns(): { callTool: MCPCallTool; resultToString: MCPToolResultToString @@ -248,8 +248,8 @@ class MCPService extends Disposable implements IMCPService { } // toggle MCP server and update isOn in void settings - public async toggleServer(serverName: string, isOn: boolean): Promise { - this.channel.call('toggleServer', { serverName, isOn }) + public async toggleMCPServer(serverName: string, isOn: boolean): Promise { + this.channel.call('toggleMCPServer', { serverName, isOn }) await this.voidSettingsService.setMCPServerState(serverName, { isOn }); } diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 79e5b3b1..977ea841 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -100,8 +100,8 @@ export class MCPChannel implements IServerChannel { else if (command === 'closeAllMCPServers') { await this._closeAllMCPServers() } - else if (command === 'toggleServer') { - await this._toggleServer(params.serverName, params.isOn) + else if (command === 'toggleMCPServer') { + await this._toggleMCPServer(params.serverName, params.isOn) } else if (command === 'callTool') { const response = await this._safeCallTool(params.serverName, params.toolName, params.params) @@ -345,7 +345,7 @@ export class MCPChannel implements IServerChannel { } } - private async _toggleServer(serverName: string, isOn: boolean) { + private async _toggleMCPServer(serverName: string, isOn: boolean) { const prevServer = this.clients[serverName]?.formattedServer if (isOn) { // Handle turning on the server From 72143608a180aa86a71e44bf78d6c86890bef43d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 20:57:54 -0700 Subject: [PATCH 56/98] misc fixes --- .../src/void-settings-tsx/MCPServersList.tsx | 52 +++-------------- .../contrib/void/common/mcpService.ts | 56 ++++++++----------- .../contrib/void/common/mcpServiceTypes.ts | 2 +- .../contrib/void/common/voidSettingsTypes.ts | 2 +- 4 files changed, 34 insertions(+), 78 deletions(-) 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 f629fb3a..83835f5b 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,17 +1,7 @@ -import { VoidSwitch } from '../util/inputs.js'; +import { VoidSwitch, VoidButtonBgDarken } from '../util/inputs.js'; import { MCPConfigFileParseErrorResponse, MCPServerEventType, MCPServerEventResponse, MCPServerObject, MCPServerOfName } from '../../../../common/mcpServiceTypes.js'; import { useEffect, useState } from 'react'; import { useAccessor, useMCPServiceState } from '../util/services.js'; -import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; - -// Command display component -const CommandDisplay = ({ command }: { command: string }) => { - return ( -
- {command} -
- ); -}; // MCP Server component @@ -44,7 +34,7 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) {/* Power toggle switch */}
@@ -70,11 +60,13 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject })
- {/* Command display */} + {/* Command badge */} {server.isOn && server.command && (
Command:
- +
+ {server.command} +
)} @@ -97,36 +89,8 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) const MCPServersList = () => { const mcpServiceState = useMCPServiceState() - - // 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'); - // } - + const accessor = useAccessor(); + const mcpService = accessor.get('IMCPService'); return (
diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 96beb4c3..de25163c 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -10,7 +10,6 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { IFileService } from '../../../../platform/files/common/files.js'; import { IPathService } from '../../../services/path/common/pathService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { join } from '../../../../base/common/path.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; @@ -148,10 +147,10 @@ class MCPService extends Disposable implements IMCPService { this._onDidChangeState.fire(); } - private readonly _setHasError = async (hasError: string | undefined) => { + private readonly _setHasError = async (errMsg: string | undefined) => { this.state = { ...this.state, - error: hasError ? `MCP config file not found` : undefined, + error: errMsg, } this._onDidChangeState.fire(); } @@ -165,32 +164,6 @@ class MCPService extends Disposable implements IMCPService { await this.fileService.writeFile(mcpConfigUri, buffer); } - private async _parseMCPConfigFile(): Promise { - // TODO!!!!!!! double check this - // this._onConfigParsingError.fire({ - // response: { - // type: 'config-file-error', - // error: null - // } - // }); - - // Process config file - const mcpConfigUri = await this._getMCPConfigFilePath(); - - try { - const fileContent = await this.fileService.readFile(mcpConfigUri); - const contentString = fileContent.value.toString(); - const configJson = JSON.parse(contentString); - if (!configJson.mcpServers) { - throw new Error('Invalid MCP config file: missing mcpServers property'); - } - return configJson as MCPConfigFileType; - } catch (error) { - const fullError = `Error parsing MCP config file: ${error}`; - this._setHasError(fullError) - return null; - } - } private async _addMCPConfigFileWatcher(): Promise { const mcpConfigUri = await this._getMCPConfigFilePath(); @@ -258,8 +231,8 @@ class MCPService extends Disposable implements IMCPService { private async _getMCPConfigFilePath(): Promise { const appName = this.productService.dataFolderName const userHome = await this.pathService.userHome(); - const mcpConfigPath = join(userHome.path, appName, MCP_CONFIG_FILE_NAME); - return URI.file(mcpConfigPath); + const uri = URI.joinPath(userHome, appName, MCP_CONFIG_FILE_NAME) + return uri } private async _configFileExists(mcpConfigUri: URI): Promise { @@ -271,6 +244,25 @@ class MCPService extends Disposable implements IMCPService { } } + + private async _parseMCPConfigFile(): Promise { + const mcpConfigUri = await this._getMCPConfigFilePath(); + try { + const fileContent = await this.fileService.readFile(mcpConfigUri); + const contentString = fileContent.value.toString(); + const configFileJson = JSON.parse(contentString); + if (!configFileJson.mcpServers) { + throw new Error('Missing mcpServers property'); + } + return configFileJson as MCPConfigFileType; + } catch (error) { + const fullError = `Error parsing MCP config file: ${error}`; + this._setHasError(fullError) + return null; + } + } + + // Handle server state changes private async _refreshMCPServers(): Promise { @@ -285,7 +277,7 @@ class MCPService extends Disposable implements IMCPService { const availableServers = Object.keys(mcpConfigFile.mcpServers); // Handle added servers - const addedServers = availableServers.filter(serverName => !currMCPStateOfName[serverName].isOn); + const addedServers = availableServers.filter(serverName => !currMCPStateOfName[serverName]?.isOn); const addedServersObject = addedServers.reduce((acc, serverName) => { acc[serverName] = { isOn: true }; return acc; diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index 441fdb72..cc568191 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -138,7 +138,7 @@ export interface MCPServerObject { // Command-based server properties tools: MCPTool[], status: 'loading' | 'error' | 'success' | 'offline', - isOn: boolean, + isOn: boolean | undefined, command?: string, error?: string, } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 6faa40b2..e2faf506 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -495,7 +495,7 @@ export const defaultOverridesOfModel = overridesOfModel export interface MCPServerStateOfName { - [serverName: string]: MCPServerState; + [serverName: string]: MCPServerState | undefined; } export interface MCPServerState { From 18d1f2e6b9dc8ff8edda96366a933901d398e529 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 21:24:46 -0700 Subject: [PATCH 57/98] move MCP to settings --- .../src/void-settings-tsx/MCPServersList.tsx | 113 --------------- .../react/src/void-settings-tsx/Settings.tsx | 135 ++++++++++++++++-- 2 files changed, 125 insertions(+), 123 deletions(-) delete mode 100644 src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/MCPServersList.tsx 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 deleted file mode 100644 index 83835f5b..00000000 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/MCPServersList.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { VoidSwitch, VoidButtonBgDarken } from '../util/inputs.js'; -import { MCPConfigFileParseErrorResponse, MCPServerEventType, MCPServerEventResponse, MCPServerObject, MCPServerOfName } from '../../../../common/mcpServiceTypes.js'; -import { useEffect, useState } from 'react'; -import { useAccessor, useMCPServiceState } from '../util/services.js'; - - -// MCP Server component -const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) => { - - const accessor = useAccessor(); - const mcpService = accessor.get('IMCPService'); - - const handleChangeEvent = (e: boolean) => { - // Handle the change event - mcpService.toggleMCPServer(name, e); - } - - return ( -
-
- {/* Status indicator */} -
- - {/* Server name */} -
{name}
- - {/* Power toggle switch */} -
- -
-
- - {/* Tools section */} -
-
- {server.isOn && server.tools.length > 0 ? ( - server.tools.map((tool) => ( - - {tool.name} - - )) - ) : ( - No tools available - )} -
-
- - {/* Command badge */} - {server.isOn && server.command && ( -
-
Command:
-
- {server.command} -
-
- )} - - {/* Error message if present */} - {server.error && ( -
- - - - - - {server.error} -
- )} -
- ); -}; - -// Main component that renders the list of servers -const MCPServersList = () => { - - const mcpServiceState = useMCPServiceState() - const accessor = useAccessor(); - const mcpService = accessor.get('IMCPService'); - - return ( -
-
- {!mcpServiceState.error && Object.entries(mcpServiceState.mcpServerOfName).map(([name, server]) => ( -
- -
- ))} - {mcpServiceState.error && ( -
- {mcpServiceState.error} -
- )} -
-
- ); -}; - -export default MCPServersList; diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index d5174cbe..7dca05c7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -19,7 +19,8 @@ import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsSer import Severity from '../../../../../../../base/common/severity.js' import { getModelCapabilities, modelOverrideKeys, ModelOverrides } from '../../../../common/modelCapabilities.js'; import { TransferEditorType, TransferFilesInfo } from '../../../extensionTransferTypes.js'; -import MCPServersList from './MCPServersList.js'; +import { MCPServerObject } from '../../../../common/mcpServiceTypes.js'; +import { useMCPServiceState } from '../util/services.js'; const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => { @@ -899,6 +900,118 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: // full settings +// MCP Server component +const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) => { + const accessor = useAccessor(); + const mcpService = accessor.get('IMCPService'); + + const handleChangeEvent = (e: boolean) => { + // Handle the change event + mcpService.toggleMCPServer(name, e); + } + + return ( +
+
+ {/* Status indicator */} +
+ + {/* Server name */} +
{name}
+ + {/* Power toggle switch */} +
+ +
+
+ + {/* Tools section */} +
+
+ {server.isOn && server.tools.length > 0 ? ( + server.tools.map((tool: { name: string; description?: string }) => ( + + {tool.name} + + )) + ) : ( + No tools available + )} +
+
+ + {/* Command badge */} + {server.isOn && server.command && ( +
+
Command:
+
+ {server.command} +
+
+ )} + + {/* Error message if present */} + {server.error && ( +
+ + + + + + {server.error} +
+ )} +
+ ); +}; + +// Main component that renders the list of servers +const MCPServersList = () => { + const mcpServiceState = useMCPServiceState() + const accessor = useAccessor(); + const mcpService = accessor.get('IMCPService'); + + + let content: React.ReactNode + if (mcpServiceState.error) { + content =
+ {mcpServiceState.error} +
+ } + else { + const entries = Object.entries(mcpServiceState.mcpServerOfName) + if (entries.length === 0) { + content =
+ No servers found +
+ } + else { + content = entries.map(([name, server]) => ( +
+ +
+ )) + } + } + + return content +}; + export const Settings = () => { const isDark = useIsDark() const accessor = useAccessor() @@ -1235,6 +1348,7 @@ export const Settings = () => {

AI Instructions

+

-
-

MCP

- { await mcpService.revealMCPConfigFile() }}> - Add MCP Server - -
+

MCP

- - - +
+ { await mcpService.revealMCPConfigFile() }}> + Add MCP Server + +
+ + + +

From 8b99b6ec78fe866fc96f225a3fb43bcc55adfec4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 21:33:05 -0700 Subject: [PATCH 58/98] userspecified --- .../react/src/void-settings-tsx/Settings.tsx | 4 ++- .../contrib/void/common/mcpService.ts | 28 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 7dca05c7..641b06da 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -984,7 +984,9 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) const MCPServersList = () => { const mcpServiceState = useMCPServiceState() const accessor = useAccessor(); - const mcpService = accessor.get('IMCPService'); + + const userSpecifiedMCPServerNames = mcpServiceState.userSpecifiedMCPServerNames + // TODO tell the user what servers they've specified (might be different from those found) let content: React.ReactNode diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index de25163c..cdba9da2 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -25,21 +25,22 @@ type MCPState = { mcpServerOfName: MCPServerOfName, error: string | undefined, isLoading: boolean, // TODO!!!!!! + userSpecifiedMCPServerNames: string[], } export interface IMCPService { readonly _serviceBrand: undefined; revealMCPConfigFile(): Promise; toggleMCPServer(serverName: string, isOn: boolean): Promise; + + readonly state: MCPState; // NOT persisted + onDidChangeState: Event; + + getCurrentMCPToolNames(): InternalToolInfo[]; getMCPToolFns(): { callTool: MCPCallTool; resultToString: MCPToolResultToString }; - - readonly state: MCPState; - onDidChangeState: Event; - - getCurrentMCPToolNames(): InternalToolInfo[]; } export const IMCPService = createDecorator('mcpConfigService'); @@ -78,6 +79,7 @@ class MCPService extends Disposable implements IMCPService { mcpServerOfName: {}, error: undefined, isLoading: false, + userSpecifiedMCPServerNames: [], } // Emitters for server events @@ -154,6 +156,20 @@ class MCPService extends Disposable implements IMCPService { } this._onDidChangeState.fire(); } + private readonly _setUserSpecifiedServerNames = async (names: string[]) => { + this.state = { + ...this.state, + userSpecifiedMCPServerNames: names, + } + this._onDidChangeState.fire(); + } + // private readonly _setIsLoading = async (isLoading: boolean) => { + // this.state = { + // ...this.state, + // isLoading: isLoading, + // } + // this._onDidChangeState.fire(); + // } @@ -273,6 +289,8 @@ class MCPService extends Disposable implements IMCPService { 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 } + this._setUserSpecifiedServerNames(Object.keys(mcpConfigFile.mcpServers)) + const currMCPStateOfName = this.voidSettingsService.state.mcpServerStateOfName; const availableServers = Object.keys(mcpConfigFile.mcpServers); From 66575b5381bb3e020ef3897f200aadfffb04e5b3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 21:38:30 -0700 Subject: [PATCH 59/98] add bg colors --- .../void/browser/react/src/void-settings-tsx/Settings.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 641b06da..f901ad7b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -915,10 +915,10 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject })
{/* Status indicator */}
From 446248aa791e845effbd1012bc43b2e15c0fb548 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 22:02:24 -0700 Subject: [PATCH 60/98] loading --- .../contrib/void/common/mcpService.ts | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index cdba9da2..3b73af87 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -24,7 +24,6 @@ import { MCPServerStateOfName } from './voidSettingsTypes.js'; type MCPState = { mcpServerOfName: MCPServerOfName, error: string | undefined, - isLoading: boolean, // TODO!!!!!! userSpecifiedMCPServerNames: string[], } @@ -78,7 +77,6 @@ class MCPService extends Disposable implements IMCPService { state: MCPState = { mcpServerOfName: {}, error: undefined, - isLoading: false, userSpecifiedMCPServerNames: [], } @@ -122,12 +120,8 @@ class MCPService extends Disposable implements IMCPService { await this._createMCPConfigFile(mcpConfigUri); console.log('MCP Config file created:', mcpConfigUri.toString()); } - - await this._refreshMCPServers(); - - // Add a watcher to the MCP config file await this._addMCPConfigFileWatcher(); - + await this._refreshMCPServers(); } catch (error) { console.error('Error initializing MCPService:', error); } @@ -156,22 +150,6 @@ class MCPService extends Disposable implements IMCPService { } this._onDidChangeState.fire(); } - private readonly _setUserSpecifiedServerNames = async (names: string[]) => { - this.state = { - ...this.state, - userSpecifiedMCPServerNames: names, - } - this._onDidChangeState.fire(); - } - // private readonly _setIsLoading = async (isLoading: boolean) => { - // this.state = { - // ...this.state, - // isLoading: isLoading, - // } - // this._onDidChangeState.fire(); - // } - - // Create the file/directory if it doesn't exist private async _createMCPConfigFile(mcpConfigUri: URI): Promise { @@ -289,7 +267,18 @@ class MCPService extends Disposable implements IMCPService { 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 } - this._setUserSpecifiedServerNames(Object.keys(mcpConfigFile.mcpServers)) + // set state to loading if it's the first time we're seeing it + const mcpConfigOfName = mcpConfigFile.mcpServers + for (const serverName in mcpConfigOfName) { + if (serverName in this.state.mcpServerOfName) continue + this._setMCPServer(serverName, { + isOn: false, + status: 'loading', + error: undefined, + command: undefined, + tools: [], + }) + } const currMCPStateOfName = this.voidSettingsService.state.mcpServerStateOfName; const availableServers = Object.keys(mcpConfigFile.mcpServers); From 57bc32ac9b4c0b6c2a072bba46a95403cfab35c7 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 22:51:55 -0700 Subject: [PATCH 61/98] update loading? --- .../void/browser/react/src/util/services.tsx | 2 +- .../react/src/void-settings-tsx/Settings.tsx | 14 ++-- .../contrib/void/common/mcpService.ts | 82 +++++++------------ .../contrib/void/common/mcpServiceTypes.ts | 1 - .../void/common/voidSettingsService.ts | 44 +++++----- .../contrib/void/common/voidSettingsTypes.ts | 6 +- .../contrib/void/electron-main/mcpChannel.ts | 15 ++-- 7 files changed, 67 insertions(+), 97 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 de5ff82a..dc67784c 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 { MCPServerState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js' +import { MCPUserState, 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' diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index f901ad7b..76d2c17e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -910,6 +910,9 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) mcpService.toggleMCPServer(name, e); } + const voidSettings = useSettingsState() + const isOn = voidSettings.mcpUserStateOfName[name]?.isOn + return (
@@ -929,7 +932,7 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) {/* Power toggle switch */}
@@ -939,7 +942,7 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) {/* Tools section */}
- {server.isOn && server.tools.length > 0 ? ( + {isOn && server.tools.length > 0 ? ( server.tools.map((tool: { name: string; description?: string }) => ( {/* Command badge */} - {server.isOn && server.command && ( + {isOn && server.command && (
Command:
@@ -983,11 +986,6 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) // Main component that renders the list of servers const MCPServersList = () => { const mcpServiceState = useMCPServiceState() - const accessor = useAccessor(); - - const userSpecifiedMCPServerNames = mcpServiceState.userSpecifiedMCPServerNames - // TODO tell the user what servers they've specified (might be different from those found) - let content: React.ReactNode if (mcpServiceState.error) { diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 3b73af87..8868b9fb 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -14,17 +14,16 @@ 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, MCPAddServerResponse, MCPUpdateServerResponse, MCPDeleteServerResponse, MCPServerEventResponse, MCPServerObject, MCPToolCallParams, MCPGenericToolResponse } from './mcpServiceTypes.js'; +import { MCPServerOfName, MCPConfigFileType, MCPAddServerResponse, MCPUpdateServerResponse, MCPDeleteServerResponse, MCPServerObject, MCPToolCallParams, MCPGenericToolResponse } 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'; +import { MCPUserStateOfName } from './voidSettingsTypes.js'; type MCPState = { mcpServerOfName: MCPServerOfName, - error: string | undefined, - userSpecifiedMCPServerNames: string[], + error: string | undefined, // global parsing error } export interface IMCPService { @@ -77,7 +76,6 @@ class MCPService extends Disposable implements IMCPService { state: MCPState = { mcpServerOfName: {}, error: undefined, - userSpecifiedMCPServerNames: [], } // Emitters for server events @@ -96,15 +94,12 @@ class MCPService extends Disposable implements IMCPService { @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, ) { super(); - // 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._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._register((this.channel.listen('onAdd_server') satisfies Event)(e => { this._setMCPServerState(e.response.name, e.response.newServer) })); + this._register((this.channel.listen('onUpdate_server') satisfies Event)(e => { this._setMCPServerState(e.response.name, e.response.newServer) })); + this._register((this.channel.listen('onDelete_server') satisfies Event)(e => { this._setMCPServerState(e.response.name, e.response.newServer) })); + this._initialize(); } @@ -127,12 +122,7 @@ class MCPService extends Disposable implements IMCPService { } } - private async _onGetServerEvent(e: MCPServerEventResponse) { - this._setMCPServer(e.response.name, e.response.newServer) - } - - - private readonly _setMCPServer = async (serverName: string, newServer: MCPServerObject | undefined) => { + private readonly _setMCPServerState = async (serverName: string, newServer: MCPServerObject | undefined) => { this.state = { ...this.state, mcpServerOfName: { @@ -261,49 +251,37 @@ class MCPService extends Disposable implements IMCPService { private async _refreshMCPServers(): Promise { this._setHasError(undefined) - // TODO!!! set is loading - 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 newConfigFileJSON = await this._parseMCPConfigFile(); + if (!newConfigFileJSON) { console.log(`Not setting state: MCP config file not found`); return } + if (!newConfigFileJSON?.mcpServers) { console.log(`Not setting state: MCP config file did not have an 'mcpServers' field`); return } - // set state to loading if it's the first time we're seeing it - const mcpConfigOfName = mcpConfigFile.mcpServers + + const oldConfigFileNames = Object.keys(this.state.mcpServerOfName) + const newConfigFileNames = Object.keys(newConfigFileJSON.mcpServers) + + const addedServerNames = newConfigFileNames.filter(serverName => !oldConfigFileNames.includes(serverName)); // in new and not in old + const removedServerNames = oldConfigFileNames.filter(serverName => !newConfigFileNames.includes(serverName)); // in old and not in new + + // set isOn to any new servers in the config + const addedUserStateOfName: MCPUserStateOfName = {} + for (const name in addedServerNames) { addedUserStateOfName[name] = { isOn: true } } + await this.voidSettingsService.addMCPUserStateOfNames(addedUserStateOfName); + + // delete isOn for any servers that no longer show up in the config + await this.voidSettingsService.removeMCPUserStateOfNames(removedServerNames); + + // set all servers to loading + const mcpConfigOfName = newConfigFileJSON.mcpServers for (const serverName in mcpConfigOfName) { if (serverName in this.state.mcpServerOfName) continue - this._setMCPServer(serverName, { - isOn: false, + this._setMCPServerState(serverName, { status: 'loading', - error: undefined, - command: undefined, tools: [], }) } - const currMCPStateOfName = this.voidSettingsService.state.mcpServerStateOfName; - const availableServers = Object.keys(mcpConfigFile.mcpServers); - - // Handle added servers - const addedServers = availableServers.filter(serverName => !currMCPStateOfName[serverName]?.isOn); - const addedServersObject = addedServers.reduce((acc, serverName) => { - acc[serverName] = { isOn: true }; - return acc; - }, {} as MCPServerStateOfName); - await this.voidSettingsService.addMCPServerStateOfName(addedServersObject); - - // Handle removed servers - const removedServers = Object.keys(currMCPStateOfName).filter(serverName => availableServers.indexOf(serverName) === -1); - await this.voidSettingsService.removeMCPServerStateNames(removedServers); - - // Compile the updated server list as MCPServerStates - const updatedServers = Object.keys(currMCPStateOfName).reduce((acc, serverName) => { - if (availableServers.includes(serverName)) { - acc[serverName] = currMCPStateOfName[serverName]; - } - return acc; - }, {} as MCPServerStateOfName); - - this.channel.call('refreshMCPServers', { mcpConfig: mcpConfigFile, serverStates: updatedServers }) + this.channel.call('refreshMCPServers', { mcpConfig: newConfigFileJSON, userStateOfName: this.voidSettingsService.state.mcpUserStateOfName }) } public async callMCPTool(toolData: MCPToolCallParams): Promise { diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index cc568191..61bcf095 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -138,7 +138,6 @@ export interface MCPServerObject { // Command-based server properties tools: MCPTool[], status: 'loading' | 'error' | 'success' | 'offline', - isOn: boolean | undefined, command?: string, error?: string, } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 2458b1cd..b8f0b2dd 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -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, MCPServerStateOfName, MCPServerState } from './voidSettingsTypes.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPUserStateOfName as MCPUserStateOfName, MCPUserState } 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 mcpServerStateOfName: MCPServerStateOfName; + readonly mcpUserStateOfName: MCPUserStateOfName; // user-controlled state of MCP servers readonly _modelOptions: ModelOption[] // computed based on the two above items } @@ -76,9 +76,9 @@ export interface IVoidSettingsService { addModel(providerName: ProviderName, modelName: string): void; deleteModel(providerName: ProviderName, modelName: string): boolean; - addMCPServerStateOfName(serverStateOfName: MCPServerStateOfName): Promise; - removeMCPServerStateNames(serverNames: string[]): Promise; - setMCPServerState(serverName: string, state: MCPServerState): Promise; + addMCPUserStateOfNames(userStateOfName: MCPUserStateOfName): Promise; + removeMCPUserStateOfNames(serverNames: string[]): Promise; + setMCPServerState(serverName: string, state: MCPUserState): Promise; } @@ -218,7 +218,7 @@ const defaultState = () => { optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} }, overridesOfModel: deepClone(defaultOverridesOfModel), _modelOptions: [], // computed later - mcpServerStateOfName: {}, + mcpUserStateOfName: {}, } return d } @@ -368,7 +368,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const newGlobalSettings = this.state.globalSettings const newOverridesOfModel = this.state.overridesOfModel - const newMCPServerStateOfName = this.state.mcpServerStateOfName + const newMCPUserStateOfName = this.state.mcpUserStateOfName const newState = { modelSelectionOfFeature: newModelSelectionOfFeature, @@ -376,7 +376,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { settingsOfProvider: newSettingsOfProvider, globalSettings: newGlobalSettings, overridesOfModel: newOverridesOfModel, - mcpServerStateOfName: newMCPServerStateOfName, + mcpUserStateOfName: newMCPUserStateOfName, } this.state = _validatedModelState(newState) @@ -541,11 +541,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } // MCP Server State - private _setMCPServerStateOfName = async (newStates: MCPServerStateOfName) => { + private _setMCPUserStateOfName = async (newStates: MCPUserStateOfName) => { const newState: VoidSettingsState = { ...this.state, - mcpServerStateOfName: { - ...this.state.mcpServerStateOfName, + mcpUserStateOfName: { + ...this.state.mcpUserStateOfName, ...newStates } }; @@ -555,18 +555,18 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { this._metricsService.capture('Set MCP Server States', { newStates }); } - addMCPServerStateOfName = async (newMCPStates: MCPServerStateOfName) => { - const { mcpServerStateOfName: mcpServerStates } = this.state + addMCPUserStateOfNames = async (newMCPStates: MCPUserStateOfName) => { + const { mcpUserStateOfName: mcpServerStates } = this.state const newMCPServerStates = { ...mcpServerStates, ...newMCPStates, } - await this._setMCPServerStateOfName(newMCPServerStates) - this._metricsService.capture('Add MCP Server', { servers: Object.keys(newMCPStates).join(', ') }); + await this._setMCPUserStateOfName(newMCPServerStates) + this._metricsService.capture('Add MCP Servers', { servers: Object.keys(newMCPStates).join(', ') }); } - removeMCPServerStateNames = async (serverNames: string[]) => { - const { mcpServerStateOfName: mcpServerStates } = this.state + removeMCPUserStateOfNames = async (serverNames: string[]) => { + const { mcpUserStateOfName: mcpServerStates } = this.state const newMCPServerStates = { ...mcpServerStates, } @@ -575,18 +575,18 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { delete newMCPServerStates[serverName] } }) - await this._setMCPServerStateOfName(newMCPServerStates) - this._metricsService.capture('Remove MCP Server', { servers: serverNames.join(', ') }); + await this._setMCPUserStateOfName(newMCPServerStates) + this._metricsService.capture('Remove MCP Servers', { servers: serverNames.join(', ') }); } - setMCPServerState = async (serverName: string, state: MCPServerState) => { - const { mcpServerStateOfName: mcpServerStates } = this.state + setMCPServerState = async (serverName: string, state: MCPUserState) => { + const { mcpUserStateOfName: mcpServerStates } = this.state if (!(serverName in mcpServerStates)) return // if not in list, do nothing const newMCPServerStates = { ...mcpServerStates, [serverName]: state, } - await this._setMCPServerStateOfName(newMCPServerStates) + await this._setMCPUserStateOfName(newMCPServerStates) this._metricsService.capture('Update MCP Server State', { serverName, state }); } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index e2faf506..bdb34504 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -494,10 +494,10 @@ export const defaultOverridesOfModel = overridesOfModel -export interface MCPServerStateOfName { - [serverName: string]: MCPServerState | undefined; +export interface MCPUserStateOfName { + [serverName: string]: MCPUserState | undefined; } -export interface MCPServerState { +export interface MCPUserState { isOn: boolean; } diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 977ea841..0dcf98fa 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -17,7 +17,7 @@ import { MCPConfigFileType, MCPConfigFileServerType, MCPServerErrorModel, MCPSer import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { equals } from '../../../../base/common/objects.js'; -import { MCPServerStateOfName } from '../common/voidSettingsTypes.js'; +import { MCPUserStateOfName } from '../common/voidSettingsTypes.js'; // const getLoadingServerObject = (serverName: string, isOn: boolean | undefined) => { @@ -118,9 +118,9 @@ export class MCPChannel implements IServerChannel { // server functions - private async _refreshMCPServers(params: { mcpConfig: MCPConfigFileType, serverStates: MCPServerStateOfName }) { + private async _refreshMCPServers(params: { mcpConfig: MCPConfigFileType, userStateOfName: MCPUserStateOfName }) { - const { mcpConfig, serverStates } = params + const { mcpConfig, userStateOfName } = params // Get all prevServers const prevServers = { ...this.clients } @@ -174,7 +174,7 @@ export class MCPChannel implements IServerChannel { if (addedServers.length > 0) { // emit added servers const addPromises = addedServers.map(async (serverName) => { - const addedServer = await this._safeSetupServer(mcpServers[serverName], serverName, serverStates[serverName]?.isOn) + const addedServer = await this._safeSetupServer(mcpServers[serverName], serverName, userStateOfName[serverName]?.isOn) return { type: 'add', newServer: addedServer, @@ -189,7 +189,7 @@ export class MCPChannel implements IServerChannel { // 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) + const newServer = await this._safeSetupServer(mcpServers[serverName], serverName, userStateOfName[serverName]?.isOn) return { type: 'update', prevServer, @@ -234,7 +234,6 @@ export class MCPChannel implements IServerChannel { const { tools } = await client.listTools() formattedServer = { status: isOn ? 'success' : 'offline', - isOn, tools: tools, command: server.url.toString(), } @@ -245,7 +244,6 @@ export class MCPChannel implements IServerChannel { console.log(`Connected via SSE to ${serverName}`); formattedServer = { status: isOn ? 'success' : 'offline', - isOn, tools: [], command: server.url.toString(), } @@ -272,7 +270,6 @@ export class MCPChannel implements IServerChannel { // Format server object formattedServer = { status: isOn ? 'success' : 'offline', - isOn, tools: tools, command: fullCommand, } @@ -301,7 +298,6 @@ export class MCPChannel implements IServerChannel { const formattedError: MCPServerErrorModel = { status: 'error', - isOn: false, tools: [], error: typedErr.message, command: fullCommand, @@ -369,7 +365,6 @@ export class MCPChannel implements IServerChannel { name: serverName, newServer: { status: 'offline', - isOn, tools: [], command: '', // Explicitly set error to undefined From 89ac548ae9b2c8953a5b9b7582ab3f74f2191dcc Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 23:29:41 -0700 Subject: [PATCH 62/98] clearer types --- .../react/src/void-settings-tsx/Settings.tsx | 8 +- .../contrib/void/common/mcpService.ts | 20 +-- .../contrib/void/common/mcpServiceTypes.ts | 57 ++++----- .../contrib/void/electron-main/mcpChannel.ts | 115 +++++++++--------- 4 files changed, 91 insertions(+), 109 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 76d2c17e..d8b81fb4 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -19,7 +19,7 @@ import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsSer import Severity from '../../../../../../../base/common/severity.js' import { getModelCapabilities, modelOverrideKeys, ModelOverrides } from '../../../../common/modelCapabilities.js'; import { TransferEditorType, TransferFilesInfo } from '../../../extensionTransferTypes.js'; -import { MCPServerObject } from '../../../../common/mcpServiceTypes.js'; +import { MCPServer } from '../../../../common/mcpServiceTypes.js'; import { useMCPServiceState } from '../util/services.js'; const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => { @@ -901,7 +901,7 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: // full settings // MCP Server component -const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) => { +const MCPServer = ({ name, server }: { name: string, server: MCPServer }) => { const accessor = useAccessor(); const mcpService = accessor.get('IMCPService'); @@ -942,8 +942,8 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject }) {/* Tools section */}
- {isOn && server.tools.length > 0 ? ( - server.tools.map((tool: { name: string; description?: string }) => ( + {isOn && (server.tools ?? []).length > 0 ? ( + (server.tools ?? []).map((tool: { name: string; description?: string }) => ( ; toggleMCPServer(serverName: string, isOn: boolean): Promise; - readonly state: MCPState; // NOT persisted + readonly state: MCPServiceState; // NOT persisted onDidChangeState: Event; getCurrentMCPToolNames(): InternalToolInfo[]; @@ -73,7 +73,7 @@ class MCPService extends Disposable implements IMCPService { private readonly channel: IChannel // MCPChannel // list of MCP servers pulled from mcpChannel - state: MCPState = { + state: MCPServiceState = { mcpServerOfName: {}, error: undefined, } @@ -122,7 +122,7 @@ class MCPService extends Disposable implements IMCPService { } } - private readonly _setMCPServerState = async (serverName: string, newServer: MCPServerObject | undefined) => { + private readonly _setMCPServerState = async (serverName: string, newServer: MCPServer | undefined) => { this.state = { ...this.state, mcpServerOfName: { @@ -180,7 +180,7 @@ class MCPService extends Disposable implements IMCPService { public getCurrentMCPToolNames(): InternalToolInfo[] { const allTools = Object.entries(this.state.mcpServerOfName).flatMap(([serverName, server]) => { - return server.tools.map(tool => { + return server.tools?.map(tool => { // Convert JsonSchema to the expected format const convertedParams: { [paramName: string]: { description: string } } = {}; @@ -200,7 +200,7 @@ class MCPService extends Disposable implements IMCPService { serverName, }; }); - }); + }).filter(s => s !== undefined) return allTools; } @@ -229,7 +229,7 @@ class MCPService extends Disposable implements IMCPService { } - private async _parseMCPConfigFile(): Promise { + private async _parseMCPConfigFile(): Promise { const mcpConfigUri = await this._getMCPConfigFilePath(); try { const fileContent = await this.fileService.readFile(mcpConfigUri); @@ -238,7 +238,7 @@ class MCPService extends Disposable implements IMCPService { if (!configFileJson.mcpServers) { throw new Error('Missing mcpServers property'); } - return configFileJson as MCPConfigFileType; + return configFileJson as MCPConfigFileJSON; } catch (error) { const fullError = `Error parsing MCP config file: ${error}`; this._setHasError(fullError) @@ -281,7 +281,7 @@ class MCPService extends Disposable implements IMCPService { }) } - this.channel.call('refreshMCPServers', { mcpConfig: newConfigFileJSON, userStateOfName: this.voidSettingsService.state.mcpUserStateOfName }) + this.channel.call('refreshMCPServers', { mcpConfigFileJSON: newConfigFileJSON, userStateOfName: this.voidSettingsService.state.mcpUserStateOfName }) } public async callMCPTool(toolData: MCPToolCallParams): Promise { diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index 61bcf095..eb572bc7 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -116,7 +116,7 @@ export interface MCPTool { // MCP SERVER CONFIG FILE TYPES ----------------------------- -export interface MCPConfigFileServerType { +export interface MCPConfigFileEntryJSON { // Command-based server properties command?: string; args?: string[]; @@ -127,73 +127,60 @@ export interface MCPConfigFileServerType { headers?: Record; } -export interface MCPConfigFileType { - mcpServers: Record; +export interface MCPConfigFileJSON { + mcpServers: Record; } // SERVER EVENT TYPES ------------------------------------------ -export interface MCPServerObject { +export type MCPServer = { // Command-based server properties tools: MCPTool[], - status: 'loading' | 'error' | 'success' | 'offline', + status: 'loading' | 'success' | 'offline', command?: string, error?: string, +} | { + tools?: undefined, + status: 'error', + command?: string, + error: string, } export interface MCPServerOfName { - [serverName: string]: MCPServerObject; + [serverName: string]: MCPServer; } -// Create separate types for success and error cases -export type MCPServerSuccessModel = MCPServerObject; -export type MCPServerErrorModel = Omit & { error: string }; - - -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 = Parameters[0] -export type EventMCPServerSetupOnError = Parameters[0] - -export type MCPServerModel = MCPServerSuccessModel | MCPServerErrorModel; - - -export type MCPServerEventType = { +export type MCPServerEvent = { type: 'add'; name: string; prevServer?: undefined; - newServer: MCPServerModel; + newServer: MCPServer; } | { type: 'update'; name: string; - prevServer: MCPServerModel; - newServer: MCPServerModel; + prevServer: MCPServer; + newServer: MCPServer; } | { type: 'delete'; name: string; newServer?: undefined; - prevServer: MCPServerModel; + prevServer: MCPServer; } | { type: 'loading'; name: string; prevServer?: undefined; - newServer: MCPServerModel; + newServer: MCPServer; } // Response types -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 MCPServerEventResponse = { response: MCPServerEvent } + +export type MCPAddServerResponse = { response: MCPServerEvent & { type: 'add' } } +export type MCPUpdateServerResponse = { response: MCPServerEvent & { type: 'update' } } +export type MCPDeleteServerResponse = { response: MCPServerEvent & { type: 'delete' } } -export type MCPServerEventResponse = { response: MCPServerEventType } export interface MCPConfigFileParseErrorResponse { response: { diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 0dcf98fa..f9bd844b 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -13,28 +13,12 @@ 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, MCPAddServerResponse, MCPUpdateServerResponse, MCPDeleteServerResponse, MCPGenericToolResponse, MCPToolErrorResponse } from '../common/mcpServiceTypes.js'; +import { MCPConfigFileJSON, MCPConfigFileEntryJSON, MCPServer, MCPAddServerResponse, MCPUpdateServerResponse, MCPDeleteServerResponse, MCPGenericToolResponse, MCPToolErrorResponse } from '../common/mcpServiceTypes.js'; import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { equals } from '../../../../base/common/objects.js'; import { MCPUserStateOfName } 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`, @@ -44,10 +28,26 @@ const getClientConfig = (serverName: string) => { } + +type MCPServerNonError = MCPServer & { status: Omit } +type MCPServerError = MCPServer & { status: 'error' } + + + export class MCPChannel implements IServerChannel { // connected clients - private clients: { [clientId: string]: { client?: Client, mcpConfig: MCPConfigFileServerType, formattedServer: MCPServerModel } } = {} + private infoOfClientId: { + [clientId: string]: { + _client: Client, + mcpConfig: MCPConfigFileEntryJSON, + mcpServer: MCPServerNonError, + } | { + _client?: undefined, + mcpConfig: MCPConfigFileEntryJSON, + mcpServer: MCPServerError, + } + } = {} // mcp emitters private readonly mcpEmitters = { @@ -118,21 +118,17 @@ export class MCPChannel implements IServerChannel { // server functions - private async _refreshMCPServers(params: { mcpConfig: MCPConfigFileType, userStateOfName: MCPUserStateOfName }) { + private async _refreshMCPServers(params: { mcpConfigFileJSON: MCPConfigFileJSON, userStateOfName: MCPUserStateOfName }) { - const { mcpConfig, userStateOfName } = params + const { mcpConfigFileJSON, userStateOfName } = params // Get all prevServers - const prevServers = { ...this.clients } + const prevServers = { ...this.infoOfClientId } // Handle config file setup and changes - const { mcpServers } = mcpConfig + const { mcpServers } = mcpConfigFileJSON const serverNames = Object.keys(mcpServers) - if (serverNames.length === 0) { - // TODO: CHANGE THIS TO AN ERROR EVENT - console.log('No MCP servers found in config file.') - return - } + const getPrevAndNewServerConfig = (serverName: string) => { const prevMCPConfig = prevServers[serverName]?.mcpConfig const newMCPConfig = mcpServers[serverName] @@ -188,7 +184,7 @@ export class MCPChannel implements IServerChannel { if (updatedServers.length > 0) { // emit updated servers const updatePromises = updatedServers.map(async (serverName) => { - const prevServer = this.clients[serverName]?.formattedServer; + const prevServer = this.infoOfClientId[serverName]?.mcpServer; const newServer = await this._safeSetupServer(mcpServers[serverName], serverName, userStateOfName[serverName]?.isOn) return { type: 'update', @@ -204,9 +200,9 @@ export class MCPChannel implements IServerChannel { if (deletedServers.length > 0) { // emit deleted servers const deletePromises = deletedServers.map(async (serverName) => { - const prevServer = this.clients[serverName]?.formattedServer; - await this._closeServer(serverName) - this._removeServer(serverName) + const prevServer = this.infoOfClientId[serverName]?.mcpServer; + await this._closeClient(serverName) + this._removeClient(serverName) return { type: 'delete', prevServer, @@ -218,12 +214,12 @@ export class MCPChannel implements IServerChannel { } } - private async _callSetupServer(server: MCPConfigFileServerType, serverName: string, isOn = true) { + private async _callSetupServer(server: MCPConfigFileEntryJSON, serverName: string, isOn = true) { const clientConfig = getClientConfig(serverName) const client = new Client(clientConfig) let transport: Transport; - let formattedServer: MCPServerModel; + let formattedServer: MCPServer; if (server.url) { // first try HTTP, fall back to SSE @@ -279,12 +275,12 @@ export class MCPChannel implements IServerChannel { } - this.clients[serverName] = { client, mcpConfig: server, formattedServer } + this.infoOfClientId[serverName] = { _client: client, mcpConfig: server, mcpServer: formattedServer } return formattedServer; } // Helper function to safely setup a server - private async _safeSetupServer(serverConfig: MCPConfigFileServerType, serverName: string, isOn = true) { + private async _safeSetupServer(serverConfig: MCPConfigFileEntryJSON, serverName: string, isOn = true) { try { return await this._callSetupServer(serverConfig, serverName, isOn) } catch (err) { @@ -296,17 +292,17 @@ export class MCPChannel implements IServerChannel { fullCommand = `${serverConfig.command} ${serverConfig.args?.join(' ') || ''}` } - const formattedError: MCPServerErrorModel = { + const formattedError: MCPServerError = { status: 'error', - tools: [], + // tools: [], error: typedErr.message, command: fullCommand, } // Add the error to the clients object - this.clients[serverName] = { + this.infoOfClientId[serverName] = { mcpConfig: serverConfig, - formattedServer: formattedError, + mcpServer: formattedError, } return formattedError @@ -314,39 +310,38 @@ export class MCPChannel implements IServerChannel { } private async _closeAllMCPServers() { - for (const serverName in this.clients) { - await this._closeServer(serverName) - this._removeServer(serverName) + for (const serverName in this.infoOfClientId) { + await this._closeClient(serverName) + this._removeClient(serverName) } console.log('Closed all MCP servers'); } - private async _closeServer(serverName: string) { - const server = this.clients[serverName] - if (server) { - const { client } = server - if (client) { - await client.close() - } - // Remove the client from the clients object - delete this.clients[serverName].client - console.log(`Closed MCP server ${serverName}`); + private async _closeClient(serverName: string) { + const info = this.infoOfClientId[serverName] + if (!info) return + const { _client: client } = info + if (client) { + await client.close() } + // Remove the client from the clients object + delete this.infoOfClientId[serverName]._client + console.log(`Closed MCP server ${serverName}`); } - private _removeServer(serverName: string) { - if (this.clients[serverName]) { - delete this.clients[serverName] + private _removeClient(serverName: string) { + if (this.infoOfClientId[serverName]) { + delete this.infoOfClientId[serverName] console.log(`Removed MCP server ${serverName}`); } } private async _toggleMCPServer(serverName: string, isOn: boolean) { - const prevServer = this.clients[serverName]?.formattedServer + const prevServer = this.infoOfClientId[serverName]?.mcpServer if (isOn) { // Handle turning on the server // this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn)) - const formattedServer = await this._callSetupServer(this.clients[serverName].mcpConfig, serverName) + const formattedServer = await this._callSetupServer(this.infoOfClientId[serverName].mcpConfig, serverName) this.mcpEmitters.serverEvent.onUpdate.fire({ response: { type: 'update', @@ -358,7 +353,7 @@ export class MCPChannel implements IServerChannel { } else { // Handle turning off the server // this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn)) - this._closeServer(serverName) + this._closeClient(serverName) this.mcpEmitters.serverEvent.onUpdate.fire({ response: { type: 'update', @@ -380,9 +375,9 @@ export class MCPChannel implements IServerChannel { // tool call functions private async _callTool(serverName: string, toolName: string, params: any): Promise { - const server = this.clients[serverName] + const server = this.infoOfClientId[serverName] if (!server) throw new Error(`Server ${serverName} not found`) - const { client } = server + const { _client: client } = server if (!client) throw new Error(`Client for server ${serverName} not found`) // Call the tool with the provided parameters From f5b0f2444536715a8f778f963423728143a5899d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 23:51:00 -0700 Subject: [PATCH 63/98] fix --- .../void/browser/react/src/void-settings-tsx/Settings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index d8b81fb4..1e8e7f32 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -901,7 +901,7 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: // full settings // MCP Server component -const MCPServer = ({ name, server }: { name: string, server: MCPServer }) => { +const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer }) => { const accessor = useAccessor(); const mcpService = accessor.get('IMCPService'); @@ -1003,7 +1003,7 @@ const MCPServersList = () => { else { content = entries.map(([name, server]) => (
- +
)) } From 7aacbca580582c3e475b94a5fbd99f7ad6aca345 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 22 May 2025 02:04:28 -0700 Subject: [PATCH 64/98] updates/fixes to mcp --- .../contrib/void/browser/chatThreadService.ts | 45 +++- .../react/src/void-settings-tsx/Settings.tsx | 7 +- .../contrib/void/browser/toolsService.ts | 35 +-- .../void/common/chatThreadServiceTypes.ts | 12 +- .../void/common/directoryStrService.ts | 6 +- .../contrib/void/common/mcpService.ts | 115 ++++---- .../contrib/void/common/mcpServiceTypes.ts | 28 +- .../contrib/void/common/prompt/prompts.ts | 10 +- .../contrib/void/common/toolsServiceTypes.ts | 4 +- .../void/common/voidSettingsService.ts | 5 +- .../contrib/void/electron-main/mcpChannel.ts | 249 ++++++------------ 11 files changed, 199 insertions(+), 317 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 40a87ce8..567f2b7c 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -16,7 +16,7 @@ import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } import { generateUuid } from '../../../../base/common/uuid.js'; import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; -import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'; +import { approvalTypeOfToolName, BuiltinToolCallParams, BuiltinToolResultType } from '../common/toolsServiceTypes.js'; import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; @@ -181,7 +181,7 @@ export type ThreadStreamState = { llmInfo?: undefined; toolInfo: { toolName: ToolName; - toolParams: ToolCallParams[ToolName]; + toolParams: BuiltinToolCallParams[ToolName]; id: string; content: string; rawParams: RawToolParamsObj; @@ -532,7 +532,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const lastMsg = thread.messages[thread.messages.length - 1] - let params: ToolCallParams[ToolName] + let params: BuiltinToolCallParams[ToolName] if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') { params = lastMsg.params } @@ -597,12 +597,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { threadId: string, toolName: ToolName, toolId: string, - opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj }, + opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: BuiltinToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj }, ): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => { // compute these below - let toolParams: ToolCallParams[ToolName] - let toolResult: Awaited + let toolParams: BuiltinToolCallParams[ToolName] + let toolResult: Awaited let toolResultStr: string if (!opts.preapproved) { // skip this if pre-approved @@ -616,8 +616,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { return {} } // once validated, add checkpoint for edit - if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) } - if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['rewrite_file']).uri }) } + if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as BuiltinToolCallParams['edit_file']).uri }) } + if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as BuiltinToolCallParams['rewrite_file']).uri }) } // 2. if tool requires approval, break from the loop, awaiting approval @@ -638,6 +638,33 @@ class ChatThreadService extends Disposable implements IChatThreadService { + // TODO!!!!!!!!! + // const isBuiltInTool = (toolNames as string[]).includes(toolName) + // const callToolFn = (toolName: string, toolParams: BuiltinToolCallParams[ToolName]) => { + // if (isBuiltInTool) { + + + // } + // else { + + // } + + // } + + // const stringifyToolFn = (toolName: string, toolResult: Awaited) => { + // if (isBuiltInTool) { + + + // } + // else { + // if (result.event === 'error' || result.event === 'text') { + // return result.text; + // } + // } + // } + + + // 3. call the tool // this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams } as const @@ -1300,7 +1327,7 @@ We only need to do it for files that were edited since `from`, ie files between } // URIs of files that have been read else if (m.role === 'tool' && m.type === 'success' && m.name === 'read_file') { - const params = m.params as ToolCallParams['read_file'] + const params = m.params as BuiltinToolCallParams['read_file'] addURI(params.uri) } } diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 1e8e7f32..1be70ad8 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -905,11 +905,6 @@ const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer const accessor = useAccessor(); const mcpService = accessor.get('IMCPService'); - const handleChangeEvent = (e: boolean) => { - // Handle the change event - mcpService.toggleMCPServer(name, e); - } - const voidSettings = useSettingsState() const isOn = voidSettings.mcpUserStateOfName[name]?.isOn @@ -934,7 +929,7 @@ const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer mcpService.toggleServerIsOn(name, !isOn)} />
diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index c0b0e9ca..1c07ceb8 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -8,7 +8,7 @@ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js' import { ISearchService } from '../../../services/search/common/search.js' import { IEditCodeService } from './editCodeServiceInterface.js' import { ITerminalToolService } from './terminalToolService.js' -import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js' +import { LintErrorItem, BuiltinToolCallParams, BuiltinToolResultType } from '../common/toolsServiceTypes.js' import { IVoidModelService } from '../common/voidModelService.js' import { EndOfLinePreference } from '../../../../editor/common/model.js' import { IVoidCommandBarService } from './voidCommandBarService.js' @@ -19,22 +19,12 @@ import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js' import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js' import { IVoidSettingsService } from '../common/voidSettingsService.js' import { generateUuid } from '../../../../base/common/uuid.js' -import { IMCPService, MCPCallTool, MCPToolResultToString } from '../common/mcpService.js' // tool use for AI - - - - -type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] } -type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T] | Promise, interruptTool?: () => void }> } -type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited) => string } - -// Interfaces that accept both internal tools and MCP tools -export type ToolHandler = CallTool & MCPCallTool; -export type ToolResultToStringHandler = ToolResultToString & MCPToolResultToString - +type ValidateBuiltinParams = { [T in ToolName]: (p: RawToolParamsObj) => BuiltinToolCallParams[T] } +type CallBuiltinTool = { [T in ToolName]: (p: BuiltinToolCallParams[T]) => Promise<{ result: BuiltinToolResultType[T] | Promise, interruptTool?: () => void }> } +type BuiltinToolResultToString = { [T in ToolName]: (p: BuiltinToolCallParams[T], result: Awaited) => string } const isFalsy = (u: unknown) => { @@ -115,9 +105,9 @@ const checkIfIsFolder = (uriStr: string) => { export interface IToolsService { readonly _serviceBrand: undefined; - validateParams: ValidateParams; - callTool: ToolHandler; - stringOfResult: ToolResultToStringHandler; + validateParams: ValidateBuiltinParams; + callTool: CallBuiltinTool; + stringOfResult: BuiltinToolResultToString; } export const IToolsService = createDecorator('ToolsService'); @@ -126,9 +116,9 @@ export class ToolsService implements IToolsService { readonly _serviceBrand: undefined; - public validateParams: ValidateParams; - public callTool: ToolHandler; - public stringOfResult: ToolResultToStringHandler; + public validateParams: ValidateBuiltinParams; + public callTool: CallBuiltinTool; + public stringOfResult: BuiltinToolResultToString; constructor( @IFileService fileService: IFileService, @@ -142,7 +132,6 @@ export class ToolsService implements IToolsService { @IDirectoryStrService private readonly directoryStrService: IDirectoryStrService, @IMarkerService private readonly markerService: IMarkerService, @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, - @IMCPService private readonly mcpService: IMCPService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); @@ -452,8 +441,6 @@ export class ToolsService implements IToolsService { await this.terminalToolService.killPersistentTerminal(persistentTerminalId) return { result: {} } }, - // Returns MCP server call tool functions - ...this.mcpService.getMCPToolFns().callTool, } @@ -557,8 +544,6 @@ export class ToolsService implements IToolsService { kill_persistent_terminal: (params, _result) => { return `Successfully closed terminal "${params.persistentTerminalId}".`; }, - // All MCP server result to string functions - ...this.mcpService.getMCPToolFns().resultToString, } diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index fbf8e18e..ef2fb127 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -7,7 +7,7 @@ import { URI } from '../../../../base/common/uri.js'; import { VoidFileSnapshot } from './editCodeServiceTypes.js'; import { ToolName } from './prompt/prompts.js'; import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js'; -import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js'; +import { BuiltinToolCallParams, BuiltinToolResultType } from './toolsServiceTypes.js'; export type ToolMessage = { role: 'tool'; @@ -18,13 +18,13 @@ export type ToolMessage = { // in order of events: | { type: 'invalid_params', result: null, name: T, } - | { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user + | { type: 'tool_request', result: null, name: T, params: BuiltinToolCallParams[T], } // params were validated, awaiting user - | { type: 'running_now', result: null, name: T, params: ToolCallParams[T], } + | { type: 'running_now', result: null, name: T, params: BuiltinToolCallParams[T], } - | { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running - | { type: 'success', result: Awaited, name: T, params: ToolCallParams[T], } - | { type: 'rejected', result: null, name: T, params: ToolCallParams[T] } + | { type: 'tool_error', result: string, name: T, params: BuiltinToolCallParams[T], } // error when tool was running + | { type: 'success', result: Awaited, name: T, params: BuiltinToolCallParams[T], } + | { type: 'rejected', result: null, name: T, params: BuiltinToolCallParams[T] } ) // user rejected export type DecorativeCanceledTool = { diff --git a/src/vs/workbench/contrib/void/common/directoryStrService.ts b/src/vs/workbench/contrib/void/common/directoryStrService.ts index f661eca8..d9d0a319 100644 --- a/src/vs/workbench/contrib/void/common/directoryStrService.ts +++ b/src/vs/workbench/contrib/void/common/directoryStrService.ts @@ -9,7 +9,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IFileService, IFileStat } from '../../../../platform/files/common/files.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from './toolsServiceTypes.js'; +import { ShallowDirectoryItem, BuiltinToolCallParams, BuiltinToolResultType } from './toolsServiceTypes.js'; import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from './prompt/prompts.js'; @@ -76,7 +76,7 @@ export const computeDirectoryTree1Deep = async ( fileService: IFileService, rootURI: URI, pageNumber: number = 1, -): Promise => { +): Promise => { const stat = await fileService.resolve(rootURI, { resolveMetadata: false }); if (!stat.isDirectory) { return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 }; @@ -107,7 +107,7 @@ export const computeDirectoryTree1Deep = async ( }; }; -export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => { +export const stringifyDirectoryTree1Deep = (params: BuiltinToolCallParams['ls_dir'], result: BuiltinToolResultType['ls_dir']): string => { if (!result.children) { return `Error: ${params.uri} is not a directory`; } diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index a4af7e73..4180c5e8 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -14,7 +14,7 @@ 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, MCPConfigFileJSON, MCPAddServerResponse, MCPUpdateServerResponse, MCPDeleteServerResponse, MCPServer, MCPToolCallParams, MCPGenericToolResponse } from './mcpServiceTypes.js'; +import { MCPServerOfName, MCPConfigFileJSON, MCPServer, MCPToolCallParams, MCPGenericToolResponse, MCPServerEventResponse } from './mcpServiceTypes.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { InternalToolInfo } from './prompt/prompts.js'; import { IVoidSettingsService } from './voidSettingsService.js'; @@ -29,16 +29,16 @@ type MCPServiceState = { export interface IMCPService { readonly _serviceBrand: undefined; revealMCPConfigFile(): Promise; - toggleMCPServer(serverName: string, isOn: boolean): Promise; + toggleServerIsOn(serverName: string, isOn: boolean): Promise; readonly state: MCPServiceState; // NOT persisted onDidChangeState: Event; getCurrentMCPToolNames(): InternalToolInfo[]; - getMCPToolFns(): { - callTool: MCPCallTool; - resultToString: MCPToolResultToString - }; + + // TODO!!!!!!!!! getMCPToolDescriptors (the equivalent of tools in prompts.ts) + + // getMCPToolFns(): MCPCallToolOfToolName; } export const IMCPService = createDecorator('mcpConfigService'); @@ -50,21 +50,13 @@ const MCP_CONFIG_SAMPLE = { mcpServers: {} } const MCP_CONFIG_SAMPLE_STRING = JSON.stringify(MCP_CONFIG_SAMPLE, null, 2); -export interface MCPCallTool { +export interface MCPCallToolOfToolName { [toolName: string]: (params: any) => Promise<{ result: any | Promise, interruptTool?: () => void }>; } -export interface MCPToolResultToString { - [toolName: string]: (params: any, result: any) => string; -} - - - - - class MCPService extends Disposable implements IMCPService { _serviceBrand: undefined; @@ -96,9 +88,9 @@ class MCPService extends Disposable implements IMCPService { super(); this.channel = this.mainProcessService.getChannel('void-channel-mcp') - this._register((this.channel.listen('onAdd_server') satisfies Event)(e => { this._setMCPServerState(e.response.name, e.response.newServer) })); - this._register((this.channel.listen('onUpdate_server') satisfies Event)(e => { this._setMCPServerState(e.response.name, e.response.newServer) })); - this._register((this.channel.listen('onDelete_server') satisfies Event)(e => { this._setMCPServerState(e.response.name, e.response.newServer) })); + this._register((this.channel.listen('onAdd_server') satisfies Event)(e => { this._setMCPServerState(e.response.name, e.response.newServer) })); + this._register((this.channel.listen('onUpdate_server') satisfies Event)(e => { this._setMCPServerState(e.response.name, e.response.newServer) })); + this._register((this.channel.listen('onDelete_server') satisfies Event)(e => { this._setMCPServerState(e.response.name, e.response.newServer) })); this._initialize(); } @@ -205,9 +197,9 @@ class MCPService extends Disposable implements IMCPService { } // toggle MCP server and update isOn in void settings - public async toggleMCPServer(serverName: string, isOn: boolean): Promise { - this.channel.call('toggleMCPServer', { serverName, isOn }) + public async toggleServerIsOn(serverName: string, isOn: boolean): Promise { await this.voidSettingsService.setMCPServerState(serverName, { isOn }); + this.channel.call('toggleMCPServer', { serverName, isOn }) } // utility functions @@ -272,66 +264,55 @@ class MCPService extends Disposable implements IMCPService { await this.voidSettingsService.removeMCPUserStateOfNames(removedServerNames); // set all servers to loading - const mcpConfigOfName = newConfigFileJSON.mcpServers - for (const serverName in mcpConfigOfName) { + for (const serverName in newConfigFileJSON.mcpServers) { if (serverName in this.state.mcpServerOfName) continue this._setMCPServerState(serverName, { status: 'loading', tools: [], }) } + const updatedServerNames = Object.keys(newConfigFileJSON.mcpServers).filter(serverName => !addedServerNames.includes(serverName) && !removedServerNames.includes(serverName)) - this.channel.call('refreshMCPServers', { mcpConfigFileJSON: newConfigFileJSON, userStateOfName: this.voidSettingsService.state.mcpUserStateOfName }) + this.channel.call('refreshMCPServers', { + mcpConfigFileJSON: newConfigFileJSON, + addedServerNames, + removedServerNames, + updatedServerNames, + userStateOfName: this.voidSettingsService.state.mcpUserStateOfName, + }) } - public async callMCPTool(toolData: MCPToolCallParams): Promise { - const response = await this.channel.call('callTool', toolData); - return response; + + public async callMCPTool(toolData: MCPToolCallParams): Promise<{ result: MCPGenericToolResponse }> { + const result = await this.channel.call('callTool', toolData); + return { result }; } - public getMCPToolFns(): { callTool: MCPCallTool; resultToString: MCPToolResultToString } { - const tools = this.getCurrentMCPToolNames(); - const toolFns: MCPCallTool = {}; - const toolResultToStringFns: MCPToolResultToString = {}; + // public getMCPToolFns(): MCPToolResultType { + // const tools = this.getCurrentMCPToolNames(); + // const toolFns: MCPToolResultType = {}; - tools.forEach((tool) => { - const name = tool.name; - const serverName = tool.mcpServerName; + // tools.forEach((tool) => { + // const name = tool.name; + // // Define the tool call function + // const toolFn = async (params: { + // serverName: string, + // toolName: string, + // args: any + // }) => { + // const { serverName, toolName, args } = params; + // const response = await this.callMCPTool({ + // serverName, + // toolName, + // params: args, + // }); + // return { result: response } + // }; + // toolFns[name] = toolFn; + // }); - // Define the tool call function - const toolFn = async (params: { - serverName: string, - toolName: string, - args: any - }) => { - const { serverName, toolName, args } = params; - const response = await this.callMCPTool({ - serverName, - toolName, - params: args, - }); - return { - result: response, - }; - }; - - // Define the result-to-string function - const resultToStringFn = (params: any, result: MCPGenericToolResponse): string => { - if (result.event === 'error' || result.event === 'text') { - return result.text; - } - throw new Error(`MCP Server ${serverName} and Tool ${name} returned an unexpected result: ${JSON.stringify(result)}`); - }; - - toolFns[name] = toolFn; - toolResultToStringFns[name] = resultToStringFn; - }); - - return { - callTool: toolFns, - resultToString: toolResultToStringFns - }; - } + // return toolFns + // } } registerSingleton(IMCPService, MCPService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index eb572bc7..541dc443 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -152,36 +152,12 @@ export interface MCPServerOfName { } export type MCPServerEvent = { - type: 'add'; name: string; - prevServer?: undefined; - newServer: MCPServer; -} | { - type: 'update'; - name: string; - prevServer: MCPServer; - newServer: MCPServer; -} | { - type: 'delete'; - name: string; - newServer?: undefined; - prevServer: MCPServer; -} | { - type: 'loading'; - name: string; - prevServer?: undefined; - newServer: MCPServer; + prevServer?: MCPServer; + newServer?: MCPServer; } - -// Response types export type MCPServerEventResponse = { response: MCPServerEvent } -export type MCPAddServerResponse = { response: MCPServerEvent & { type: 'add' } } -export type MCPUpdateServerResponse = { response: MCPServerEvent & { type: 'update' } } -export type MCPDeleteServerResponse = { response: MCPServerEvent & { type: 'delete' } } - - - export interface MCPConfigFileParseErrorResponse { response: { type: 'config-file-error'; diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 2115728e..51289138 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -9,7 +9,7 @@ import { IDirectoryStrService } from '../directoryStrService.js'; import { StagingSelectionItem } from '../chatThreadServiceTypes.js'; import { os } from '../helpers/systemInfo.js'; import { RawToolParamsObj } from '../sendLLMMessageTypes.js'; -import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../toolsServiceTypes.js'; +import { approvalTypeOfToolName, BuiltinToolCallParams, BuiltinToolResultType } from '../toolsServiceTypes.js'; import { ChatMode } from '../voidSettingsTypes.js'; // Triple backtick wrapper used throughout the prompts for code blocks @@ -186,11 +186,11 @@ export type SnakeCaseKeys> = { // export const voidTools = { export const voidTools : { - [T in keyof ToolCallParams]: { + [T in keyof BuiltinToolCallParams]: { name: string; description: string; // more params can be generated than exist here, but these params must be a subset of them - params: Partial<{ [paramName in keyof SnakeCaseKeys]: { description: string } }> + params: Partial<{ [paramName in keyof SnakeCaseKeys]: { description: string } }> } } = { @@ -345,10 +345,10 @@ export const voidTools // go_to_definition // go_to_usages - } satisfies { [T in keyof ToolResultType]: InternalToolInfo } + } satisfies { [T in keyof BuiltinToolResultType]: InternalToolInfo } -export type ToolName = keyof ToolResultType +export type ToolName = keyof BuiltinToolResultType export const toolNames = Object.keys(voidTools) as ToolName[] type ToolParamNameOfTool = keyof (typeof voidTools)[T]['params'] diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 2eb3a784..57457366 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -37,7 +37,7 @@ export const toolApprovalTypes = new Set( ) // PARAMS OF TOOL CALL -export type ToolCallParams = { +export type BuiltinToolCallParams = { 'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number }, 'ls_dir': { uri: URI, pageNumber: number }, 'get_dir_tree': { uri: URI }, @@ -58,7 +58,7 @@ export type ToolCallParams = { } // RESULT OF TOOL CALL -export type ToolResultType = { +export type BuiltinToolResultType = { 'read_file': { fileContents: string, totalFileLen: number, totalNumLines: number, hasNextPage: boolean }, 'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, 'get_dir_tree': { str: string, }, diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index b8f0b2dd..07c893f9 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -580,10 +580,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } setMCPServerState = async (serverName: string, state: MCPUserState) => { - const { mcpUserStateOfName: mcpServerStates } = this.state - if (!(serverName in mcpServerStates)) return // if not in list, do nothing + const { mcpUserStateOfName } = this.state const newMCPServerStates = { - ...mcpServerStates, + ...mcpUserStateOfName, [serverName]: state, } await this._setMCPUserStateOfName(newMCPServerStates) diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index f9bd844b..adc7c9ff 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -13,10 +13,9 @@ 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 { MCPConfigFileJSON, MCPConfigFileEntryJSON, MCPServer, MCPAddServerResponse, MCPUpdateServerResponse, MCPDeleteServerResponse, MCPGenericToolResponse, MCPToolErrorResponse } from '../common/mcpServiceTypes.js'; +import { MCPConfigFileJSON, MCPConfigFileEntryJSON, MCPServer, MCPGenericToolResponse, MCPToolErrorResponse, MCPServerEventResponse } from '../common/mcpServiceTypes.js'; import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import { equals } from '../../../../base/common/objects.js'; import { MCPUserStateOfName } from '../common/voidSettingsTypes.js'; const getClientConfig = (serverName: string) => { @@ -27,53 +26,46 @@ const getClientConfig = (serverName: string) => { } } - - type MCPServerNonError = MCPServer & { status: Omit } type MCPServerError = MCPServer & { status: 'error' } +type ClientInfo = { + _client: Client, // _client is the client that connects with an mcp client. We're calling mcp clients "server" everywhere except here for naming consistency. + mcpServerEntryJSON: MCPConfigFileEntryJSON, + mcpServer: MCPServerNonError, +} | { + _client?: undefined, + mcpServerEntryJSON: MCPConfigFileEntryJSON, + mcpServer: MCPServerError, +} + +type InfoOfClientId = { + [clientId: string]: ClientInfo +} + export class MCPChannel implements IServerChannel { - // connected clients - private infoOfClientId: { - [clientId: string]: { - _client: Client, - mcpConfig: MCPConfigFileEntryJSON, - mcpServer: MCPServerNonError, - } | { - _client?: undefined, - mcpConfig: MCPConfigFileEntryJSON, - mcpServer: MCPServerError, - } - } = {} + private readonly infoOfClientId: InfoOfClientId = {} + private readonly _refreshingServerNames: Set = new Set() // mcp emitters private readonly mcpEmitters = { serverEvent: { - onAdd: new Emitter(), - onUpdate: new Emitter(), - onDelete: new Emitter(), - // onResult: new Emitter<>(), - // onError: new Emitter<>(), - // onChangeLoading: new Emitter(), // really onStart + onAdd: new Emitter(), + onUpdate: new Emitter(), + onDelete: new Emitter(), } - // toolCall: { - // success: new Emitter(), - // error: new Emitter(), - // }, } satisfies { serverEvent: { - onAdd: Emitter, - onUpdate: Emitter, - onDelete: Emitter, - // onChangeLoading: Emitter, + onAdd: Emitter, + onUpdate: Emitter, + onDelete: Emitter, } } constructor( - // private readonly metricsService: IMetricsService, ) { } // browser uses this to listen for changes @@ -118,108 +110,62 @@ export class MCPChannel implements IServerChannel { // server functions - private async _refreshMCPServers(params: { mcpConfigFileJSON: MCPConfigFileJSON, userStateOfName: MCPUserStateOfName }) { - const { mcpConfigFileJSON, userStateOfName } = params + private async _refreshMCPServers(params: { mcpConfigFileJSON: MCPConfigFileJSON, userStateOfName: MCPUserStateOfName, addedServerNames: string[], removedServerNames: string[], updatedServerNames: string[] }) { - // Get all prevServers - const prevServers = { ...this.infoOfClientId } + const { + mcpConfigFileJSON, + userStateOfName, + addedServerNames, + removedServerNames, + updatedServerNames, + } = params - // Handle config file setup and changes - const { mcpServers } = mcpConfigFileJSON - const serverNames = Object.keys(mcpServers) + const { mcpServers: mcpServersJSON } = mcpConfigFileJSON - const getPrevAndNewServerConfig = (serverName: string) => { - const prevMCPConfig = prevServers[serverName]?.mcpConfig - const newMCPConfig = mcpServers[serverName] - return { prevMCPConfig, newMCPConfig } - } + const allChanges: { type: 'added' | 'removed' | 'updated', serverName: string }[] = [ + ...addedServerNames.map(n => ({ serverName: n, type: 'added' }) as const), + ...removedServerNames.map(n => ({ serverName: n, type: 'removed' }) as const), + ...updatedServerNames.map(n => ({ serverName: n, type: 'updated' }) as const), + ] - // Divide the server based on event - const addedServers = serverNames.filter((serverName) => { - const { prevMCPConfig, newMCPConfig } = getPrevAndNewServerConfig(serverName) - 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 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 isNew = prevMCPConfig && !newMCPConfig - // if (isDeleted) { - // this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, serverStates[serverName]?.isOn)) - // } - return isNew - }) + await Promise.all( + allChanges.map(async ({ serverName, type }) => { - // Check if no changes were made - if (addedServers.length === 0 && updatedServers.length === 0 && deletedServers.length === 0) { - console.log('No changes to MCP servers found.') - return - } + // check if already refreshing + if (this._refreshingServerNames.has(serverName)) return + this._refreshingServerNames.add(serverName) - if (addedServers.length > 0) { - // emit added servers - const addPromises = addedServers.map(async (serverName) => { - const addedServer = await this._safeSetupServer(mcpServers[serverName], serverName, userStateOfName[serverName]?.isOn) - return { - type: 'add', - newServer: addedServer, - name: serverName, - } as const - }); - const formattedAddedResponses = await Promise.all(addPromises); - formattedAddedResponses.forEach((formattedResponse) => (this.mcpEmitters.serverEvent.onAdd.fire({ response: formattedResponse }))); - } - - if (updatedServers.length > 0) { - // emit updated servers - const updatePromises = updatedServers.map(async (serverName) => { const prevServer = this.infoOfClientId[serverName]?.mcpServer; - const newServer = await this._safeSetupServer(mcpServers[serverName], serverName, userStateOfName[serverName]?.isOn) - return { - type: 'update', - prevServer, - newServer: newServer, - name: serverName, - } as const - }); - const formattedUpdatedResponses = await Promise.all(updatePromises); - formattedUpdatedResponses.forEach((formattedResponse) => (this.mcpEmitters.serverEvent.onUpdate.fire({ response: formattedResponse }))); - } - if (deletedServers.length > 0) { - // emit deleted servers - const deletePromises = deletedServers.map(async (serverName) => { - const prevServer = this.infoOfClientId[serverName]?.mcpServer; - await this._closeClient(serverName) - this._removeClient(serverName) - return { - type: 'delete', - prevServer, - name: serverName, - } as const - }); - const formattedDeletedResponses = await Promise.all(deletePromises); - formattedDeletedResponses.forEach((formattedResponse) => (this.mcpEmitters.serverEvent.onDelete.fire({ response: formattedResponse }))); - } + // close and delete the old client + if (type === 'removed' || type === 'updated') { + await this._closeClient(serverName) + delete this.infoOfClientId[serverName] + this.mcpEmitters.serverEvent.onDelete.fire({ response: { prevServer, name: serverName, } }) + } + + // create a new client + if (type === 'added' || type === 'updated') { + const clientInfo = await this._createClient(mcpServersJSON[serverName], serverName, userStateOfName[serverName]?.isOn) + this.infoOfClientId[serverName] = clientInfo + this.mcpEmitters.serverEvent.onAdd.fire({ response: { newServer: clientInfo.mcpServer, name: serverName, } }) + } + }) + ) + + allChanges.forEach(({ serverName, type }) => { + this._refreshingServerNames.delete(serverName) + }) + } - private async _callSetupServer(server: MCPConfigFileEntryJSON, serverName: string, isOn = true) { + private async _createClientUnsafe(server: MCPConfigFileEntryJSON, serverName: string, isOn: boolean): Promise { const clientConfig = getClientConfig(serverName) const client = new Client(clientConfig) let transport: Transport; - let formattedServer: MCPServer; + let info: MCPServerNonError; if (server.url) { // first try HTTP, fall back to SSE @@ -228,7 +174,7 @@ export class MCPChannel implements IServerChannel { await client.connect(transport); console.log(`Connected via HTTP to ${serverName}`); const { tools } = await client.listTools() - formattedServer = { + info = { status: isOn ? 'success' : 'offline', tools: tools, command: server.url.toString(), @@ -238,7 +184,7 @@ export class MCPChannel implements IServerChannel { transport = new SSEClientTransport(server.url); await client.connect(transport); console.log(`Connected via SSE to ${serverName}`); - formattedServer = { + info = { status: isOn ? 'success' : 'offline', tools: [], command: server.url.toString(), @@ -264,7 +210,7 @@ export class MCPChannel implements IServerChannel { const fullCommand = `${server.command} ${server.args?.join(' ') || ''}` // Format server object - formattedServer = { + info = { status: isOn ? 'success' : 'offline', tools: tools, command: fullCommand, @@ -275,44 +221,25 @@ export class MCPChannel implements IServerChannel { } - this.infoOfClientId[serverName] = { _client: client, mcpConfig: server, mcpServer: formattedServer } - return formattedServer; + return { _client: client, mcpServerEntryJSON: server, mcpServer: info } } - // Helper function to safely setup a server - private async _safeSetupServer(serverConfig: MCPConfigFileEntryJSON, serverName: string, isOn = true) { + private async _createClient(serverConfig: MCPConfigFileEntryJSON, serverName: string, isOn = true): Promise { try { - return await this._callSetupServer(serverConfig, serverName, isOn) + const c: ClientInfo = await this._createClientUnsafe(serverConfig, serverName, isOn) + return c } catch (err) { - const typedErr = err as Error console.error(`❌ Failed to connect to server "${serverName}":`, err) - - let fullCommand = '' - if (serverConfig.command) { - fullCommand = `${serverConfig.command} ${serverConfig.args?.join(' ') || ''}` - } - - const formattedError: MCPServerError = { - status: 'error', - // tools: [], - error: typedErr.message, - command: fullCommand, - } - - // Add the error to the clients object - this.infoOfClientId[serverName] = { - mcpConfig: serverConfig, - mcpServer: formattedError, - } - - return formattedError + const fullCommand = !serverConfig.command ? '' : `${serverConfig.command} ${serverConfig.args?.join(' ') || ''}` + const c: MCPServerError = { status: 'error', error: err + '', command: fullCommand, } + return { mcpServerEntryJSON: serverConfig, mcpServer: c, } } } private async _closeAllMCPServers() { for (const serverName in this.infoOfClientId) { await this._closeClient(serverName) - this._removeClient(serverName) + delete this.infoOfClientId[serverName] } console.log('Closed all MCP servers'); } @@ -324,46 +251,38 @@ export class MCPChannel implements IServerChannel { if (client) { await client.close() } - // Remove the client from the clients object - delete this.infoOfClientId[serverName]._client console.log(`Closed MCP server ${serverName}`); } - private _removeClient(serverName: string) { - if (this.infoOfClientId[serverName]) { - delete this.infoOfClientId[serverName] - console.log(`Removed MCP server ${serverName}`); - } - } private async _toggleMCPServer(serverName: string, isOn: boolean) { const prevServer = this.infoOfClientId[serverName]?.mcpServer + // Handle turning on the server if (isOn) { - // Handle turning on the server // this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn)) - const formattedServer = await this._callSetupServer(this.infoOfClientId[serverName].mcpConfig, serverName) + const clientInfo = await this._createClientUnsafe(this.infoOfClientId[serverName].mcpServerEntryJSON, serverName, isOn) this.mcpEmitters.serverEvent.onUpdate.fire({ response: { - type: 'update', name: serverName, - newServer: formattedServer, + newServer: clientInfo.mcpServer, prevServer: prevServer, } }) - } else { - // Handle turning off the server + } + // Handle turning off the server + else { // this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn)) this._closeClient(serverName) + delete this.infoOfClientId[serverName]._client + this.mcpEmitters.serverEvent.onUpdate.fire({ response: { - type: 'update', name: serverName, newServer: { status: 'offline', tools: [], command: '', - // Explicitly set error to undefined - // to reset the error state + // Explicitly set error to undefined to reset the error state error: undefined, }, prevServer: prevServer, From d2d47175b557e91cc7dc7ac2e6b73279f4ad3f28 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 22 May 2025 02:18:14 -0700 Subject: [PATCH 65/98] cleanup + final mcp todos --- .../contrib/void/browser/chatThreadService.ts | 3 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 34 +++++++++---------- .../contrib/void/common/mcpService.ts | 18 ++++++---- .../contrib/void/common/modelCapabilities.ts | 1 - .../contrib/void/common/prompt/prompts.ts | 6 ++++ .../contrib/void/common/voidSettingsTypes.ts | 2 +- .../llmMessage/sendLLMMessage.impl.ts | 2 ++ .../contrib/void/electron-main/mcpChannel.ts | 5 +-- 8 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 567f2b7c..af93090d 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -638,7 +638,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { - // TODO!!!!!!!!! + // TOOL_TODO!!!!!!!!! call the builtin versus the MCP tool here and stringify + // const isBuiltInTool = (toolNames as string[]).includes(toolName) // const callToolFn = (toolName: string, toolParams: BuiltinToolCallParams[ToolName]) => { // if (isBuiltInTool) { diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 7e60cac8..9852d5c9 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -24,7 +24,7 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react'; import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js'; -import { approvalTypeOfToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes, ToolCallParams } from '../../../../common/toolsServiceTypes.js'; +import { approvalTypeOfToolName, BuiltinToolCallParams, LintErrorItem, ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'; import { CopyButton, EditToolAcceptRejectButtonsHTML, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../markdown/ApplyBlockHoverButtons.js'; import { IsRunningType } from '../../../chatThreadService.js'; import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; @@ -1420,7 +1420,7 @@ const getTitle = (toolMessage: Pick): { +const toolNameToDesc = (toolName: ToolName, _toolParams: BuiltinToolCallParams[ToolName] | undefined, accessor: ReturnType): { desc1: React.ReactNode, desc1Info?: string, } => { @@ -1431,95 +1431,95 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName const x = { 'read_file': () => { - const toolParams = _toolParams as ToolCallParams['read_file'] + const toolParams = _toolParams as BuiltinToolCallParams['read_file'] return { desc1: getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), }; }, 'ls_dir': () => { - const toolParams = _toolParams as ToolCallParams['ls_dir'] + const toolParams = _toolParams as BuiltinToolCallParams['ls_dir'] return { desc1: getFolderName(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), }; }, 'search_pathnames_only': () => { - const toolParams = _toolParams as ToolCallParams['search_pathnames_only'] + const toolParams = _toolParams as BuiltinToolCallParams['search_pathnames_only'] return { desc1: `"${toolParams.query}"`, } }, 'search_for_files': () => { - const toolParams = _toolParams as ToolCallParams['search_for_files'] + const toolParams = _toolParams as BuiltinToolCallParams['search_for_files'] return { desc1: `"${toolParams.query}"`, } }, 'search_in_file': () => { - const toolParams = _toolParams as ToolCallParams['search_in_file']; + const toolParams = _toolParams as BuiltinToolCallParams['search_in_file']; return { desc1: `"${toolParams.query}"`, desc1Info: getRelative(toolParams.uri, accessor), }; }, 'create_file_or_folder': () => { - const toolParams = _toolParams as ToolCallParams['create_file_or_folder'] + const toolParams = _toolParams as BuiltinToolCallParams['create_file_or_folder'] return { desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), } }, 'delete_file_or_folder': () => { - const toolParams = _toolParams as ToolCallParams['delete_file_or_folder'] + const toolParams = _toolParams as BuiltinToolCallParams['delete_file_or_folder'] return { desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), } }, 'rewrite_file': () => { - const toolParams = _toolParams as ToolCallParams['rewrite_file'] + const toolParams = _toolParams as BuiltinToolCallParams['rewrite_file'] return { desc1: getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), } }, 'edit_file': () => { - const toolParams = _toolParams as ToolCallParams['edit_file'] + const toolParams = _toolParams as BuiltinToolCallParams['edit_file'] return { desc1: getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), } }, 'run_command': () => { - const toolParams = _toolParams as ToolCallParams['run_command'] + const toolParams = _toolParams as BuiltinToolCallParams['run_command'] return { desc1: `"${toolParams.command}"`, } }, 'run_persistent_command': () => { - const toolParams = _toolParams as ToolCallParams['run_persistent_command'] + const toolParams = _toolParams as BuiltinToolCallParams['run_persistent_command'] return { desc1: `"${toolParams.command}"`, } }, 'open_persistent_terminal': () => { - const toolParams = _toolParams as ToolCallParams['open_persistent_terminal'] + const toolParams = _toolParams as BuiltinToolCallParams['open_persistent_terminal'] return { desc1: '' } }, 'kill_persistent_terminal': () => { - const toolParams = _toolParams as ToolCallParams['kill_persistent_terminal'] + const toolParams = _toolParams as BuiltinToolCallParams['kill_persistent_terminal'] return { desc1: toolParams.persistentTerminalId } }, 'get_dir_tree': () => { - const toolParams = _toolParams as ToolCallParams['get_dir_tree'] + const toolParams = _toolParams as BuiltinToolCallParams['get_dir_tree'] return { desc1: getFolderName(toolParams.uri.fsPath) ?? '/', desc1Info: getRelative(toolParams.uri, accessor), } }, 'read_lint_errors': () => { - const toolParams = _toolParams as ToolCallParams['read_lint_errors'] + const toolParams = _toolParams as BuiltinToolCallParams['read_lint_errors'] return { desc1: getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 4180c5e8..a892aa39 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -36,8 +36,12 @@ export interface IMCPService { getCurrentMCPToolNames(): InternalToolInfo[]; - // TODO!!!!!!!!! getMCPToolDescriptors (the equivalent of tools in prompts.ts) + // TOOL_TODO!!!! implement getMCPTools here, which gets merged with builtins in prompts + callMCPTool(toolData: MCPToolCallParams): Promise<{ result: MCPGenericToolResponse }>; + + + // this is outdated: // getMCPToolFns(): MCPCallToolOfToolName; } @@ -50,12 +54,12 @@ const MCP_CONFIG_SAMPLE = { mcpServers: {} } const MCP_CONFIG_SAMPLE_STRING = JSON.stringify(MCP_CONFIG_SAMPLE, null, 2); -export interface MCPCallToolOfToolName { - [toolName: string]: (params: any) => Promise<{ - result: any | Promise, - interruptTool?: () => void - }>; -} +// export interface MCPCallToolOfToolName { +// [toolName: string]: (params: any) => Promise<{ +// result: any | Promise, +// interruptTool?: () => void +// }>; +// } class MCPService extends Disposable implements IMCPService { diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 49b38b84..ee180dbc 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -1270,7 +1270,6 @@ const openRouterModelOptions_assumingOpenAICompat = { const openRouterSettings: VoidStaticProviderInfo = { modelOptions: openRouterModelOptions_assumingOpenAICompat, - // TODO!!! send a query to openrouter to get the price, etc. modelOptionsFallback: (modelName) => { const res = extensiveModelOptionsFallback(modelName) // openRouter does not support gemini-style, use openai-style instead diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 51289138..fc3fad15 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -362,6 +362,12 @@ export const isAToolName = (toolName: string): toolName is ToolName => { } export const availableTools = (chatMode: ChatMode) => { + + // TOOL_TODO!!!! + // merge MCP tools with these builtin tools + + + const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined : chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !(toolName in approvalTypeOfToolName)) : chatMode === 'agent' ? Object.keys(voidTools) as ToolName[] diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index bdb34504..62aaae65 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -33,7 +33,7 @@ export type VoidStatefulModelInfo = { // <-- STATEFUL modelName: string, type: 'default' | 'autodetected' | 'custom'; isHidden: boolean, // whether or not the user is hiding it (switched off) -} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves +} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 1190b283..9bba271d 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -217,6 +217,8 @@ const openAITools = (chatMode: ChatMode) => { return openAITools } + +// convert LLM tool call to our tool format const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => { if (!isAToolName(name)) return null const rawParams: RawToolParamsObj = {} diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index adc7c9ff..4ed0d65f 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -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 { MCPConfigFileJSON, MCPConfigFileEntryJSON, MCPServer, MCPGenericToolResponse, MCPToolErrorResponse, MCPServerEventResponse } from '../common/mcpServiceTypes.js'; +import { MCPConfigFileJSON, MCPConfigFileEntryJSON, MCPServer, MCPGenericToolResponse, MCPToolErrorResponse, MCPServerEventResponse, MCPToolCallParams } from '../common/mcpServiceTypes.js'; import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { MCPUserStateOfName } from '../common/voidSettingsTypes.js'; @@ -96,7 +96,8 @@ export class MCPChannel implements IServerChannel { await this._toggleMCPServer(params.serverName, params.isOn) } else if (command === 'callTool') { - const response = await this._safeCallTool(params.serverName, params.toolName, params.params) + const p: MCPToolCallParams = params + const response = await this._safeCallTool(p.serverName, p.toolName, p.params) return response } else { From 883a4663c9a46c7a3d7b6b2cc38e88c0d44b2669 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 22 May 2025 02:21:17 -0700 Subject: [PATCH 66/98] clarify --- src/vs/workbench/contrib/void/browser/chatThreadService.ts | 3 +-- src/vs/workbench/contrib/void/common/mcpService.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index af93090d..7b0fe613 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -638,8 +638,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { - // TOOL_TODO!!!!!!!!! call the builtin versus the MCP tool here and stringify - + // TOOL_TODO!!!!!!!!! call the builtin versus the MCP tool here (finish filling in the comment below and replace it out with the tool call and stringify functions further below) // const isBuiltInTool = (toolNames as string[]).includes(toolName) // const callToolFn = (toolName: string, toolParams: BuiltinToolCallParams[ToolName]) => { // if (isBuiltInTool) { diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index a892aa39..f516822d 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -36,7 +36,7 @@ export interface IMCPService { getCurrentMCPToolNames(): InternalToolInfo[]; - // TOOL_TODO!!!! implement getMCPTools here, which gets merged with builtins in prompts + // TOOL_TODO!!!! implement getMCPTools here, which gets merged with builtins in prompts.ts. Should generally be the same shape as voidTools in prompts.ts. callMCPTool(toolData: MCPToolCallParams): Promise<{ result: MCPGenericToolResponse }>; From 627ec18cc99ae0b2687755bcfd4034a0ea331995 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 22 May 2025 02:30:43 -0700 Subject: [PATCH 67/98] got event fires --- .../react/src/void-settings-tsx/Settings.tsx | 14 +++----------- src/vs/workbench/contrib/void/common/mcpService.ts | 11 ++++++++--- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 1be70ad8..126df985 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -925,9 +925,10 @@ const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer
{name}
{/* Power toggle switch */} -
+
mcpService.toggleServerIsOn(name, !isOn)} /> @@ -964,16 +965,7 @@ const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer )} {/* Error message if present */} - {server.error && ( -
- - - - - - {server.error} -
- )} + {server.error && ()}
); }; diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index f516822d..ca6d94a1 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -92,9 +92,14 @@ class MCPService extends Disposable implements IMCPService { super(); this.channel = this.mainProcessService.getChannel('void-channel-mcp') - this._register((this.channel.listen('onAdd_server') satisfies Event)(e => { this._setMCPServerState(e.response.name, e.response.newServer) })); - this._register((this.channel.listen('onUpdate_server') satisfies Event)(e => { this._setMCPServerState(e.response.name, e.response.newServer) })); - this._register((this.channel.listen('onDelete_server') satisfies Event)(e => { this._setMCPServerState(e.response.name, e.response.newServer) })); + + const onEvent = (e: MCPServerEventResponse) => { + console.log('GOT EVENT', e) + this._setMCPServerState(e.response.name, e.response.newServer) + } + this._register((this.channel.listen('onAdd_server') satisfies Event)(onEvent)); + this._register((this.channel.listen('onUpdate_server') satisfies Event)(onEvent)); + this._register((this.channel.listen('onDelete_server') satisfies Event)(onEvent)); this._initialize(); } From 81ab21868a65c4a87ac8b67e6092b30475ac547a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 22 May 2025 02:50:26 -0700 Subject: [PATCH 68/98] mcp ui + misc --- .../react/src/void-settings-tsx/Settings.tsx | 2 +- .../contrib/void/common/mcpService.ts | 23 ++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 126df985..5f6c632b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -1355,7 +1355,7 @@ Use Model Context Protocol to provide Agent mode with more tools. `} chatMessageLocation={undefined} />
- { await mcpService.revealMCPConfigFile() }}> + { await mcpService.revealMCPConfigFile() }}> Add MCP Server
diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index ca6d94a1..d6a77a74 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -205,14 +205,6 @@ class MCPService extends Disposable implements IMCPService { return allTools; } - // toggle MCP server and update isOn in void settings - public async toggleServerIsOn(serverName: string, isOn: boolean): Promise { - await this.voidSettingsService.setMCPServerState(serverName, { isOn }); - this.channel.call('toggleMCPServer', { serverName, isOn }) - } - - // utility functions - private async _getMCPConfigFilePath(): Promise { const appName = this.productService.dataFolderName const userHome = await this.pathService.userHome(); @@ -274,11 +266,7 @@ class MCPService extends Disposable implements IMCPService { // set all servers to loading for (const serverName in newConfigFileJSON.mcpServers) { - if (serverName in this.state.mcpServerOfName) continue - this._setMCPServerState(serverName, { - status: 'loading', - tools: [], - }) + this._setMCPServerState(serverName, { status: 'loading', tools: [] }) } const updatedServerNames = Object.keys(newConfigFileJSON.mcpServers).filter(serverName => !addedServerNames.includes(serverName) && !removedServerNames.includes(serverName)) @@ -292,6 +280,15 @@ class MCPService extends Disposable implements IMCPService { } + // toggle MCP server and update isOn in void settings + public async toggleServerIsOn(serverName: string, isOn: boolean): Promise { + this._setMCPServerState(serverName, { status: 'loading', tools: [] }) + + await this.voidSettingsService.setMCPServerState(serverName, { isOn }); + this.channel.call('toggleMCPServer', { serverName, isOn }) + } + + public async callMCPTool(toolData: MCPToolCallParams): Promise<{ result: MCPGenericToolResponse }> { const result = await this.channel.call('callTool', toolData); return { result }; From 83ec2000392afd17f7d8ab40a34d1d5fe962509b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 22 May 2025 11:22:06 -0700 Subject: [PATCH 69/98] claude 4 --- .../contrib/void/common/modelCapabilities.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index ee180dbc..4c12a2f5 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -78,6 +78,8 @@ export const defaultModelsOfProvider = { // 'gpt-4o-mini', ], anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models + 'claude-opus-4-0', + 'claude-sonnet-4-0', 'claude-3-7-sonnet-latest', 'claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest', @@ -106,6 +108,8 @@ export const defaultModelsOfProvider = { openRouter: [ // https://openrouter.ai/models // 'anthropic/claude-3.7-sonnet:thinking', + 'anthropic/claude-opus-4', + 'anthropic/claude-sonnet-4', 'qwen/qwen3-235b-a22b', 'anthropic/claude-3.7-sonnet', 'anthropic/claude-3.5-sonnet', @@ -465,6 +469,40 @@ const anthropicModelOptions = { reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000. we cap at 8192 because above is typically not necessary (often even buggy) }, + }, + 'claude-opus-4-20250514': { + contextWindow: 200_000, + reservedOutputTokenSpace: 8_192, + cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 30.00 }, + downloadable: false, + supportsFIM: false, + specialToolFormat: 'anthropic-style', + supportsSystemMessage: 'separated', + reasoningCapabilities: { + supportsReasoning: true, + canTurnOffReasoning: true, + canIOReasoning: true, + reasoningReservedOutputTokenSpace: 8192, // can bump it to 128_000 with beta mode output-128k-2025-02-19 + reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000. we cap at 8192 because above is typically not necessary (often even buggy) + }, + + }, + 'claude-sonnet-4-20250514': { + contextWindow: 200_000, + reservedOutputTokenSpace: 8_192, + cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 6.00 }, + downloadable: false, + supportsFIM: false, + specialToolFormat: 'anthropic-style', + supportsSystemMessage: 'separated', + reasoningCapabilities: { + supportsReasoning: true, + canTurnOffReasoning: true, + canIOReasoning: true, + reasoningReservedOutputTokenSpace: 8192, // can bump it to 128_000 with beta mode output-128k-2025-02-19 + reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000. we cap at 8192 because above is typically not necessary (often even buggy) + }, + }, 'claude-3-5-sonnet-20241022': { contextWindow: 200_000, @@ -524,6 +562,10 @@ const anthropicSettings: VoidStaticProviderInfo = { modelOptionsFallback: (modelName) => { const lower = modelName.toLowerCase() let fallbackName: keyof typeof anthropicModelOptions | null = null + if (lower.includes('claude-4-opus') || lower.includes('claude-opus-4')) fallbackName = 'claude-opus-4-20250514' + if (lower.includes('claude-4-sonnet') || lower.includes('claude-sonnet-4')) fallbackName = 'claude-sonnet-4-20250514' + + if (lower.includes('claude-3-7-sonnet')) fallbackName = 'claude-3-7-sonnet-20250219' if (lower.includes('claude-3-5-sonnet')) fallbackName = 'claude-3-5-sonnet-20241022' if (lower.includes('claude-3-5-haiku')) fallbackName = 'claude-3-5-haiku-20241022' @@ -1211,6 +1253,24 @@ const openRouterModelOptions_assumingOpenAICompat = { cost: { input: 0.8, output: 2.4 }, downloadable: false, }, + 'anthropic/claude-opus-4': { + contextWindow: 200_000, + reservedOutputTokenSpace: null, + cost: { input: 15.00, output: 75.00 }, + downloadable: false, + supportsFIM: false, + supportsSystemMessage: 'system-role', + reasoningCapabilities: false, + }, + 'anthropic/claude-sonnet-4': { + contextWindow: 200_000, + reservedOutputTokenSpace: null, + cost: { input: 15.00, output: 75.00 }, + downloadable: false, + supportsFIM: false, + supportsSystemMessage: 'system-role', + reasoningCapabilities: false, + }, 'anthropic/claude-3.7-sonnet:thinking': { contextWindow: 200_000, reservedOutputTokenSpace: null, From 5747904b851d1e8053ec57b787da4f0f94ca005e Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Thu, 22 May 2025 15:42:16 -0400 Subject: [PATCH 70/98] Created getMCPTools function which transforms the MCP tool to a similar shape to voidTools, also created a helper functions which turns InputSchema into params --- .../contrib/void/common/mcpService.ts | 63 ++++++++++++------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index d6a77a74..71677a74 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -34,7 +34,7 @@ export interface IMCPService { readonly state: MCPServiceState; // NOT persisted onDidChangeState: Event; - getCurrentMCPToolNames(): InternalToolInfo[]; + getMCPTools(): Record; // TOOL_TODO!!!! implement getMCPTools here, which gets merged with builtins in prompts.ts. Should generally be the same shape as voidTools in prompts.ts. @@ -179,30 +179,45 @@ class MCPService extends Disposable implements IMCPService { } } - public getCurrentMCPToolNames(): InternalToolInfo[] { - const allTools = Object.entries(this.state.mcpServerOfName).flatMap(([serverName, server]) => { - return server.tools?.map(tool => { - // Convert JsonSchema to the expected format - const convertedParams: { [paramName: string]: { description: string } } = {}; + public getMCPTools(): Record { + const allTools: Record = {}; + for (const serverName in this.state.mcpServerOfName) { + const server = this.state.mcpServerOfName[serverName]; + if (server.tools) { + server.tools.forEach(tool => { + allTools[tool.name] = { + description: tool.description || '', + params: this._transformInputSchemaToParams(tool.inputSchema), + name: tool.name, + mcpServerName: serverName, + }; + }); + } + } + return allTools + } - // Assuming tool.inputSchema has a 'properties' field that contains parameter definitions - if (tool.inputSchema && tool.inputSchema.properties) { - Object.entries(tool.inputSchema.properties).forEach(([paramName, paramSchema]: [string, any]) => { - convertedParams[paramName] = { - description: paramSchema.description || '' - }; - }); - } + private _transformInputSchemaToParams(inputSchema?: Record): { [paramName: string]: { description: string } } { - return { - description: tool.description || '', - params: convertedParams, - name: tool.name, - serverName, - }; - }); - }).filter(s => s !== undefined) - return allTools; + // Check if inputSchema is valid + if (!inputSchema || !inputSchema.properties) return {}; + + const params: { [paramName: string]: { description: string } } = {}; + Object.keys(inputSchema.properties).forEach(paramName => { + const propertyValues = inputSchema.properties[paramName]; + + // Check if propertyValues is not an object + if (typeof propertyValues !== 'object') { + console.warn(`Invalid property value for ${paramName}: expected object, got ${typeof propertyValues}`); + return; // in forEach the return is equivalent to continue + } + + // Add the parameter to the params object + params[paramName] = { + description: JSON.stringify(propertyValues.description || '', null, 2) || '', + } + }); + return params; } private async _getMCPConfigFilePath(): Promise { @@ -295,7 +310,7 @@ class MCPService extends Disposable implements IMCPService { } // public getMCPToolFns(): MCPToolResultType { - // const tools = this.getCurrentMCPToolNames(); + // const tools = this.getMCPTools(); // const toolFns: MCPToolResultType = {}; // tools.forEach((tool) => { From 43b6dba7a3d3f3f3f8d8ddf5c2945457269c12b6 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 22 May 2025 14:47:56 -0700 Subject: [PATCH 71/98] tool progress --- .../contrib/void/browser/chatThreadService.ts | 104 ++++++++++------- .../browser/convertToLLMMessageService.ts | 7 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 109 ++++++++++++++---- .../void/browser/react/src/util/inputs.tsx | 99 +++++++++++++++- .../contrib/void/browser/toolsService.ts | 10 +- .../void/common/chatThreadServiceTypes.ts | 13 +-- .../contrib/void/common/mcpService.ts | 10 +- .../contrib/void/common/mcpServiceTypes.ts | 2 +- .../contrib/void/common/prompt/prompts.ts | 31 ++--- .../void/common/sendLLMMessageTypes.ts | 6 +- .../contrib/void/common/toolsServiceTypes.ts | 41 +++++-- .../llmMessage/extractGrammar.ts | 19 +-- .../llmMessage/sendLLMMessage.impl.ts | 25 ++-- .../contrib/void/electron-main/mcpChannel.ts | 6 +- 14 files changed, 338 insertions(+), 144 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 7b0fe613..93a683a2 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -11,12 +11,12 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; -import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js'; +import { chat_userMessageContent } from '../common/prompt/prompts.js'; import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; -import { approvalTypeOfToolName, BuiltinToolCallParams, BuiltinToolResultType } from '../common/toolsServiceTypes.js'; +import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, isABuiltinToolName, ToolCallParams, ToolName, ToolResult } from '../common/toolsServiceTypes.js'; import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; @@ -37,6 +37,8 @@ import { deepClone } from '../../../../base/common/objects.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IDirectoryStrService } from '../common/directoryStrService.js'; import { IFileService } from '../../../../platform/files/common/files.js'; +import { IMCPService } from '../common/mcpService.js'; +import { RawMCPToolCall } from '../common/mcpServiceTypes.js'; // related to retrying when LLM message has error @@ -181,7 +183,7 @@ export type ThreadStreamState = { llmInfo?: undefined; toolInfo: { toolName: ToolName; - toolParams: BuiltinToolCallParams[ToolName]; + toolParams: ToolCallParams; id: string; content: string; rawParams: RawToolParamsObj; @@ -323,6 +325,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IDirectoryStrService private readonly _directoryStringService: IDirectoryStrService, @IFileService private readonly _fileService: IFileService, + @IMCPService private readonly _mcpService: IMCPService, ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -532,7 +535,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const lastMsg = thread.messages[thread.messages.length - 1] - let params: BuiltinToolCallParams[ToolName] + let params: ToolCallParams if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') { params = lastMsg.params } @@ -597,20 +600,30 @@ class ChatThreadService extends Disposable implements IChatThreadService { threadId: string, toolName: ToolName, toolId: string, - opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: BuiltinToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj }, + opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj }, ): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => { // compute these below - let toolParams: BuiltinToolCallParams[ToolName] - let toolResult: Awaited + let toolParams: ToolCallParams + let toolResult: ToolResult let toolResultStr: string + // Check if it's a built-in tool + const isBuiltInTool = isABuiltinToolName(toolName) + + if (!opts.preapproved) { // skip this if pre-approved // 1. validate tool params try { - const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams) - toolParams = params - } catch (error) { + if (isBuiltInTool) { + const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams) + toolParams = params + } + else { + toolParams = opts.unvalidatedToolParams + } + } + catch (error) { const errorMessage = getErrorMessage(error) this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, }) return {} @@ -621,8 +634,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 2. if tool requires approval, break from the loop, awaiting approval - - const approvalType = approvalTypeOfToolName[toolName] + const approvalType = isBuiltInTool ? approvalTypeOfBuiltinToolName[toolName] : 'mcp-tools' if (approvalType) { const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType] // add a tool_request because we use it for UI if a tool is loading (this should be improved in the future) @@ -638,30 +650,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { - // TOOL_TODO!!!!!!!!! call the builtin versus the MCP tool here (finish filling in the comment below and replace it out with the tool call and stringify functions further below) - // const isBuiltInTool = (toolNames as string[]).includes(toolName) - // const callToolFn = (toolName: string, toolParams: BuiltinToolCallParams[ToolName]) => { - // if (isBuiltInTool) { - - - // } - // else { - - // } - - // } - - // const stringifyToolFn = (toolName: string, toolResult: Awaited) => { - // if (isBuiltInTool) { - - - // } - // else { - // if (result.event === 'error' || result.event === 'text') { - // return result.text; - // } - // } - // } @@ -679,11 +667,26 @@ class ChatThreadService extends Disposable implements IChatThreadService { // set stream state this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams } }) - const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) - const interruptor = () => { interrupted = true; interruptTool?.() } - resolveInterruptor(interruptor) + if (isBuiltInTool) { + const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) + const interruptor = () => { interrupted = true; interruptTool?.() } + resolveInterruptor(interruptor) - toolResult = await result + toolResult = await result + } + else { + const mcpTools = this._mcpService.getMCPTools() + const mcpTool = mcpTools[toolName] + if (!mcpTool) { throw new Error(`MCP tool ${toolName} not found`) } + + resolveInterruptor(() => { }) + + toolResult = (await this._mcpService.callMCPTool({ + serverName: mcpTool.mcpServerName ?? 'unknown_mcp_server', + toolName: toolName, + params: toolParams + })).result + } if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here } @@ -698,7 +701,26 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 4. stringify the result to give to the LLM try { - toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) + if (isBuiltInTool) { + toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) + } + // For MCP tools, handle the result based on its type + else { + const toolResult_ = toolResult as RawMCPToolCall + if (toolResult_.event === 'text') { + toolResultStr = toolResult_.text + } else if (toolResult_.event === 'error') { + toolResultStr = `Error: ${toolResult_.text}` + } else if (toolResult_.event === 'image') { + toolResultStr = `[Image: ${toolResult_.image.mimeType}]` + } else if (toolResult_.event === 'audio') { + toolResultStr = `[Audio content]` + } else if (toolResult_.event === 'resource') { + toolResultStr = `[Resource content]` + } else { + toolResultStr = JSON.stringify(toolResult) + } + } } catch (error) { const errorMessage = this.toolErrMsgs.errWhenStringifying(error) this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams }) diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 01fd58aa..f27828c2 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -7,7 +7,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ChatMessage } from '../common/chatThreadServiceTypes.js'; import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../common/modelCapabilities.js'; -import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js'; +import { reParsedToolXMLString, chat_systemMessage } from '../common/prompt/prompts.js'; import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js'; @@ -16,6 +16,7 @@ import { ITerminalToolService } from './terminalToolService.js'; import { IVoidModelService } from '../common/voidModelService.js'; import { URI } from '../../../../base/common/uri.js'; import { EndOfLinePreference } from '../../../../editor/common/model.js'; +import { ToolName } from '../common/toolsServiceTypes.js'; export const EMPTY_MESSAGE = '(empty message)' @@ -455,8 +456,8 @@ const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => { return { text: c.text } } else if (c.type === 'tool_use') { - latestToolName = c.name as ToolName - return { functionCall: { id: c.id, name: c.name as ToolName, args: c.input } } + latestToolName = c.name + return { functionCall: { id: c.id, name: c.name, args: c.input } } } else return null }).filter(m => !!m) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 9852d5c9..16c9c2ee 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -13,7 +13,7 @@ import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markd import { URI } from '../../../../../../../base/common/uri.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; import { ErrorDisplay } from './ErrorDisplay.js'; -import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js'; +import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch, VoidDiffEditor } from '../util/inputs.js'; import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js'; import { PastThreadsList } from './SidebarThreadSelector.js'; import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; @@ -24,11 +24,11 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react'; import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js'; -import { approvalTypeOfToolName, BuiltinToolCallParams, LintErrorItem, ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'; +import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, builtinToolNames, isABuiltinToolName, ToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'; import { CopyButton, EditToolAcceptRejectButtonsHTML, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../markdown/ApplyBlockHoverButtons.js'; import { IsRunningType } from '../../../chatThreadService.js'; import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; -import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName, toolNames } from '../../../../common/prompt/prompts.js'; +import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME } from '../../../../common/prompt/prompts.js'; import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js'; import ErrorBoundary from './ErrorBoundary.js'; import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js'; @@ -36,6 +36,7 @@ import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js'; import { persistentTerminalNameOfId } from '../../../terminalToolService.js'; + export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps) => { return ( voidOpenFileFn(params.uri, accessor) const componentParams: ToolHeaderParams = { title, desc1, desc1OnClick, desc1Info, isError, icon, isRejected, } + + const editToolType = toolMessage.name === 'edit_file' ? 'diff' : 'rewrite' if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') { componentParams.children = // JumpToFileButton removed in favor of FileLinkText @@ -936,6 +940,7 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters @@ -1388,7 +1393,7 @@ const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => { } -const titleOfToolName = { +const titleOfBuiltinToolName = { 'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, 'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, 'get_dir_tree': { done: 'Inspected folder tree', proposed: 'Inspect folder tree', running: loadingTitleWrapper('Inspecting folder tree') }, @@ -1406,21 +1411,21 @@ const titleOfToolName = { 'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') }, 'search_in_file': { done: 'Searched in file', proposed: 'Search in file', running: loadingTitleWrapper('Searching in file') }, -} as const satisfies Record +} as const satisfies Record const getTitle = (toolMessage: Pick): React.ReactNode => { const t = toolMessage - if (!toolNames.includes(t.name as ToolName)) return t.name // good measure + if (!builtinToolNames.includes(t.name as BuiltinToolName)) return t.name // good measure - const toolName = t.name as ToolName - if (t.type === 'success') return titleOfToolName[toolName].done - if (t.type === 'running_now') return titleOfToolName[toolName].running - return titleOfToolName[toolName].proposed + const toolName = t.name as BuiltinToolName + if (t.type === 'success') return titleOfBuiltinToolName[toolName].done + if (t.type === 'running_now') return titleOfBuiltinToolName[toolName].running + return titleOfBuiltinToolName[toolName].proposed } -const toolNameToDesc = (toolName: ToolName, _toolParams: BuiltinToolCallParams[ToolName] | undefined, accessor: ReturnType): { +const toolNameToDesc = (toolName: BuiltinToolName, _toolParams: BuiltinToolCallParams[BuiltinToolName] | undefined, accessor: ReturnType): { desc1: React.ReactNode, desc1Info?: string, } => { @@ -1590,7 +1595,7 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) => ) - const approvalType = approvalTypeOfToolName[toolName] + const approvalType = isABuiltinToolName(toolName) ? approvalTypeOfBuiltinToolName[toolName] : 'mcp-tools' const approvalToggle = approvalType ?
: null @@ -1604,7 +1609,7 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) => export const ToolChildrenWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => { return
-
+
{children}
@@ -1633,12 +1638,18 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: -const EditToolChildren = ({ uri, code }: { uri: URI | undefined, code: string }) => { +const EditToolChildren = ({ uri, code, type }: { uri: URI | undefined, code: string, type: 'diff' | 'rewrite' }) => { + + const content = type === 'diff' ? + + : + return
- + {content}
+ } @@ -1819,9 +1830,57 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({ } +type WrapperProps = { toolMessage: Exclude, { type: 'invalid_params' }>, messageIdx: number, threadId: string } +const MCPToolWrapper = ({ toolMessage }: WrapperProps) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') -type ResultWrapper = (props: { toolMessage: Exclude, { type: 'invalid_params' }>, messageIdx: number, threadId: string }) => React.ReactNode -const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } } = { + const title = getTitle(toolMessage) + const desc1 = toolMessage.name + const icon = null + + if (toolMessage.type === 'tool_request') return null // do not show past requests + if (toolMessage.type === 'running_now') return null // do not show running + + const isError = false + const isRejected = toolMessage.type === 'rejected' + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected, } + + if (toolMessage.type === 'success') { + const { result } = toolMessage + componentParams.children = + + + + + } + else if (toolMessage.type === 'tool_error') { + const { result } = toolMessage + componentParams.bottomChildren = + + {result} + + + } + + return + +} + +type ResultWrapper = (props: WrapperProps) => React.ReactNode + +const builtinToolNameToComponent: { [T in BuiltinToolName]: { resultWrapper: ResultWrapper, } } = { 'read_file': { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -2257,12 +2316,12 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, }, 'rewrite_file': { resultWrapper: (params) => { - return + return } }, 'edit_file': { resultWrapper: (params) => { - return + return } }, @@ -2446,7 +2505,11 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me
} - const ToolResultWrapper = toolNameToComponent[chatMessage.name]?.resultWrapper as ResultWrapper + const toolName = chatMessage.name + const isBuiltInTool = isABuiltinToolName(toolName) + const ToolResultWrapper = isBuiltInTool ? builtinToolNameToComponent[toolName]?.resultWrapper as ResultWrapper + : MCPToolWrapper as ResultWrapper + if (ToolResultWrapper) return <>
@@ -2746,12 +2809,13 @@ const CommandBarInChat = () => { const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => { + if (!isABuiltinToolName( toolCallSoFar.name)) return null const accessor = useAccessor() const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined - const title = titleOfToolName[toolCallSoFar.name].proposed + const title = titleOfBuiltinToolName[toolCallSoFar.name].proposed const uriDone = toolCallSoFar.doneParams.includes('uri') const desc1 = @@ -2772,12 +2836,11 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => - - } diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index d2b838e1..a8e411ec 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -20,6 +20,11 @@ import { URI } from '../../../../../../../base/common/uri.js'; import { getBasename, getFolderName } from '../sidebar-tsx/SidebarChat.js'; import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react'; import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js'; +import { DiffEditorWidget } from '../../../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js'; +import { extractSearchReplaceBlocks } from '../../../../common/helpers/extractCodeFromResult.js'; +import { IAccessibilitySignalService } from '../../../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; +import { IEditorProgressService } from '../../../../../../../platform/progress/common/progress.js'; +import { detectLanguage } from '../../../../common/helpers/languageHelpers.js'; // type guard @@ -951,11 +956,11 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac const contextViewProvider = accessor.get('IContextViewService') return [ container, contextViewProvider, @@ -991,8 +996,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac inputBoxRef.current = instance; return disposables - }, [onChangeText, onCreateInstance, inputBoxRef]) - } + }, [onChangeText, onCreateInstance, inputBoxRef])} /> }; @@ -1840,4 +1844,93 @@ export const VoidButtonBgDarken = ({ children, disabled, onClick, className }: { // return
; // }; +/** + * ToolDiffEditor mounts a native VSCode DiffEditorWidget to show a diff between original and modified code blocks. + * Props: + * - uri: URI of the file (for language detection, etc) + * - searchReplaceBlocks: string in search/replace format (from LLM) + * - language?: string (optional, fallback to 'plaintext') + */ +export const VoidDiffEditor = ({ uri, searchReplaceBlocks, language }: { uri?: any, searchReplaceBlocks: string, language?: string }) => { + const accessor = useAccessor(); + const modelService = accessor.get('IModelService'); + const instantiationService = accessor.get('IInstantiationService'); + const languageService = accessor.get('ILanguageService'); + const contextKeyService = accessor.get('IContextKeyService'); + const codeEditorService = accessor.get('ICodeEditorService'); + + // Extract the first block (if present) + const blocks = extractSearchReplaceBlocks(searchReplaceBlocks); + const block = blocks[0] || { orig: '', final: '' }; + + // Use detectLanguage for language detection if not provided + let lang = language; + if (!lang) { + lang = detectLanguage(languageService, { uri: uri ?? null, fileContents: block.orig }); + } + + // Use ILanguageSelection for model creation + const languageSelection = useMemo(() => languageService.createById(lang!), [lang, languageService]); + + // Create models for original and modified + const originalModel = useMemo(() => + modelService.createModel(block.orig, languageSelection), + [block.orig, languageSelection, modelService] + ); + const modifiedModel = useMemo(() => + modelService.createModel(block.final, languageSelection), + [block.final, languageSelection, modelService] + ); + + // Clean up models on unmount + useEffect(() => { + return () => { + originalModel.dispose(); + modifiedModel.dispose(); + }; + }, [originalModel, modifiedModel]); + + // Imperatively mount the DiffEditorWidget + const divRef = useRef(null); + const editorRef = useRef(null); + + useEffect(() => { + if (!divRef.current) return; + // Create the diff editor instance + const editor = instantiationService.createInstance( + DiffEditorWidget, + divRef.current, + { + automaticLayout: true, + readOnly: true, + renderSideBySide: true, + minimap: { enabled: false }, + lineNumbers: 'off', + scrollbar: { vertical: 'auto', horizontal: 'auto', verticalScrollbarSize: 8, horizontalScrollbarSize: 8 }, + hover: { enabled: false }, + folding: false, + selectionHighlight: false, + renderLineHighlight: 'none', + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + overviewRulerBorder: false, + glyphMargin: false, + stickyScroll: { enabled: false }, + }, + { originalEditor: { isSimpleWidget: true }, modifiedEditor: { isSimpleWidget: true } } + ); + editor.setModel({ original: originalModel, modified: modifiedModel }); + editor.layout(); + editorRef.current = editor; + return () => { + editor.dispose(); + editorRef.current = null; + }; + }, [originalModel, modifiedModel, instantiationService]); + + return ( +
+ ); +}; + diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 1c07ceb8..6d56bd46 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -8,7 +8,7 @@ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js' import { ISearchService } from '../../../services/search/common/search.js' import { IEditCodeService } from './editCodeServiceInterface.js' import { ITerminalToolService } from './terminalToolService.js' -import { LintErrorItem, BuiltinToolCallParams, BuiltinToolResultType } from '../common/toolsServiceTypes.js' +import { LintErrorItem, BuiltinToolCallParams, BuiltinToolResultType, BuiltinToolName } from '../common/toolsServiceTypes.js' import { IVoidModelService } from '../common/voidModelService.js' import { EndOfLinePreference } from '../../../../editor/common/model.js' import { IVoidCommandBarService } from './voidCommandBarService.js' @@ -16,15 +16,15 @@ import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js' import { timeout } from '../../../../base/common/async.js' import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js' -import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js' +import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js' import { IVoidSettingsService } from '../common/voidSettingsService.js' import { generateUuid } from '../../../../base/common/uuid.js' // tool use for AI -type ValidateBuiltinParams = { [T in ToolName]: (p: RawToolParamsObj) => BuiltinToolCallParams[T] } -type CallBuiltinTool = { [T in ToolName]: (p: BuiltinToolCallParams[T]) => Promise<{ result: BuiltinToolResultType[T] | Promise, interruptTool?: () => void }> } -type BuiltinToolResultToString = { [T in ToolName]: (p: BuiltinToolCallParams[T], result: Awaited) => string } +type ValidateBuiltinParams = { [T in BuiltinToolName]: (p: RawToolParamsObj) => BuiltinToolCallParams[T] } +type CallBuiltinTool = { [T in BuiltinToolName]: (p: BuiltinToolCallParams[T]) => Promise<{ result: BuiltinToolResultType[T] | Promise, interruptTool?: () => void }> } +type BuiltinToolResultToString = { [T in BuiltinToolName]: (p: BuiltinToolCallParams[T], result: Awaited) => string } const isFalsy = (u: unknown) => { diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index ef2fb127..2c6e7da2 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -5,9 +5,8 @@ import { URI } from '../../../../base/common/uri.js'; import { VoidFileSnapshot } from './editCodeServiceTypes.js'; -import { ToolName } from './prompt/prompts.js'; import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js'; -import { BuiltinToolCallParams, BuiltinToolResultType } from './toolsServiceTypes.js'; +import { ToolCallParams, ToolName, ToolResult } from './toolsServiceTypes.js'; export type ToolMessage = { role: 'tool'; @@ -18,13 +17,13 @@ export type ToolMessage = { // in order of events: | { type: 'invalid_params', result: null, name: T, } - | { type: 'tool_request', result: null, name: T, params: BuiltinToolCallParams[T], } // params were validated, awaiting user + | { type: 'tool_request', result: null, name: T, params: ToolCallParams, } // params were validated, awaiting user - | { type: 'running_now', result: null, name: T, params: BuiltinToolCallParams[T], } + | { type: 'running_now', result: null, name: T, params: ToolCallParams, } - | { type: 'tool_error', result: string, name: T, params: BuiltinToolCallParams[T], } // error when tool was running - | { type: 'success', result: Awaited, name: T, params: BuiltinToolCallParams[T], } - | { type: 'rejected', result: null, name: T, params: BuiltinToolCallParams[T] } + | { type: 'tool_error', result: string, name: T, params: ToolCallParams, } // error when tool was running + | { type: 'success', result: Awaited>, name: T, params: ToolCallParams, } + | { type: 'rejected', result: null, name: T, params: ToolCallParams } ) // user rejected export type DecorativeCanceledTool = { diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 71677a74..ad2215dc 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -14,7 +14,7 @@ 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, MCPConfigFileJSON, MCPServer, MCPToolCallParams, MCPGenericToolResponse, MCPServerEventResponse } from './mcpServiceTypes.js'; +import { MCPServerOfName, MCPConfigFileJSON, MCPServer, MCPToolCallParams, RawMCPToolCall, MCPServerEventResponse } from './mcpServiceTypes.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { InternalToolInfo } from './prompt/prompts.js'; import { IVoidSettingsService } from './voidSettingsService.js'; @@ -36,9 +36,7 @@ export interface IMCPService { getMCPTools(): Record; - // TOOL_TODO!!!! implement getMCPTools here, which gets merged with builtins in prompts.ts. Should generally be the same shape as voidTools in prompts.ts. - - callMCPTool(toolData: MCPToolCallParams): Promise<{ result: MCPGenericToolResponse }>; + callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }>; // this is outdated: @@ -304,8 +302,8 @@ class MCPService extends Disposable implements IMCPService { } - public async callMCPTool(toolData: MCPToolCallParams): Promise<{ result: MCPGenericToolResponse }> { - const result = await this.channel.call('callTool', toolData); + public async callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }> { + const result = await this.channel.call('callTool', toolData); return { result }; } diff --git a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts index 541dc443..170cb843 100644 --- a/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/mcpServiceTypes.ts @@ -227,7 +227,7 @@ export type MCPToolErrorResponse = MCPToolEventResponse<'error'>; export type MCPToolImageResponse = MCPToolEventResponse<'image'>; export type MCPToolAudioResponse = MCPToolEventResponse<'audio'>; export type MCPToolResourceResponse = MCPToolEventResponse<'resource'>; -export type MCPGenericToolResponse = MCPToolTextResponse | MCPToolErrorResponse | MCPToolImageResponse | MCPToolAudioResponse | MCPToolResourceResponse; +export type RawMCPToolCall = MCPToolTextResponse | MCPToolErrorResponse | MCPToolImageResponse | MCPToolAudioResponse | MCPToolResourceResponse; export interface MCPToolCallParams { serverName: string; diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index fc3fad15..2ddaa369 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -9,7 +9,7 @@ import { IDirectoryStrService } from '../directoryStrService.js'; import { StagingSelectionItem } from '../chatThreadServiceTypes.js'; import { os } from '../helpers/systemInfo.js'; import { RawToolParamsObj } from '../sendLLMMessageTypes.js'; -import { approvalTypeOfToolName, BuiltinToolCallParams, BuiltinToolResultType } from '../toolsServiceTypes.js'; +import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, BuiltinToolResultType, ToolName } from '../toolsServiceTypes.js'; import { ChatMode } from '../voidSettingsTypes.js'; // Triple backtick wrapper used throughout the prompts for code blocks @@ -184,7 +184,7 @@ export type SnakeCaseKeys> = { // export const voidTools = { -export const voidTools +export const builtinTools : { [T in keyof BuiltinToolCallParams]: { name: string; @@ -348,32 +348,21 @@ export const voidTools } satisfies { [T in keyof BuiltinToolResultType]: InternalToolInfo } -export type ToolName = keyof BuiltinToolResultType -export const toolNames = Object.keys(voidTools) as ToolName[] - -type ToolParamNameOfTool = keyof (typeof voidTools)[T]['params'] -export type ToolParamName = { [T in ToolName]: ToolParamNameOfTool }[ToolName] - -const toolNamesSet = new Set(toolNames) - -export const isAToolName = (toolName: string): toolName is ToolName => { - const isAToolName = toolNamesSet.has(toolName) - return isAToolName -} export const availableTools = (chatMode: ChatMode) => { // TOOL_TODO!!!! // merge MCP tools with these builtin tools + // Note: This requires refactoring the messaging architecture to pass MCP tools from the renderer process + // to the electron-main process through the IPC channel. For now, MCP tools are handled separately + // in the chatThreadService where both built-in and MCP tools are called appropriately. - - - const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined - : chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !(toolName in approvalTypeOfToolName)) - : chatMode === 'agent' ? Object.keys(voidTools) as ToolName[] + const toolNames: BuiltinToolName[] | undefined = chatMode === 'normal' ? undefined + : chatMode === 'gather' ? (Object.keys(builtinTools) as BuiltinToolName[]).filter(toolName => !(toolName in approvalTypeOfBuiltinToolName)) + : chatMode === 'agent' ? Object.keys(builtinTools) as BuiltinToolName[] : undefined - const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName]) + const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => builtinTools[toolName]) return tools } @@ -390,7 +379,7 @@ const toolCallDefinitionsXMLString = (tools: InternalToolInfo[]) => { } export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => { - const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}`).join('\n') + const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName]}`).join('\n') return `\ <${toolName}>${!params ? '' : `\n${params}`} ` diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index f6e634ee..79140ebb 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { ToolName, ToolParamName } from './prompt/prompts.js' +import { ToolName, ToolParamName } from './toolsServiceTypes.js' import { ChatMode, ModelSelection, ModelSelectionOptions, OverridesOfModel, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js' @@ -78,12 +78,12 @@ export type LLMFIMMessage = { export type RawToolParamsObj = { - [paramName in ToolParamName]?: string; + [paramName in ToolParamName]?: string; } export type RawToolCallObj = { name: ToolName; rawParams: RawToolParamsObj; - doneParams: ToolParamName[]; + doneParams: ToolParamName[]; id: string; isDone: boolean; }; diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 57457366..413baef7 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -1,5 +1,7 @@ import { URI } from '../../../../base/common/uri.js' -import { ToolName } from './prompt/prompts.js'; +import { RawMCPToolCall } from './mcpServiceTypes.js'; +import { builtinTools } from './prompt/prompts.js'; +import { RawToolParamsObj } from './sendLLMMessageTypes.js'; @@ -16,7 +18,7 @@ export type ShallowDirectoryItem = { } -export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = { +export const approvalTypeOfBuiltinToolName: Partial<{ [T in BuiltinToolName]?: 'edits' | 'terminal' | 'mcp-tools' }> = { 'create_file_or_folder': 'edits', 'delete_file_or_folder': 'edits', 'rewrite_file': 'edits', @@ -28,13 +30,16 @@ export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'term } +export type ToolApprovalType = NonNullable<(typeof approvalTypeOfBuiltinToolName)[keyof typeof approvalTypeOfBuiltinToolName]>; + + +export const toolApprovalTypes = new Set([ + ...Object.values(approvalTypeOfBuiltinToolName), + 'mcp-tools', +]) + -// {{add: define new type for approval types}} -export type ToolApprovalType = NonNullable<(typeof approvalTypeOfToolName)[keyof typeof approvalTypeOfToolName]>; -export const toolApprovalTypes = new Set( - Object.values(approvalTypeOfToolName).filter((v): v is ToolApprovalType => v !== undefined) -) // PARAMS OF TOOL CALL export type BuiltinToolCallParams = { @@ -78,3 +83,25 @@ export type BuiltinToolResultType = { 'kill_persistent_terminal': {}, } + +export type ToolCallParams = T extends BuiltinToolName ? BuiltinToolCallParams[T] : RawToolParamsObj +export type ToolResult = T extends BuiltinToolName ? BuiltinToolResultType[T] : RawMCPToolCall + + +export type BuiltinToolName = keyof BuiltinToolResultType +export const builtinToolNames = Object.keys(builtinTools) as BuiltinToolName[] + +type BuiltinToolParamNameOfTool = keyof (typeof builtinTools)[T]['params'] +export type BuiltinToolParamName = { [T in BuiltinToolName]: BuiltinToolParamNameOfTool }[BuiltinToolName] + +const toolNamesSet = new Set(builtinToolNames) + +export const isABuiltinToolName = (toolName: string): toolName is BuiltinToolName => { + const isAToolName = toolNamesSet.has(toolName) + return isAToolName +} + + + +export type ToolName = BuiltinToolName | (string & {}) +export type ToolParamName = T extends BuiltinToolName ? BuiltinToolParamNameOfTool : string diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts index e96117b2..03f07bda 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts @@ -5,8 +5,9 @@ import { generateUuid } from '../../../../../base/common/uuid.js' import { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js' -import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js' +import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js' import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js' +import { BuiltinToolName, BuiltinToolParamName } from '../../common/toolsServiceTypes.js' import { ChatMode } from '../../common/voidSettingsTypes.js' @@ -164,15 +165,15 @@ const findIndexOfAny = (fullText: string, matches: string[]) => { type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined } -const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => { +const parseXMLPrefixToToolCall = (toolName: BuiltinToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => { const paramsObj: RawToolParamsObj = {} - const doneParams: ToolParamName[] = [] + const doneParams: BuiltinToolParamName[] = [] let isDone = false const getAnswer = (): RawToolCallObj => { // trim off all whitespace at and before first \n and after last \n for each param for (const p in paramsObj) { - const paramName = p as ToolParamName + const paramName = p as BuiltinToolParamName const orig = paramsObj[paramName] if (orig === undefined) continue paramsObj[paramName] = trimBeforeAndAfterNewLines(orig) @@ -202,16 +203,16 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: strin const pm = new SurroundingsRemover(str) - const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName[] + const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as BuiltinToolParamName[] if (allowedParams.length === 0) return getAnswer() - let latestMatchedOpenParam: null | ToolParamName = null + let latestMatchedOpenParam: null | BuiltinToolParamName = null let n = 0 while (true) { n += 1 if (n > 10) return getAnswer() // just for good measure as this code is early // find the param name opening tag - let matchedOpenParam: null | ToolParamName = null + let matchedOpenParam: null | BuiltinToolParamName = null for (const paramName of allowedParams) { const removed = pm.removeFromStartUntilFullMatch(`<${paramName}>`, true) if (removed) { @@ -278,7 +279,7 @@ export const extractXMLToolsWrapper = ( let trueFullText = '' let latestToolCall: RawToolCallObj | undefined = undefined - let foundOpenTag: { idx: number, toolName: ToolName } | null = null + let foundOpenTag: { idx: number, toolName: BuiltinToolName } | null = null let openToolTagBuffer = '' // the characters we've seen so far that come after a < with no space afterwards, not yet added to fullText let prevFullTextLen = 0 @@ -308,7 +309,7 @@ export const extractXMLToolsWrapper = ( const i = findIndexOfAny(fullText, toolOpenTags) if (i !== null) { const [idx, toolTag] = i - const toolName = toolTag.substring(1, toolTag.length - 1) as ToolName + const toolName = toolTag.substring(1, toolTag.length - 1) as BuiltinToolName // console.log('found ', toolName) foundOpenTag = { idx, toolName } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 9bba271d..37d50c9f 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -18,8 +18,9 @@ import { AnthropicLLMChatMessage, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMe import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js'; import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js'; -import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js'; +import { availableTools, InternalToolInfo, builtinTools } from '../../common/prompt/prompts.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; +import { isABuiltinToolName, BuiltinToolParamName } from '../../common/toolsServiceTypes.js'; const getGoogleApiKey = async () => { // module‑level singleton @@ -220,7 +221,7 @@ const openAITools = (chatMode: ChatMode) => { // convert LLM tool call to our tool format const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => { - if (!isAToolName(name)) return null + if (!isABuiltinToolName(name)) return null const rawParams: RawToolParamsObj = {} let input: unknown try { @@ -231,10 +232,10 @@ const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawT } if (input === null) return null if (typeof input !== 'object') return null - for (const paramName in voidTools[name].params) { - rawParams[paramName as ToolParamName] = (input as any)[paramName] + for (const paramName in builtinTools[name].params) { + rawParams[paramName as BuiltinToolParamName] = (input as any)[paramName] } - return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true } + return { id, name, rawParams, doneParams: Object.keys(rawParams) as BuiltinToolParamName[], isDone: true } } @@ -337,7 +338,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, - toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined, + toolCall: isABuiltinToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined, }) } @@ -425,14 +426,14 @@ const anthropicTools = (chatMode: ChatMode) => { const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => { const { id, name, input } = toolBlock - if (!isAToolName(name)) return null + if (!isABuiltinToolName(name)) return null const rawParams: RawToolParamsObj = {} if (input === null) return null if (typeof input !== 'object') return null - for (const paramName in voidTools[name].params) { - rawParams[paramName as ToolParamName] = (input as any)[paramName] + for (const paramName in builtinTools[name].params) { + rawParams[paramName as BuiltinToolParamName] = (input as any)[paramName] } - return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true } + return { id, name, rawParams, doneParams: Object.keys(rawParams) as BuiltinToolParamName[], isDone: true } } // ------------ ANTHROPIC ------------ @@ -494,7 +495,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag onText({ fullText, fullReasoning, - toolCall: isAToolName(fullToolName) ? { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined, + toolCall: isABuiltinToolName(fullToolName) ? { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined, }) } // there are no events for tool_use, it comes in at the end @@ -788,7 +789,7 @@ const sendGeminiChat = async ({ onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, - toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined, + toolCall: isABuiltinToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined, }) } diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 4ed0d65f..7e727131 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -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 { MCPConfigFileJSON, MCPConfigFileEntryJSON, MCPServer, MCPGenericToolResponse, MCPToolErrorResponse, MCPServerEventResponse, MCPToolCallParams } from '../common/mcpServiceTypes.js'; +import { MCPConfigFileJSON, MCPConfigFileEntryJSON, MCPServer, RawMCPToolCall, MCPToolErrorResponse, MCPServerEventResponse, MCPToolCallParams } from '../common/mcpServiceTypes.js'; import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { MCPUserStateOfName } from '../common/voidSettingsTypes.js'; @@ -294,7 +294,7 @@ export class MCPChannel implements IServerChannel { // tool call functions - private async _callTool(serverName: string, toolName: string, params: any): Promise { + private async _callTool(serverName: string, toolName: string, params: any): Promise { const server = this.infoOfClientId[serverName] if (!server) throw new Error(`Server ${serverName} not found`) const { _client: client } = server @@ -341,7 +341,7 @@ export class MCPChannel implements IServerChannel { } // tool call error wrapper - private async _safeCallTool(serverName: string, toolName: string, params: any): Promise { + private async _safeCallTool(serverName: string, toolName: string, params: any): Promise { try { const response = await this._callTool(serverName, toolName, params) return response From da6ffecc1da4c79872568357c1e8d857434bfd15 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 22 May 2025 15:31:56 -0700 Subject: [PATCH 72/98] mcp works! --- .../contrib/void/browser/chatThreadService.ts | 6 +- .../browser/convertToLLMMessageService.ts | 6 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 4 +- .../contrib/void/common/mcpService.ts | 30 +- .../contrib/void/common/prompt/prompts.ts | 343 +++++++++--------- .../void/common/sendLLMMessageService.ts | 5 + .../void/common/sendLLMMessageTypes.ts | 2 + .../contrib/void/common/toolsServiceTypes.ts | 10 - .../llmMessage/extractGrammar.ts | 7 +- .../llmMessage/sendLLMMessage.impl.ts | 34 +- .../llmMessage/sendLLMMessage.ts | 3 +- 11 files changed, 231 insertions(+), 219 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 93a683a2..c424d513 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -11,12 +11,12 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; -import { chat_userMessageContent } from '../common/prompt/prompts.js'; +import { chat_userMessageContent, isABuiltinToolName } from '../common/prompt/prompts.js'; import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; -import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, isABuiltinToolName, ToolCallParams, ToolName, ToolResult } from '../common/toolsServiceTypes.js'; +import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, ToolCallParams, ToolName, ToolResult } from '../common/toolsServiceTypes.js'; import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; @@ -676,7 +676,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } else { const mcpTools = this._mcpService.getMCPTools() - const mcpTool = mcpTools[toolName] + const mcpTool = mcpTools?.find(t => t.name === toolName) if (!mcpTool) { throw new Error(`MCP tool ${toolName} not found`) } resolveInterruptor(() => { }) diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index f27828c2..d8f398ac 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -17,6 +17,7 @@ import { IVoidModelService } from '../common/voidModelService.js'; import { URI } from '../../../../base/common/uri.js'; import { EndOfLinePreference } from '../../../../editor/common/model.js'; import { ToolName } from '../common/toolsServiceTypes.js'; +import { IMCPService } from '../common/mcpService.js'; export const EMPTY_MESSAGE = '(empty message)' @@ -539,6 +540,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess @ITerminalToolService private readonly terminalToolService: ITerminalToolService, @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, @IVoidModelService private readonly voidModelService: IVoidModelService, + @IMCPService private readonly mcpService: IMCPService, ) { super() } @@ -588,8 +590,10 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess const includeXMLToolDefinitions = !specialToolFormat + const mcpTools = this.mcpService.getMCPTools() + const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds() - const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, includeXMLToolDefinitions }) + const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, mcpTools, includeXMLToolDefinitions }) return systemMessage } diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 16c9c2ee..3b935267 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -24,11 +24,11 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react'; import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js'; -import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, builtinToolNames, isABuiltinToolName, ToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'; +import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, ToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'; import { CopyButton, EditToolAcceptRejectButtonsHTML, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../markdown/ApplyBlockHoverButtons.js'; import { IsRunningType } from '../../../chatThreadService.js'; import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; -import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME } from '../../../../common/prompt/prompts.js'; +import { builtinToolNames, isABuiltinToolName, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME } from '../../../../common/prompt/prompts.js'; import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js'; import ErrorBoundary from './ErrorBoundary.js'; import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js'; diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index ad2215dc..8d03544b 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -34,13 +34,8 @@ export interface IMCPService { readonly state: MCPServiceState; // NOT persisted onDidChangeState: Event; - getMCPTools(): Record; - + getMCPTools(): InternalToolInfo[] | undefined; callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }>; - - - // this is outdated: - // getMCPToolFns(): MCPCallToolOfToolName; } export const IMCPService = createDecorator('mcpConfigService'); @@ -177,21 +172,20 @@ class MCPService extends Disposable implements IMCPService { } } - public getMCPTools(): Record { - const allTools: Record = {}; + public getMCPTools(): InternalToolInfo[] | undefined { + const allTools: InternalToolInfo[] = [] for (const serverName in this.state.mcpServerOfName) { const server = this.state.mcpServerOfName[serverName]; - if (server.tools) { - server.tools.forEach(tool => { - allTools[tool.name] = { - description: tool.description || '', - params: this._transformInputSchemaToParams(tool.inputSchema), - name: tool.name, - mcpServerName: serverName, - }; - }); - } + server.tools?.forEach(tool => { + allTools.push({ + description: tool.description || '', + params: this._transformInputSchemaToParams(tool.inputSchema), + name: tool.name, + mcpServerName: serverName, + }) + }) } + if (allTools.length === 0) return undefined return allTools } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 2ddaa369..9e9c3b57 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -183,186 +183,197 @@ export type SnakeCaseKeys> = { -// export const voidTools = { -export const builtinTools - : { - [T in keyof BuiltinToolCallParams]: { - name: string; - description: string; - // more params can be generated than exist here, but these params must be a subset of them - params: Partial<{ [paramName in keyof SnakeCaseKeys]: { description: string } }> - } +export const builtinTools: { + [T in keyof BuiltinToolCallParams]: { + name: string; + description: string; + // more params can be generated than exist here, but these params must be a subset of them + params: Partial<{ [paramName in keyof SnakeCaseKeys]: { description: string } }> } - = { - // --- context-gathering (read/search/list) --- +} = { + // --- context-gathering (read/search/list) --- - read_file: { - name: 'read_file', - description: `Returns full contents of a given file.`, - params: { - ...uriParam('file'), - start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' }, - end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' }, - ...paginationParam, - }, + read_file: { + name: 'read_file', + description: `Returns full contents of a given file.`, + params: { + ...uriParam('file'), + start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' }, + end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' }, + ...paginationParam, }, + }, - ls_dir: { - name: 'ls_dir', - description: `Lists all files and folders in the given URI.`, - params: { - uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` }, - ...paginationParam, - }, + ls_dir: { + name: 'ls_dir', + description: `Lists all files and folders in the given URI.`, + params: { + uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` }, + ...paginationParam, }, + }, - get_dir_tree: { - name: 'get_dir_tree', - description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `, - params: { - ...uriParam('folder') - } - }, - - // pathname_search: { - // name: 'pathname_search', - // description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`, - - search_pathnames_only: { - name: 'search_pathnames_only', - description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`, - params: { - query: { description: `Your query for the search.` }, - include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' }, - ...paginationParam, - }, - }, - - - - search_for_files: { - name: 'search_for_files', - description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`, - params: { - query: { description: `Your query for the search.` }, - search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' }, - is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }, - ...paginationParam, - }, - }, - - // add new search_in_file tool - search_in_file: { - name: 'search_in_file', - description: `Returns an array of all the start line numbers where the content appears in the file.`, - params: { - ...uriParam('file'), - query: { description: 'The string or regex to search for in the file.' }, - is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' } - } - }, - - read_lint_errors: { - name: 'read_lint_errors', - description: `Use this tool to view all the lint errors on a file.`, - params: { - ...uriParam('file'), - }, - }, - - // --- editing (create/delete) --- - - create_file_or_folder: { - name: 'create_file_or_folder', - description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`, - params: { - ...uriParam('file or folder'), - }, - }, - - delete_file_or_folder: { - name: 'delete_file_or_folder', - description: `Delete a file or folder at the given path.`, - params: { - ...uriParam('file or folder'), - is_recursive: { description: 'Optional. Return true to delete recursively.' } - }, - }, - - edit_file: { - name: 'edit_file', - description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`, - params: { - ...uriParam('file'), - search_replace_blocks: { description: replaceTool_description } - }, - }, - - rewrite_file: { - name: 'rewrite_file', - description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`, - params: { - ...uriParam('file'), - new_content: { description: `The new contents of the file. Must be a string.` } - }, - }, - run_command: { - name: 'run_command', - description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`, - params: { - command: { description: 'The terminal command to run.' }, - cwd: { description: cwdHelper }, - }, - }, - - run_persistent_command: { - name: 'run_persistent_command', - description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`, - params: { - command: { description: 'The terminal command to run.' }, - persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' }, - }, - }, - - - - open_persistent_terminal: { - name: 'open_persistent_terminal', - description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`, - params: { - cwd: { description: cwdHelper }, - } - }, - - - kill_persistent_terminal: { - name: 'kill_persistent_terminal', - description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`, - params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } } + get_dir_tree: { + name: 'get_dir_tree', + description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `, + params: { + ...uriParam('folder') } + }, + // pathname_search: { + // name: 'pathname_search', + // description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`, - // go_to_definition - // go_to_usages - - } satisfies { [T in keyof BuiltinToolResultType]: InternalToolInfo } + search_pathnames_only: { + name: 'search_pathnames_only', + description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`, + params: { + query: { description: `Your query for the search.` }, + include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' }, + ...paginationParam, + }, + }, -export const availableTools = (chatMode: ChatMode) => { + search_for_files: { + name: 'search_for_files', + description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`, + params: { + query: { description: `Your query for the search.` }, + search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' }, + is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }, + ...paginationParam, + }, + }, - // TOOL_TODO!!!! - // merge MCP tools with these builtin tools - // Note: This requires refactoring the messaging architecture to pass MCP tools from the renderer process - // to the electron-main process through the IPC channel. For now, MCP tools are handled separately - // in the chatThreadService where both built-in and MCP tools are called appropriately. + // add new search_in_file tool + search_in_file: { + name: 'search_in_file', + description: `Returns an array of all the start line numbers where the content appears in the file.`, + params: { + ...uriParam('file'), + query: { description: 'The string or regex to search for in the file.' }, + is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' } + } + }, - const toolNames: BuiltinToolName[] | undefined = chatMode === 'normal' ? undefined + read_lint_errors: { + name: 'read_lint_errors', + description: `Use this tool to view all the lint errors on a file.`, + params: { + ...uriParam('file'), + }, + }, + + // --- editing (create/delete) --- + + create_file_or_folder: { + name: 'create_file_or_folder', + description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`, + params: { + ...uriParam('file or folder'), + }, + }, + + delete_file_or_folder: { + name: 'delete_file_or_folder', + description: `Delete a file or folder at the given path.`, + params: { + ...uriParam('file or folder'), + is_recursive: { description: 'Optional. Return true to delete recursively.' } + }, + }, + + edit_file: { + name: 'edit_file', + description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`, + params: { + ...uriParam('file'), + search_replace_blocks: { description: replaceTool_description } + }, + }, + + rewrite_file: { + name: 'rewrite_file', + description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`, + params: { + ...uriParam('file'), + new_content: { description: `The new contents of the file. Must be a string.` } + }, + }, + run_command: { + name: 'run_command', + description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`, + params: { + command: { description: 'The terminal command to run.' }, + cwd: { description: cwdHelper }, + }, + }, + + run_persistent_command: { + name: 'run_persistent_command', + description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`, + params: { + command: { description: 'The terminal command to run.' }, + persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' }, + }, + }, + + + + open_persistent_terminal: { + name: 'open_persistent_terminal', + description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`, + params: { + cwd: { description: cwdHelper }, + } + }, + + + kill_persistent_terminal: { + name: 'kill_persistent_terminal', + description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`, + params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } } + } + + + // go_to_definition + // go_to_usages + +} satisfies { [T in keyof BuiltinToolResultType]: InternalToolInfo } + + + + +export const builtinToolNames = Object.keys(builtinTools) as BuiltinToolName[] +const toolNamesSet = new Set(builtinToolNames) +export const isABuiltinToolName = (toolName: string): toolName is BuiltinToolName => { + const isAToolName = toolNamesSet.has(toolName) + return isAToolName +} + + + + + +export const availableTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => { + + const builtinToolNames: BuiltinToolName[] | undefined = chatMode === 'normal' ? undefined : chatMode === 'gather' ? (Object.keys(builtinTools) as BuiltinToolName[]).filter(toolName => !(toolName in approvalTypeOfBuiltinToolName)) : chatMode === 'agent' ? Object.keys(builtinTools) as BuiltinToolName[] : undefined - const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => builtinTools[toolName]) + const effectiveBuiltinTools = builtinToolNames?.map(toolName => builtinTools[toolName]) ?? undefined + const effectiveMCPTools = chatMode === 'agent' ? mcpTools : undefined + + const tools: InternalToolInfo[] | undefined = !(builtinToolNames || mcpTools) ? undefined + : [ + ...effectiveBuiltinTools ?? [], + ...effectiveMCPTools ?? [], + ] + return tools } @@ -388,8 +399,8 @@ export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolPar /* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */ // - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them. -const systemToolsXMLPrompt = (chatMode: ChatMode) => { - const tools = availableTools(chatMode) +const systemToolsXMLPrompt = (chatMode: ChatMode, mcpTools: InternalToolInfo[] | undefined) => { + const tools = availableTools(chatMode, mcpTools) if (!tools || tools.length === 0) return null const toolXMLDefinitions = (`\ @@ -414,7 +425,7 @@ const systemToolsXMLPrompt = (chatMode: ChatMode) => { // ======================================================== chat (normal, gather, agent) ======================================================== -export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => { +export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, mcpTools, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, mcpTools: InternalToolInfo[] | undefined, includeXMLToolDefinitions: boolean }) => { const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \ ${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.` : mode === 'gather' ? `to search, understand, and reference files in the user's codebase.` @@ -448,7 +459,7 @@ ${directoryStr} `) - const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null + const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode, mcpTools) : null const details: string[] = [] diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts index 7579cfec..7618e736 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts @@ -13,6 +13,7 @@ import { generateUuid } from '../../../../base/common/uuid.js'; import { Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { IVoidSettingsService } from './voidSettingsService.js'; +import { IMCPService } from './mcpService.js'; // calls channel to implement features export const ILLMMessageService = createDecorator('llmMessageService'); @@ -61,6 +62,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService @IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side) @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, // @INotificationService private readonly notificationService: INotificationService, + @IMCPService private readonly mcpService: IMCPService, ) { super() @@ -116,6 +118,8 @@ export class LLMMessageService extends Disposable implements ILLMMessageService const { settingsOfProvider, } = this.voidSettingsService.state + const mcpTools = this.mcpService.getMCPTools() + // add state for request id const requestId = generateUuid(); this.llmMessageHooks.onText[requestId] = onText @@ -129,6 +133,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService requestId, settingsOfProvider, modelSelection, + mcpTools, } satisfies MainSendLLMMessageParams); return requestId diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index 79140ebb..f476b851 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -3,6 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { InternalToolInfo } from './prompt/prompts.js' import { ToolName, ToolParamName } from './toolsServiceTypes.js' import { ChatMode, ModelSelection, ModelSelectionOptions, OverridesOfModel, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js' @@ -133,6 +134,7 @@ export type SendLLMMessageParams = { overridesOfModel: OverridesOfModel | undefined; settingsOfProvider: SettingsOfProvider; + mcpTools: InternalToolInfo[] | undefined; } & SendLLMType diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 413baef7..d930f5de 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -87,21 +87,11 @@ export type BuiltinToolResultType = { export type ToolCallParams = T extends BuiltinToolName ? BuiltinToolCallParams[T] : RawToolParamsObj export type ToolResult = T extends BuiltinToolName ? BuiltinToolResultType[T] : RawMCPToolCall - export type BuiltinToolName = keyof BuiltinToolResultType -export const builtinToolNames = Object.keys(builtinTools) as BuiltinToolName[] type BuiltinToolParamNameOfTool = keyof (typeof builtinTools)[T]['params'] export type BuiltinToolParamName = { [T in BuiltinToolName]: BuiltinToolParamNameOfTool }[BuiltinToolName] -const toolNamesSet = new Set(builtinToolNames) - -export const isABuiltinToolName = (toolName: string): toolName is BuiltinToolName => { - const isAToolName = toolNamesSet.has(toolName) - return isAToolName -} - - export type ToolName = BuiltinToolName | (string & {}) export type ToolParamName = T extends BuiltinToolName ? BuiltinToolParamNameOfTool : string diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts index 03f07bda..3f1a4224 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts @@ -261,11 +261,14 @@ const parseXMLPrefixToToolCall = (toolName: BuiltinToolName, toolId: string, str } export const extractXMLToolsWrapper = ( - onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode | null + onText: OnText, + onFinalMessage: OnFinalMessage, + chatMode: ChatMode | null, + mcpTools: InternalToolInfo[] | undefined, ): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => { if (!chatMode) return { newOnText: onText, newOnFinalMessage: onFinalMessage } - const tools = availableTools(chatMode) + const tools = availableTools(chatMode, mcpTools) if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage } const toolOfToolName: ToolOfToolName = {} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 37d50c9f..baa1b585 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -18,9 +18,9 @@ import { AnthropicLLMChatMessage, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMe import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js'; import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js'; -import { availableTools, InternalToolInfo, builtinTools } from '../../common/prompt/prompts.js'; +import { availableTools, InternalToolInfo, builtinTools, isABuiltinToolName } from '../../common/prompt/prompts.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; -import { isABuiltinToolName, BuiltinToolParamName } from '../../common/toolsServiceTypes.js'; +import { BuiltinToolParamName } from '../../common/toolsServiceTypes.js'; const getGoogleApiKey = async () => { // module‑level singleton @@ -49,6 +49,7 @@ type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; separateSystemMessage: string | undefined; chatMode: ChatMode | null; + mcpTools: InternalToolInfo[] | undefined; } type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; } export type ListParams_Internal = ModelListParams @@ -207,8 +208,8 @@ const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => { } satisfies OpenAI.Chat.Completions.ChatCompletionTool } -const openAITools = (chatMode: ChatMode) => { - const allowedTools = availableTools(chatMode) +const openAITools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => { + const allowedTools = availableTools(chatMode, mcpTools) if (!allowedTools || Object.keys(allowedTools).length === 0) return null const openAITools: OpenAI.Chat.Completions.ChatCompletionTool[] = [] @@ -242,7 +243,7 @@ const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawT // ------------ OPENAI-COMPATIBLE ------------ -const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage, overridesOfModel }: SendChatParams_Internal) => { +const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage, overridesOfModel, mcpTools }: SendChatParams_Internal) => { const { modelName, specialToolFormat, @@ -262,7 +263,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE } // tools - const potentialTools = chatMode !== null ? openAITools(chatMode) : null + const potentialTools = openAITools(chatMode, mcpTools) const nativeToolsObj = potentialTools && specialToolFormat === 'openai-style' ? { tools: potentialTools } as const : {} @@ -293,7 +294,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE // manually parse out tool results if XML if (!specialToolFormat) { - const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode) + const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools) onText = newOnText onFinalMessage = newOnFinalMessage } @@ -413,8 +414,8 @@ const toAnthropicTool = (toolInfo: InternalToolInfo) => { } satisfies Anthropic.Messages.Tool } -const anthropicTools = (chatMode: ChatMode) => { - const allowedTools = availableTools(chatMode) +const anthropicTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => { + const allowedTools = availableTools(chatMode, mcpTools) if (!allowedTools || Object.keys(allowedTools).length === 0) return null const anthropicTools: Anthropic.Messages.ToolUnion[] = [] @@ -437,7 +438,7 @@ const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBloc } // ------------ ANTHROPIC ------------ -const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => { +const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode, mcpTools }: SendChatParams_Internal) => { const { modelName, specialToolFormat, @@ -454,7 +455,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag const maxTokens = getReservedOutputTokenSpace(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled, overridesOfModel }) // tools - const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null + const potentialTools = anthropicTools(chatMode, mcpTools) const nativeToolsObj = potentialTools && specialToolFormat === 'anthropic-style' ? { tools: potentialTools, tool_choice: { type: 'auto' } } as const : {} @@ -478,7 +479,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag // manually parse out tool results if XML if (!specialToolFormat) { - const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode) + const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools) onText = newOnText onFinalMessage = newOnFinalMessage } @@ -679,8 +680,8 @@ const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => { } satisfies FunctionDeclaration } -const geminiTools = (chatMode: ChatMode): GeminiTool[] | null => { - const allowedTools = availableTools(chatMode) +const geminiTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined): GeminiTool[] | null => { + const allowedTools = availableTools(chatMode, mcpTools) if (!allowedTools || Object.keys(allowedTools).length === 0) return null const functionDecls: FunctionDeclaration[] = [] for (const t in allowedTools ?? {}) { @@ -706,6 +707,7 @@ const sendGeminiChat = async ({ providerName, modelSelectionOptions, chatMode, + mcpTools, }: SendChatParams_Internal) => { if (providerName !== 'gemini') throw new Error(`Sending Gemini chat, but provider was ${providerName}`) @@ -731,7 +733,7 @@ const sendGeminiChat = async ({ : undefined // tools - const potentialTools = chatMode !== null ? geminiTools(chatMode) : undefined + const potentialTools = geminiTools(chatMode, mcpTools) const toolConfig = potentialTools && specialToolFormat === 'gemini-style' ? potentialTools : undefined @@ -742,7 +744,7 @@ const sendGeminiChat = async ({ // manually parse out tool results if XML if (!specialToolFormat) { - const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode) + const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools) onText = newOnText onFinalMessage = newOnFinalMessage } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 969dec8f..27f35ad5 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -23,6 +23,7 @@ export const sendLLMMessage = async ({ overridesOfModel, chatMode, separateSystemMessage, + mcpTools, }: SendLLMMessageParams, metricsService: IMetricsService @@ -107,7 +108,7 @@ export const sendLLMMessage = async ({ } const { sendFIM, sendChat } = implementation if (messagesType === 'chatMessages') { - await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage, chatMode }) + await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage, chatMode, mcpTools }) return } if (messagesType === 'FIMMessage') { From f99589aaa68a59e51671892c80129dfccc70e898 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Thu, 22 May 2025 19:41:25 -0400 Subject: [PATCH 73/98] Fixed isOn server name setting --- src/vs/workbench/contrib/void/common/mcpService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 8d03544b..f2406d6e 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -265,7 +265,7 @@ class MCPService extends Disposable implements IMCPService { // set isOn to any new servers in the config const addedUserStateOfName: MCPUserStateOfName = {} - for (const name in addedServerNames) { addedUserStateOfName[name] = { isOn: true } } + for (const name of addedServerNames) { addedUserStateOfName[name] = { isOn: true } } await this.voidSettingsService.addMCPUserStateOfNames(addedUserStateOfName); // delete isOn for any servers that no longer show up in the config From 0a0234212867b6e6c6385e23dda18621ddcfe1a2 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 22 May 2025 16:42:02 -0700 Subject: [PATCH 74/98] MCP UI --- .../contrib/void/browser/chatThreadService.ts | 42 ++++++++----- .../react/src/sidebar-tsx/SidebarChat.tsx | 59 +++++++++++++------ .../void/common/chatThreadServiceTypes.ts | 2 + .../llmMessage/sendLLMMessage.impl.ts | 55 ++++++++--------- 4 files changed, 94 insertions(+), 64 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index c424d513..26115b88 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -187,6 +187,7 @@ export type ThreadStreamState = { id: string; content: string; rawParams: RawToolParamsObj; + mcpServerName: string | undefined; }; interrupt: Promise<() => void>; } | { @@ -448,7 +449,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { // if running now but stream state doesn't indicate it (happens if restart Void), cancel that last tool if (lastMessage && lastMessage.role === 'tool' && lastMessage.type === 'running_now') { - this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params }) + + this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params, mcpServerName: lastMessage.mcpServerName }) } } @@ -541,13 +543,17 @@ class ChatThreadService extends Disposable implements IChatThreadService { } else return - const { name, id, rawParams } = lastMsg + const { name, id, rawParams, mcpServerName } = lastMsg const errorMessage = this.toolErrMsgs.rejected - this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams }) + this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams, mcpServerName }) this._setStreamState(threadId, undefined) } + private _computeMCPServerOfToolName = (toolName: string) => { + return this._mcpService.getMCPTools()?.find(t => t.name === toolName)?.mcpServerName + } + async abortRunning(threadId: string) { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen @@ -556,13 +562,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (this.streamState[threadId]?.isRunning === 'LLM') { const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) + if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name, mcpServerName: this._computeMCPServerOfToolName(toolCallSoFar.name) }) } // add tool that's running else if (this.streamState[threadId]?.isRunning === 'tool') { - const { toolName, toolParams, id, content: content_, rawParams } = this.streamState[threadId].toolInfo + const { toolName, toolParams, id, content: content_, rawParams, mcpServerName } = this.streamState[threadId].toolInfo const content = content_ || this.toolErrMsgs.interrupted - this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null }) + this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null, mcpServerName }) } // reject the tool for the user if relevant else if (this.streamState[threadId]?.isRunning === 'awaiting_user') { @@ -600,6 +606,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { threadId: string, toolName: ToolName, toolId: string, + mcpServerName: string | undefined, opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj }, ): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => { @@ -625,7 +632,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } catch (error) { const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, }) + this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, mcpServerName }) return {} } // once validated, add checkpoint for edit @@ -638,7 +645,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (approvalType) { const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType] // add a tool_request because we use it for UI if a tool is loading (this should be improved in the future) - this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams }) + this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName }) if (!autoApprove) { return { awaitingUserApproval: true } } @@ -655,7 +662,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 3. call the tool // this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') - const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams } as const + const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName } as const this._updateLatestTool(threadId, runningTool) @@ -665,7 +672,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { try { // set stream state - this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams } }) + this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams, mcpServerName } }) if (isBuiltInTool) { const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) @@ -695,7 +702,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here const errorMessage = getErrorMessage(error) - this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams }) + this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName }) return {} } @@ -723,12 +730,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { } } catch (error) { const errorMessage = this.toolErrMsgs.errWhenStringifying(error) - this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams }) + this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName }) return {} } // 5. add to history and keep going - this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams }) + this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName }) return {} }; @@ -763,7 +770,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // before enter loop, call tool if (callThisToolFirst) { - const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params }) + const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, callThisToolFirst.mcpServerName, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params }) if (interrupted) { this._setStreamState(threadId, undefined) this._addUserCheckpoint({ threadId }) @@ -872,7 +879,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const { error } = llmRes const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) + if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name, mcpServerName: this._computeMCPServerOfToolName(toolCallSoFar.name) }) this._setStreamState(threadId, { isRunning: undefined, error }) this._addUserCheckpoint({ threadId }) @@ -889,7 +896,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { // call tool if there is one if (toolCall) { - const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, { preapproved: false, unvalidatedToolParams: toolCall.rawParams }) + const mcpTools = this._mcpService.getMCPTools() + const mcpTool = mcpTools?.find(t => t.name === toolCall.name) + + const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, mcpTool?.mcpServerName, { preapproved: false, unvalidatedToolParams: toolCall.rawParams }) if (interrupted) { this._setStreamState(threadId, undefined) return diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 3b935267..82c00afb 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1414,14 +1414,36 @@ const titleOfBuiltinToolName = { } as const satisfies Record -const getTitle = (toolMessage: Pick): React.ReactNode => { +const getTitle = (toolMessage: Pick): React.ReactNode => { const t = toolMessage - if (!builtinToolNames.includes(t.name as BuiltinToolName)) return t.name // good measure - const toolName = t.name as BuiltinToolName - if (t.type === 'success') return titleOfBuiltinToolName[toolName].done - if (t.type === 'running_now') return titleOfBuiltinToolName[toolName].running - return titleOfBuiltinToolName[toolName].proposed + // non-built-in title + if (!builtinToolNames.includes(t.name as BuiltinToolName)) { + + // descriptor of Running or Ran etc + const descriptor = + t.type === 'success' ? 'Ran' + : t.type === 'running_now' ? 'Running' + : t.type === 'tool_request' ? 'Requested' + : t.type === 'rejected' ? 'Canceled' + : t.type === 'invalid_params' ? 'Canceled' + : t.type === 'tool_error' ? 'Canceled' + : 'Ran' + + const title = `${descriptor} ${t.name}` + + if (t.type === 'running_now' || t.type === 'tool_request') + return loadingTitleWrapper(title) + return title + } + + // built-in title + else { + const toolName = t.name as BuiltinToolName + if (t.type === 'success') return titleOfBuiltinToolName[toolName].done + if (t.type === 'running_now') return titleOfBuiltinToolName[toolName].running + return titleOfBuiltinToolName[toolName].proposed + } } @@ -1700,9 +1722,9 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr, toolName, threadId }: -const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: string }) => { +const InvalidTool = ({ toolName, message, mcpServerName }: { toolName: ToolName, message: string, mcpServerName: string | undefined }) => { const accessor = useAccessor() - const title = getTitle({ name: toolName, type: 'invalid_params' }) + const title = getTitle({ name: toolName, type: 'invalid_params', mcpServerName }) const desc1 = 'Invalid parameters' const icon = null const isError = true @@ -1716,9 +1738,9 @@ const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: strin return } -const CanceledTool = ({ toolName }: { toolName: ToolName }) => { +const CanceledTool = ({ toolName, mcpServerName }: { toolName: ToolName, mcpServerName: string | undefined }) => { const accessor = useAccessor() - const title = getTitle({ name: toolName, type: 'rejected' }) + const title = getTitle({ name: toolName, type: 'rejected', mcpServerName }) const desc1 = '' const icon = null const isRejected = true @@ -1839,7 +1861,7 @@ const MCPToolWrapper = ({ toolMessage }: WrapperProps) => { const desc1 = toolMessage.name const icon = null - if (toolMessage.type === 'tool_request') return null // do not show past requests + if (toolMessage.type === 'running_now') return null // do not show running const isError = false @@ -1847,16 +1869,17 @@ const MCPToolWrapper = ({ toolMessage }: WrapperProps) => { const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected, } - if (toolMessage.type === 'success') { + componentParams.info = `${toolMessage.mcpServerName} MCP server` + + if (toolMessage.type === 'success' || toolMessage.type === 'tool_request') { const { result } = toolMessage componentParams.children = - +
} @@ -2529,7 +2552,7 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me else if (role === 'interrupted_streaming_tool') { return
- +
} @@ -2809,7 +2832,7 @@ const CommandBarInChat = () => { const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => { - if (!isABuiltinToolName( toolCallSoFar.name)) return null + if (!isABuiltinToolName(toolCallSoFar.name)) return null const accessor = useAccessor() diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 2c6e7da2..44dc307e 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -13,6 +13,7 @@ export type ToolMessage = { content: string; // give this result to LLM (string of value) id: string; rawParams: RawToolParamsObj; + mcpServerName: string | undefined; // the server name at the time of the call } & ( // in order of events: | { type: 'invalid_params', result: null, name: T, } @@ -29,6 +30,7 @@ export type ToolMessage = { export type DecorativeCanceledTool = { role: 'interrupted_streaming_tool'; name: ToolName; + mcpServerName: string | undefined; // the server name at the time of the call } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index baa1b585..52676f1d 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -18,7 +18,7 @@ import { AnthropicLLMChatMessage, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMe import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js'; import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js'; -import { availableTools, InternalToolInfo, builtinTools, isABuiltinToolName } from '../../common/prompt/prompts.js'; +import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { BuiltinToolParamName } from '../../common/toolsServiceTypes.js'; @@ -221,21 +221,26 @@ const openAITools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | u // convert LLM tool call to our tool format -const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => { - if (!isABuiltinToolName(name)) return null - const rawParams: RawToolParamsObj = {} +const rawToolCallObjOfParamsStr = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => { let input: unknown - try { - input = JSON.parse(toolParamsStr) - } - catch (e) { - return null - } + try { input = JSON.parse(toolParamsStr) } + catch (e) { return null } + if (input === null) return null if (typeof input !== 'object') return null - for (const paramName in builtinTools[name].params) { - rawParams[paramName as BuiltinToolParamName] = (input as any)[paramName] - } + + const rawParams: RawToolParamsObj = input + return { id, name, rawParams, doneParams: Object.keys(rawParams) as BuiltinToolParamName[], isDone: true } +} + + +const rawToolCallObjOfAnthropicParams = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => { + const { id, name, input } = toolBlock + + if (input === null) return null + if (typeof input !== 'object') return null + + const rawParams: RawToolParamsObj = input return { id, name, rawParams, doneParams: Object.keys(rawParams) as BuiltinToolParamName[], isDone: true } } @@ -339,7 +344,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, - toolCall: isABuiltinToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined, + toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId }, }) } @@ -348,7 +353,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE onError({ message: 'Void: Response from model was empty.', fullError: null }) } else { - const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId) + const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId) const toolCallObj = toolCall ? { toolCall } : {} onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj }); } @@ -425,17 +430,7 @@ const anthropicTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] return anthropicTools } -const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => { - const { id, name, input } = toolBlock - if (!isABuiltinToolName(name)) return null - const rawParams: RawToolParamsObj = {} - if (input === null) return null - if (typeof input !== 'object') return null - for (const paramName in builtinTools[name].params) { - rawParams[paramName as BuiltinToolParamName] = (input as any)[paramName] - } - return { id, name, rawParams, doneParams: Object.keys(rawParams) as BuiltinToolParamName[], isDone: true } -} + // ------------ ANTHROPIC ------------ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode, mcpTools }: SendChatParams_Internal) => { @@ -496,7 +491,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag onText({ fullText, fullReasoning, - toolCall: isABuiltinToolName(fullToolName) ? { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined, + toolCall: { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' }, }) } // there are no events for tool_use, it comes in at the end @@ -546,7 +541,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag stream.on('finalMessage', (response) => { const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking') const tools = response.content.filter(c => c.type === 'tool_use') - const toolCall = tools[0] && anthropicToolToRawToolCallObj(tools[0]) + const toolCall = tools[0] && rawToolCallObjOfAnthropicParams(tools[0]) const toolCallObj = toolCall ? { toolCall } : {} onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj }) }) @@ -791,7 +786,7 @@ const sendGeminiChat = async ({ onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, - toolCall: isABuiltinToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined, + toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId }, }) } @@ -800,7 +795,7 @@ const sendGeminiChat = async ({ onError({ message: 'Void: Response from model was empty.', fullError: null }) } else { if (!toolId) toolId = generateUuid() // ids are empty, but other providers might expect an id - const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId) + const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId) const toolCallObj = toolCall ? { toolCall } : {} onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj }); } From 66de84f4e3ffa4776b6decd6f6684aefe72d576f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 22 May 2025 16:46:52 -0700 Subject: [PATCH 75/98] auto approve --- .../contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 82c00afb..753e8387 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1619,7 +1619,7 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) => const approvalType = isABuiltinToolName(toolName) ? approvalTypeOfBuiltinToolName[toolName] : 'mcp-tools' const approvalToggle = approvalType ?
- +
: null return
From 4f4af9b8b9b881e45b1e4c430592a1fc5eb02205 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 22 May 2025 16:58:32 -0700 Subject: [PATCH 76/98] ui --- .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 753e8387..725efd93 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1419,7 +1419,6 @@ const getTitle = (toolMessage: Pick) => { const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected, } - componentParams.info = `${toolMessage.mcpServerName} MCP server` + componentParams.info = `MCP server "${toolMessage.mcpServerName}"` if (toolMessage.type === 'success' || toolMessage.type === 'tool_request') { const { result } = toolMessage @@ -1877,9 +1874,9 @@ const MCPToolWrapper = ({ toolMessage }: WrapperProps) => { Date: Thu, 22 May 2025 17:04:52 -0700 Subject: [PATCH 77/98] mcp errors handling --- src/vs/workbench/contrib/void/browser/chatThreadService.ts | 2 -- src/vs/workbench/contrib/void/common/mcpService.ts | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 26115b88..e50e841f 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -716,8 +716,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { const toolResult_ = toolResult as RawMCPToolCall if (toolResult_.event === 'text') { toolResultStr = toolResult_.text - } else if (toolResult_.event === 'error') { - toolResultStr = `Error: ${toolResult_.text}` } else if (toolResult_.event === 'image') { toolResultStr = `[Image: ${toolResult_.image.mimeType}]` } else if (toolResult_.event === 'audio') { diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index f2406d6e..3920aa3d 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -298,6 +298,9 @@ class MCPService extends Disposable implements IMCPService { public async callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }> { const result = await this.channel.call('callTool', toolData); + if (result.event === 'error') { + throw new Error(`Error: ${result.text}`) + } return { result }; } From 31aa31a18b876ecdb9e8ef5c481442c69a4525d9 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 22 May 2025 17:06:29 -0700 Subject: [PATCH 78/98] call --- .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 725efd93..aabc9120 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1421,13 +1421,13 @@ const getTitle = (toolMessage: Pick Date: Thu, 22 May 2025 17:22:07 -0700 Subject: [PATCH 79/98] better result display --- .../contrib/void/browser/chatThreadService.ts | 13 +------------ .../react/src/sidebar-tsx/SidebarChat.tsx | 13 ++++++------- .../contrib/void/browser/terminalToolService.ts | 16 ++++++++-------- .../workbench/contrib/void/common/mcpService.ts | 16 ++++++++++++++++ 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index e50e841f..eb1a6883 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -713,18 +713,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // For MCP tools, handle the result based on its type else { - const toolResult_ = toolResult as RawMCPToolCall - if (toolResult_.event === 'text') { - toolResultStr = toolResult_.text - } else if (toolResult_.event === 'image') { - toolResultStr = `[Image: ${toolResult_.image.mimeType}]` - } else if (toolResult_.event === 'audio') { - toolResultStr = `[Audio content]` - } else if (toolResult_.event === 'resource') { - toolResultStr = `[Resource content]` - } else { - toolResultStr = JSON.stringify(toolResult) - } + toolResultStr = this._mcpService.stringifyResult(toolResult as RawMCPToolCall) } } catch (error) { const errorMessage = this.toolErrMsgs.errWhenStringifying(error) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index aabc9120..fa5a5fc9 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1428,7 +1428,9 @@ const getTitle = (toolMessage: Pick = { toolMessage: Exclude, { type: 'invalid_params' }>, messageIdx: number, threadId: string } const MCPToolWrapper = ({ toolMessage }: WrapperProps) => { const accessor = useAccessor() - const commandService = accessor.get('ICommandService') + const mcpService = accessor.get('IMCPService') const title = getTitle(toolMessage) const desc1 = toolMessage.name @@ -1870,14 +1872,11 @@ const MCPToolWrapper = ({ toolMessage }: WrapperProps) => { if (toolMessage.type === 'success' || toolMessage.type === 'tool_request') { const { result } = toolMessage + const resultStr = result ? mcpService.stringifyResult(result) : 'null' componentParams.children = disposables.forEach(d => d.dispose())) + + + // read result if timed out, since we didn't get it (could clean this code up but it's ok) + if (resolveReason?.type === 'timeout') { + const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId + result = await this.readTerminal(terminalId) + } + if (!isPersistent) { interrupt() } if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.') - // read result if timed out, since we didn't get it (could clean this code up but it's ok) - if (resolveReason.type === 'timeout') { - const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId - result = await this.readTerminal(terminalId) - } - - - if (!isPersistent) result = `$ ${command}\n${result}` result = removeAnsiEscapeCodes(result) // trim diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 3920aa3d..03785e8e 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -36,6 +36,7 @@ export interface IMCPService { getMCPTools(): InternalToolInfo[] | undefined; callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }>; + stringifyResult(result: RawMCPToolCall): string } export const IMCPService = createDecorator('mcpConfigService'); @@ -286,6 +287,21 @@ class MCPService extends Disposable implements IMCPService { }) } + stringifyResult(result: RawMCPToolCall): string { + let toolResultStr: string + if (result.event === 'text') { + toolResultStr = result.text + } else if (result.event === 'image') { + toolResultStr = `[Image: ${result.image.mimeType}]` + } else if (result.event === 'audio') { + toolResultStr = `[Audio content]` + } else if (result.event === 'resource') { + toolResultStr = `[Resource content]` + } else { + toolResultStr = JSON.stringify(result) + } + return toolResultStr + } // toggle MCP server and update isOn in void settings public async toggleServerIsOn(serverName: string, isOn: boolean): Promise { From f0f536bc2d0ab623f5e3e4b43e0b179b354a6529 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 23 May 2025 01:45:21 -0700 Subject: [PATCH 80/98] mcp ui --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 8 +++++++- .../void/browser/react/src/void-settings-tsx/Settings.tsx | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index fa5a5fc9..26b12cc8 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1868,7 +1868,13 @@ const MCPToolWrapper = ({ toolMessage }: WrapperProps) => { const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected, } - componentParams.info = `MCP server "${toolMessage.mcpServerName}"` + const paramsStr = JSON.stringify(params, null, 2) + componentParams.desc2 = + + componentParams.info = !toolMessage.mcpServerName ? 'MCP tool not found' : undefined + + // Add copy inputs button in desc2 + if (toolMessage.type === 'success' || toolMessage.type === 'tool_request') { const { result } = toolMessage diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 5f6c632b..315cea60 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -943,7 +943,10 @@ const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer {tool.name} From 5a34feb328a211a6e7b20f1da4e1f5bcadafae1c Mon Sep 17 00:00:00 2001 From: William Viana Date: Fri, 23 May 2025 12:16:01 -0500 Subject: [PATCH 81/98] chore: add grok mini and fast models to dropdown options --- src/vs/workbench/contrib/void/common/modelCapabilities.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 0b44d5aa..01b29a4f 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -86,6 +86,9 @@ export const defaultModelsOfProvider = { xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1 'grok-2', 'grok-3', + 'grok-3-mini', + 'grok-3-fast', + 'grok-3-mini-fast' ], gemini: [ // https://ai.google.dev/gemini-api/docs/models/gemini 'gemini-2.5-pro-exp-03-25', From 31cde552a0faa7eaf9e3dd884dad38d7eedb42ee Mon Sep 17 00:00:00 2001 From: Animesh Gosain <126562373+animeshlego5@users.noreply.github.com> Date: Mon, 19 May 2025 12:35:11 +0530 Subject: [PATCH 82/98] Local Changes --- src/vs/platform/terminal/node/terminalProcess.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 2ecdba35..ed74ee2f 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -170,7 +170,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess cols, rows, useConpty, - useConptyDll, + ...(useConptyDll ? { useConptyDll } : {}) as IPtyForkOptions, // This option will force conpty to not redraw the whole viewport on launch conptyInheritCursor: useConpty && !!shellLaunchConfig.initialText }; @@ -392,7 +392,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess return; } // Don't throttle when using conpty.dll as it seems to have been fixed in later versions - if (this._ptyOptions.useConptyDll) { + if ((this._ptyOptions as any).useConptyDll) { return; } // Use a loop to ensure multiple calls in a single interval space out From c185807e6084de014be63020debed476007d3930 Mon Sep 17 00:00:00 2001 From: Animesh Gosain <126562373+animeshlego5@users.noreply.github.com> Date: Sat, 24 May 2025 17:44:32 +0530 Subject: [PATCH 83/98] feat: Add toggle to disable AI system message Refactor role mapping logic role Fix keymap for Void: Quick Edit to resolve conflict The current keymap causes a conflict between 'Void: Quick Edit' and 'Clear Console'/'Clear Terminal'. Using this _when_ expression, we can resolve the conflict without affecting functionality. * Add the when expression `editorFocus && !terminalFocus` to the keybinding for 'Void: Quick Edit' `void.ctrlKAction`. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/voideditor/void?shareId=XXXX-XXXX-XXXX-XXXX). feat: add Devstral model mapping + fix quick-edit keybinding type claude 4 feat: Add to .gitignore remove whitespace changed gitignore changed gitignore again changed gitignore again --- .../platform/terminal/node/terminalProcess.ts | 4 ++-- .../browser/convertToLLMMessageService.ts | 22 ++++++++++--------- .../react/src/void-settings-tsx/Settings.tsx | 20 +++++++++++++++++ .../void/common/voidSettingsService.ts | 5 ++++- .../contrib/void/common/voidSettingsTypes.ts | 2 ++ 5 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index ed74ee2f..2ecdba35 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -170,7 +170,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess cols, rows, useConpty, - ...(useConptyDll ? { useConptyDll } : {}) as IPtyForkOptions, + useConptyDll, // This option will force conpty to not redraw the whole viewport on launch conptyInheritCursor: useConpty && !!shellLaunchConfig.initialText }; @@ -392,7 +392,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess return; } // Don't throttle when using conpty.dll as it seems to have been fixed in later versions - if ((this._ptyOptions as any).useConptyDll) { + if (this._ptyOptions.useConptyDll) { return; } // Use a loop to ensure multiple calls in a single interval space out diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 01fd58aa..795d9b58 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -250,8 +250,8 @@ const prepareOpenAIOrAnthropicMessages = ({ reservedOutputTokenSpace, }: { messages: SimpleLLMMessage[], - systemMessage: string, - aiInstructions: string, + systemMessage: string | undefined, + aiInstructions: string | undefined, supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean, @@ -491,8 +491,8 @@ const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => { const prepareMessages = (params: { messages: SimpleLLMMessage[], - systemMessage: string, - aiInstructions: string, + systemMessage: string | undefined, + aiInstructions: string | undefined, supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined, supportsAnthropicReasoning: boolean, @@ -520,7 +520,7 @@ const prepareMessages = (params: { export interface IConvertToLLMMessageService { readonly _serviceBrand: undefined; prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined } - prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }> + prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null, explicitlyDisableSystemMessage?: boolean, explicitlyProvideAiInstructions?: string, }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }> prepareFIMMessage(opts: { messages: LLMFIMMessage, }): { prefix: string, suffix: string, stopTokens: string[] } } @@ -662,7 +662,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess }) return { messages, separateSystemMessage }; } - prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => { + prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection, explicitlyDisableSystemMessage, explicitlyProvideAiInstructions, }) => { if (modelSelection === null) return { messages: [], separateSystemMessage: undefined } const { overridesOfModel } = this.voidSettingsService.state @@ -673,20 +673,22 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess contextWindow, supportsSystemMessage, } = getModelCapabilities(providerName, modelName, overridesOfModel) - const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat) + + const systemMessageFromGenerator = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat) const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName] // Get combined AI instructions - const aiInstructions = this._getCombinedAIInstructions(); - + const aiInstructions = explicitlyProvideAiInstructions || this._getCombinedAIInstructions(); + const globalDisableSystemMessageSetting = this.voidSettingsService.state.globalSettings.disableSystemMessage; + const finalSystemMessageForPrepareMessages = (explicitlyDisableSystemMessage || globalDisableSystemMessageSetting) ? undefined : systemMessageFromGenerator; const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions, overridesOfModel) const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel }) const llmMessages = this._chatMessagesToSimpleMessages(chatMessages) const { messages, separateSystemMessage } = prepareMessages({ messages: llmMessages, - systemMessage, + systemMessage: finalSystemMessageForPrepareMessages, aiInstructions, supportsSystemMessage, specialToolFormat, diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index a133ff49..0d23f5bb 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -5,6 +5,7 @@ import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; // Added useRef import just in case it was missed, though likely already present import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js' +import { IVoidSettingsService, DISABLE_SYSTEM_MESSAGE_SETTING_ID } from '../../../../common/voidSettingsService.js'; import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js' import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js' @@ -1242,6 +1243,25 @@ Alternatively, place a \`.voidrules\` file in the root of your workspace. + {/* --- Disable System Message Toggle --- */} +
+ +
+ { + voidSettingsService.setGlobalSetting('disableSystemMessage', newValue); + }} + /> + + {settingsState.globalSettings.disableSystemMessage ? 'Minimal system messages sent' : 'Full system messages sent'} + +
+
+
When enabled, Void will send a minimal system message to the model to reduce token usage and improve model performance for certain tasks. +
+
diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 73c9eceb..5c3ae2d6 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -13,7 +13,8 @@ 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 } from './voidSettingsTypes.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, } from './voidSettingsTypes.js'; +export const DISABLE_SYSTEM_MESSAGE_SETTING_ID = 'void.disableSystemMessage'; // name is the name in the dropdown @@ -272,6 +273,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { // autoapprove is now an obj not a boolean (1.2.5) if (typeof readS.globalSettings.autoApprove === 'boolean') readS.globalSettings.autoApprove = {} + + if (readS.globalSettings.disableSystemMessage === undefined) readS.globalSettings.disableSystemMessage = false; } catch (e) { readS = defaultState() diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index a911dfe6..f63abec5 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -434,6 +434,7 @@ export type GlobalSettings = { showInlineSuggestions: boolean; includeToolLintErrors: boolean; isOnboardingComplete: boolean; + disableSystemMessage: boolean; } export const defaultGlobalSettings: GlobalSettings = { @@ -447,6 +448,7 @@ export const defaultGlobalSettings: GlobalSettings = { showInlineSuggestions: true, includeToolLintErrors: true, isOnboardingComplete: false, + disableSystemMessage: false, } export type GlobalSettingName = keyof GlobalSettings From 826571b410691d9db838f4ffe83308c64bcd8610 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 24 May 2025 17:09:35 -0700 Subject: [PATCH 84/98] remove ToolName from extractGrammar --- .../electron-main/llmMessage/extractGrammar.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts index 3f1a4224..66e16791 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts @@ -7,7 +7,7 @@ import { generateUuid } from '../../../../../base/common/uuid.js' import { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js' import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js' import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js' -import { BuiltinToolName, BuiltinToolParamName } from '../../common/toolsServiceTypes.js' +import { ToolName, ToolParamName } from '../../common/toolsServiceTypes.js' import { ChatMode } from '../../common/voidSettingsTypes.js' @@ -165,15 +165,15 @@ const findIndexOfAny = (fullText: string, matches: string[]) => { type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined } -const parseXMLPrefixToToolCall = (toolName: BuiltinToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => { +const parseXMLPrefixToToolCall = (toolName: T, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => { const paramsObj: RawToolParamsObj = {} - const doneParams: BuiltinToolParamName[] = [] + const doneParams: ToolParamName[] = [] let isDone = false const getAnswer = (): RawToolCallObj => { // trim off all whitespace at and before first \n and after last \n for each param for (const p in paramsObj) { - const paramName = p as BuiltinToolParamName + const paramName = p as ToolParamName const orig = paramsObj[paramName] if (orig === undefined) continue paramsObj[paramName] = trimBeforeAndAfterNewLines(orig) @@ -203,16 +203,16 @@ const parseXMLPrefixToToolCall = (toolName: BuiltinToolName, toolId: string, str const pm = new SurroundingsRemover(str) - const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as BuiltinToolParamName[] + const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName[] if (allowedParams.length === 0) return getAnswer() - let latestMatchedOpenParam: null | BuiltinToolParamName = null + let latestMatchedOpenParam: null | ToolParamName = null let n = 0 while (true) { n += 1 if (n > 10) return getAnswer() // just for good measure as this code is early // find the param name opening tag - let matchedOpenParam: null | BuiltinToolParamName = null + let matchedOpenParam: null | ToolParamName = null for (const paramName of allowedParams) { const removed = pm.removeFromStartUntilFullMatch(`<${paramName}>`, true) if (removed) { @@ -282,7 +282,7 @@ export const extractXMLToolsWrapper = ( let trueFullText = '' let latestToolCall: RawToolCallObj | undefined = undefined - let foundOpenTag: { idx: number, toolName: BuiltinToolName } | null = null + let foundOpenTag: { idx: number, toolName: ToolName } | null = null let openToolTagBuffer = '' // the characters we've seen so far that come after a < with no space afterwards, not yet added to fullText let prevFullTextLen = 0 @@ -312,7 +312,7 @@ export const extractXMLToolsWrapper = ( const i = findIndexOfAny(fullText, toolOpenTags) if (i !== null) { const [idx, toolTag] = i - const toolName = toolTag.substring(1, toolTag.length - 1) as BuiltinToolName + const toolName = toolTag.substring(1, toolTag.length - 1) as ToolName // console.log('found ', toolName) foundOpenTag = { idx, toolName } From 0625ce4b404e3d9a49aaeb2dd566f1d3d733dbde Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Tue, 27 May 2025 16:09:50 -0400 Subject: [PATCH 85/98] Updated tool name to add a unique prefix on a per session basis. Created a function that removes that prefix when displaying tools to users --- .../react/src/void-settings-tsx/Settings.tsx | 4 +++- .../contrib/void/electron-main/mcpChannel.ts | 14 +++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 315cea60..c48912bf 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -908,6 +908,8 @@ const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer const voidSettings = useSettingsState() const isOn = voidSettings.mcpUserStateOfName[name]?.isOn + const removeUniquePrefix = (name: string) => name.split('_').slice(1).join('_') + return (
@@ -948,7 +950,7 @@ const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer data-tooltip-content={tool.description || ''} data-tooltip-class-name='void-max-w-[20px]' > - {tool.name} + {removeUniquePrefix(tool.name)} )) ) : ( diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 7e727131..440a2af8 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -175,19 +175,22 @@ export class MCPChannel implements IServerChannel { await client.connect(transport); console.log(`Connected via HTTP to ${serverName}`); const { tools } = await client.listTools() + const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest })) info = { status: isOn ? 'success' : 'offline', - tools: tools, + tools: toolsWithUniqueName, command: server.url.toString(), } } catch (httpErr) { console.warn(`HTTP failed for ${serverName}, trying SSE…`, httpErr); transport = new SSEClientTransport(server.url); await client.connect(transport); + const { tools } = await client.listTools() + const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest })) console.log(`Connected via SSE to ${serverName}`); info = { status: isOn ? 'success' : 'offline', - tools: [], + tools: toolsWithUniqueName, command: server.url.toString(), } } @@ -206,6 +209,7 @@ export class MCPChannel implements IServerChannel { // Get the tools from the server const { tools } = await client.listTools() + const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest })) // Create a full command string for display const fullCommand = `${server.command} ${server.args?.join(' ') || ''}` @@ -213,7 +217,7 @@ export class MCPChannel implements IServerChannel { // Format server object info = { status: isOn ? 'success' : 'offline', - tools: tools, + tools: toolsWithUniqueName, command: fullCommand, } @@ -225,6 +229,10 @@ export class MCPChannel implements IServerChannel { return { _client: client, mcpServerEntryJSON: server, mcpServer: info } } + private _addUniquePrefix(base: string) { + return `${Math.random().toString(36).slice(2, 8)}_${base}`; + } + private async _createClient(serverConfig: MCPConfigFileEntryJSON, serverName: string, isOn = true): Promise { try { const c: ClientInfo = await this._createClientUnsafe(serverConfig, serverName, isOn) From f4557ef6a88fd3367481411a2296afe7901e0a6a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 29 May 2025 03:03:12 -0700 Subject: [PATCH 86/98] opus 4 edits: ---- 1. parse id code { "code": -32602, "name": "McpError" } out and display it to the user 2. deleting an mcp server from the mcp file doesnt work, the server remains 3. ui - non-wrapping tooltips should wrap --- .../react/src/void-settings-tsx/Settings.tsx | 4 +- .../react/src/void-tooltip/VoidTooltip.tsx | 2 + .../contrib/void/common/mcpService.ts | 20 ++++++--- .../contrib/void/electron-main/mcpChannel.ts | 44 ++++++++++++++++++- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index c48912bf..c7855359 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -948,7 +948,7 @@ const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer data-tooltip-id='void-tooltip' data-tooltip-content={tool.description || ''} - data-tooltip-class-name='void-max-w-[20px]' + data-tooltip-class-name='void-max-w-[300px]' > {removeUniquePrefix(tool.name)} @@ -1140,7 +1140,7 @@ export const Settings = () => { className='hover:brightness-110' data-tooltip-id='void-tooltip' data-tooltip-content='We recommend using the largest qwen2.5-coder model you can with Ollama (try qwen2.5-coder:3b).' - data-tooltip-class-name='void-max-w-[20px]' + data-tooltip-class-name='void-max-w-[300px]' > Only works with FIM models.* diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/VoidTooltip.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/VoidTooltip.tsx index ad80dfb5..7128adf4 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/VoidTooltip.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/VoidTooltip.tsx @@ -50,6 +50,8 @@ export const VoidTooltip = () => { padding: 0px 8px; border-radius: 6px; z-index: 999999; + max-width: 300px; + word-wrap: break-word; } #void-tooltip { diff --git a/src/vs/workbench/contrib/void/common/mcpService.ts b/src/vs/workbench/contrib/void/common/mcpService.ts index 03785e8e..c40afb43 100644 --- a/src/vs/workbench/contrib/void/common/mcpService.ts +++ b/src/vs/workbench/contrib/void/common/mcpService.ts @@ -118,11 +118,21 @@ class MCPService extends Disposable implements IMCPService { } private readonly _setMCPServerState = async (serverName: string, newServer: MCPServer | undefined) => { - this.state = { - ...this.state, - mcpServerOfName: { - ...this.state.mcpServerOfName, - ...newServer === undefined ? {} : { [serverName]: newServer, } + if (newServer === undefined) { + // Remove the server from the state + const { [serverName]: removed, ...remainingServers } = this.state.mcpServerOfName; + this.state = { + ...this.state, + mcpServerOfName: remainingServers + } + } else { + // Add or update the server + this.state = { + ...this.state, + mcpServerOfName: { + ...this.state.mcpServerOfName, + [serverName]: newServer + } } } this._onDidChangeState.fire(); diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 440a2af8..68c345d2 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -354,7 +354,49 @@ export class MCPChannel implements IServerChannel { const response = await this._callTool(serverName, toolName, params) return response } catch (err) { - const fullErrorMessage = `❌ Failed to call tool "${toolName}" on server "${serverName}": ${typeof err === 'string' ? err : JSON.stringify(err, null, 2)}` + let errorMessage: string; + + // Check if it's an MCP error with a code + if (err && typeof err === 'object' && 'code' in err) { + const errorCode = (err as any).code; + const errorName = (err as any).name || 'Unknown Error'; + const errorMsg = (err as any).message || ''; + + // Map common JSON-RPC error codes to user-friendly messages + let codeDescription = ''; + switch (errorCode) { + case -32700: + codeDescription = 'Parse Error'; + break; + case -32600: + codeDescription = 'Invalid Request'; + break; + case -32601: + codeDescription = 'Method Not Found'; + break; + case -32602: + codeDescription = 'Invalid Parameters'; + break; + case -32603: + codeDescription = 'Internal Error'; + break; + default: + codeDescription = `Error Code ${errorCode}`; + } + + errorMessage = `${errorName} (${codeDescription})${errorMsg ? ': ' + errorMsg : ''}`; + } else if (err && typeof err === 'object' && 'message' in err) { + // Standard error with message + errorMessage = (err as any).message; + } else if (typeof err === 'string') { + // String error + errorMessage = err; + } else { + // Unknown error format + errorMessage = JSON.stringify(err, null, 2); + } + + const fullErrorMessage = `❌ Failed to call tool "${toolName}" on server "${serverName}": ${errorMessage}`; const errorResponse: MCPToolErrorResponse = { event: 'error', text: fullErrorMessage, From 70d484049cea3ae912d22aea261a6904e411261f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 29 May 2025 03:07:34 -0700 Subject: [PATCH 87/98] rm any casts --- .../contrib/void/electron-main/mcpChannel.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 68c345d2..70972abf 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -355,13 +355,13 @@ export class MCPChannel implements IServerChannel { return response } catch (err) { let errorMessage: string; - + // Check if it's an MCP error with a code if (err && typeof err === 'object' && 'code' in err) { - const errorCode = (err as any).code; - const errorName = (err as any).name || 'Unknown Error'; - const errorMsg = (err as any).message || ''; - + const errorCode = err.code; + const errorName = err.name || 'Unknown Error'; + const errorMsg = err.message || ''; + // Map common JSON-RPC error codes to user-friendly messages let codeDescription = ''; switch (errorCode) { @@ -383,7 +383,7 @@ export class MCPChannel implements IServerChannel { default: codeDescription = `Error Code ${errorCode}`; } - + errorMessage = `${errorName} (${codeDescription})${errorMsg ? ': ' + errorMsg : ''}`; } else if (err && typeof err === 'object' && 'message' in err) { // Standard error with message @@ -395,7 +395,7 @@ export class MCPChannel implements IServerChannel { // Unknown error format errorMessage = JSON.stringify(err, null, 2); } - + const fullErrorMessage = `❌ Failed to call tool "${toolName}" on server "${serverName}": ${errorMessage}`; const errorResponse: MCPToolErrorResponse = { event: 'error', From 232264d5fe85447331c6067525c8c37e1916ad09 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 29 May 2025 03:07:44 -0700 Subject: [PATCH 88/98] + --- src/vs/workbench/contrib/void/electron-main/mcpChannel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts index 70972abf..7e1be86b 100644 --- a/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/mcpChannel.ts @@ -387,7 +387,7 @@ export class MCPChannel implements IServerChannel { errorMessage = `${errorName} (${codeDescription})${errorMsg ? ': ' + errorMsg : ''}`; } else if (err && typeof err === 'object' && 'message' in err) { // Standard error with message - errorMessage = (err as any).message; + errorMessage = err.message; } else if (typeof err === 'string') { // String error errorMessage = err; From 77e562e278b21094db6f1ccf19bca4182c616d99 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 29 May 2025 16:31:40 -0700 Subject: [PATCH 89/98] misc improvements --- .../browser/react/src/void-settings-tsx/Settings.tsx | 10 +++++++++- .../workbench/contrib/void/browser/sidebarActions.ts | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index c7855359..8b2cffbb 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -495,7 +495,15 @@ export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderN {/* X button */}
- {type === 'default' || type === 'autodetected' ? null : } + {type === 'default' || type === 'autodetected' ? null : }
diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 4112745c..ed2b97c9 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -172,7 +172,7 @@ registerAction2(class extends Action2 { const oldUI = await oldThread?.state.mountedInfo?.whenMounted const oldSelns = oldThread?.state.stagingSelections - const oldVal = oldUI?.textAreaRef.current?.value + const oldVal = oldUI?.textAreaRef?.current?.value // open and focus new thread chatThreadsService.openNewThread() From 98e07e1c7fcc0c5fe85145ac9ef7b6a591d3bcc1 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 29 May 2025 17:23:55 -0700 Subject: [PATCH 90/98] misc ui --- .voidrules | 2 + .../void/browser/react/src/util/inputs.tsx | 107 +++++++++++++----- .../react/src/void-settings-tsx/Settings.tsx | 4 +- 3 files changed, 85 insertions(+), 28 deletions(-) diff --git a/.voidrules b/.voidrules index 0c9e6204..59586971 100644 --- a/.voidrules +++ b/.voidrules @@ -8,3 +8,5 @@ Look for services and built-in functions that you might need to use to solve the In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any'. Find the correct type to apply and use it. Do not add or remove semicolons to any of my files. Just go with convention and make the least number of changes. + +Never modify files outside src/vs/workbench/contrib/void without consulting with the user first. diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index a8e411ec..a2239f85 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -21,7 +21,7 @@ import { getBasename, getFolderName } from '../sidebar-tsx/SidebarChat.js'; import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react'; import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js'; import { DiffEditorWidget } from '../../../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js'; -import { extractSearchReplaceBlocks } from '../../../../common/helpers/extractCodeFromResult.js'; +import { extractSearchReplaceBlocks, ExtractedSearchReplaceBlock } from '../../../../common/helpers/extractCodeFromResult.js'; import { IAccessibilitySignalService } from '../../../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { IEditorProgressService } from '../../../../../../../platform/progress/common/progress.js'; import { detectLanguage } from '../../../../common/helpers/languageHelpers.js'; @@ -1844,33 +1844,16 @@ export const VoidButtonBgDarken = ({ children, disabled, onClick, className }: { // return
; // }; -/** - * ToolDiffEditor mounts a native VSCode DiffEditorWidget to show a diff between original and modified code blocks. - * Props: - * - uri: URI of the file (for language detection, etc) - * - searchReplaceBlocks: string in search/replace format (from LLM) - * - language?: string (optional, fallback to 'plaintext') - */ -export const VoidDiffEditor = ({ uri, searchReplaceBlocks, language }: { uri?: any, searchReplaceBlocks: string, language?: string }) => { + + + +const SingleDiffEditor = ({ block, lang }: { block: ExtractedSearchReplaceBlock, lang: string | undefined }) => { const accessor = useAccessor(); const modelService = accessor.get('IModelService'); const instantiationService = accessor.get('IInstantiationService'); const languageService = accessor.get('ILanguageService'); - const contextKeyService = accessor.get('IContextKeyService'); - const codeEditorService = accessor.get('ICodeEditorService'); - // Extract the first block (if present) - const blocks = extractSearchReplaceBlocks(searchReplaceBlocks); - const block = blocks[0] || { orig: '', final: '' }; - - // Use detectLanguage for language detection if not provided - let lang = language; - if (!lang) { - lang = detectLanguage(languageService, { uri: uri ?? null, fileContents: block.orig }); - } - - // Use ILanguageSelection for model creation - const languageSelection = useMemo(() => languageService.createById(lang!), [lang, languageService]); + const languageSelection = useMemo(() => languageService.createById(lang), [lang, languageService]); // Create models for original and modified const originalModel = useMemo(() => @@ -1906,7 +1889,7 @@ export const VoidDiffEditor = ({ uri, searchReplaceBlocks, language }: { uri?: a renderSideBySide: true, minimap: { enabled: false }, lineNumbers: 'off', - scrollbar: { vertical: 'auto', horizontal: 'auto', verticalScrollbarSize: 8, horizontalScrollbarSize: 8 }, + scrollbar: { vertical: 'hidden', horizontal: 'auto', verticalScrollbarSize: 0, horizontalScrollbarSize: 8 }, hover: { enabled: false }, folding: false, selectionHighlight: false, @@ -1916,20 +1899,92 @@ export const VoidDiffEditor = ({ uri, searchReplaceBlocks, language }: { uri?: a overviewRulerBorder: false, glyphMargin: false, stickyScroll: { enabled: false }, + scrollBeyondLastLine: false, + renderGutterMenu: false, + renderIndicators: false, }, { originalEditor: { isSimpleWidget: true }, modifiedEditor: { isSimpleWidget: true } } ); editor.setModel({ original: originalModel, modified: modifiedModel }); - editor.layout(); + + // Calculate the height based on content + const updateHeight = () => { + const contentHeight = Math.max( + originalModel.getLineCount() * 19, // approximate line height + modifiedModel.getLineCount() * 19 + ) + 19 * 2 + 1; // add padding + + // Set reasonable min/max heights + const height = Math.min(Math.max(contentHeight, 100), 300); + if (divRef.current) { + divRef.current.style.height = `${height}px`; + editor.layout(); + } + }; + + updateHeight(); editorRef.current = editor; + + // Update height when content changes + const disposable1 = originalModel.onDidChangeContent(() => updateHeight()); + const disposable2 = modifiedModel.onDidChangeContent(() => updateHeight()); + return () => { + disposable1.dispose(); + disposable2.dispose(); editor.dispose(); editorRef.current = null; }; }, [originalModel, modifiedModel, instantiationService]); return ( -
+
+ ); +}; + + + + + +/** + * ToolDiffEditor mounts a native VSCode DiffEditorWidget to show a diff between original and modified code blocks. + * Props: + * - uri: URI of the file (for language detection, etc) + * - searchReplaceBlocks: string in search/replace format (from LLM) + * - language?: string (optional, fallback to 'plaintext') + */ +export const VoidDiffEditor = ({ uri, searchReplaceBlocks, language }: { uri?: any, searchReplaceBlocks: string, language?: string }) => { + const accessor = useAccessor(); + const languageService = accessor.get('ILanguageService'); + + // Extract all blocks + const blocks = extractSearchReplaceBlocks(searchReplaceBlocks); + + // Use detectLanguage for language detection if not provided + let lang = language; + if (!lang && blocks.length > 0) { + lang = detectLanguage(languageService, { uri: uri ?? null, fileContents: blocks[0].orig }); + } + + // If no blocks, show empty state + if (blocks.length === 0) { + return
No changes found
; + } + + // Display all blocks + return ( +
+ {blocks.map((block, index) => ( +
+ {blocks.length > 1 && ( +
+ Change {index + 1} of {blocks.length} +
+ )} + +
+ ))} +
); }; diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 8b2cffbb..8cef3295 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -456,7 +456,7 @@ export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderN {/* left part is width:full */}
{isNewProviderName ? providerTitle : ''} - {modelName} + {modelName}
{/* right part is anything that fits */} @@ -495,7 +495,7 @@ export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderN {/* X button */}
- {type === 'default' || type === 'autodetected' ? null :
-
When enabled, Void will send a minimal system message to the model to reduce token usage and improve model performance for certain tasks. +
+ {`When enabled, Void will not include anything in the system message except for content you specified in voidrules and AI Instructions.`}
From 29f004a826c3e4b05560b7931614adc7ba6d910e Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 29 May 2025 23:56:55 -0700 Subject: [PATCH 95/98] sys --- .../void/browser/convertToLLMMessageService.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index ca3c8c48..8ebd5e76 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -250,8 +250,8 @@ const prepareOpenAIOrAnthropicMessages = ({ reservedOutputTokenSpace, }: { messages: SimpleLLMMessage[], - systemMessage: string | undefined, - aiInstructions: string | undefined, + systemMessage: string, + aiInstructions: string, supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean, @@ -491,8 +491,8 @@ const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => { const prepareMessages = (params: { messages: SimpleLLMMessage[], - systemMessage: string | undefined, - aiInstructions: string | undefined, + systemMessage: string, + aiInstructions: string, supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined, supportsAnthropicReasoning: boolean, @@ -674,21 +674,21 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess supportsSystemMessage, } = getModelCapabilities(providerName, modelName, overridesOfModel) - const systemMessageFromGenerator = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat) + const { disableSystemMessage } = this.voidSettingsService.state.globalSettings; + const fullSystemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat) + const systemMessage = disableSystemMessage ? '' : fullSystemMessage; const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName] // Get combined AI instructions const aiInstructions = this._getCombinedAIInstructions(); - const globalDisableSystemMessageSetting = this.voidSettingsService.state.globalSettings.disableSystemMessage; - const finalSystemMessageForPrepareMessages = globalDisableSystemMessageSetting ? undefined : systemMessageFromGenerator; const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions, overridesOfModel) const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel }) const llmMessages = this._chatMessagesToSimpleMessages(chatMessages) const { messages, separateSystemMessage } = prepareMessages({ messages: llmMessages, - systemMessage: finalSystemMessageForPrepareMessages, + systemMessage, aiInstructions, supportsSystemMessage, specialToolFormat, From 0a6be740e12abf2a8d4bbf017a3648e1e74675d3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 29 May 2025 23:58:21 -0700 Subject: [PATCH 96/98] simplify --- .../contrib/void/browser/convertToLLMMessageService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 471e0c24..94545c0d 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -667,7 +667,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess }) return { messages, separateSystemMessage }; } - prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection, }) => { + prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => { if (modelSelection === null) return { messages: [], separateSystemMessage: undefined } const { overridesOfModel } = this.voidSettingsService.state From 83c4a056420389c159a653dd241fa9da444e82fb Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 30 May 2025 00:15:39 -0700 Subject: [PATCH 97/98] toggle --- .../react/src/void-settings-tsx/Settings.tsx | 154 +++++++++--------- 1 file changed, 79 insertions(+), 75 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index d0f2529d..52e00dc2 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -919,66 +919,72 @@ const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer const removeUniquePrefix = (name: string) => name.split('_').slice(1).join('_') return ( -
-
- {/* Status indicator */} -
+
+ {/* Left side - status and name */} +
+ {/* Status indicator */} +
- `}>
- - {/* Server name */} -
{name}
- - {/* Power toggle switch */} -
- mcpService.toggleServerIsOn(name, !isOn)} - /> + {/* Server name */} +
{name}
+ + {/* Right side - power toggle switch */} + mcpService.toggleServerIsOn(name, !isOn)} + />
{/* Tools section */} -
-
- {isOn && (server.tools ?? []).length > 0 ? ( - (server.tools ?? []).map((tool: { name: string; description?: string }) => ( - +
+ {(server.tools ?? []).length > 0 ? ( + (server.tools ?? []).map((tool: { name: string; description?: string }) => ( + - {removeUniquePrefix(tool.name)} - - )) - ) : ( - No tools available - )} + data-tooltip-id='void-tooltip' + data-tooltip-content={tool.description || ''} + data-tooltip-class-name='void-max-w-[300px]' + > + {removeUniquePrefix(tool.name)} + + )) + ) : ( + No tools available + )} +
-
+ )} {/* Command badge */} {isOn && server.command && ( -
-
Command:
-
+
+
Command:
+
{server.command}
)} {/* Error message if present */} - {server.error && ()} + {server.error && ( +
+ +
+ )}
); }; @@ -989,27 +995,25 @@ const MCPServersList = () => { let content: React.ReactNode if (mcpServiceState.error) { - content =
+ content =
{mcpServiceState.error}
} else { const entries = Object.entries(mcpServiceState.mcpServerOfName) if (entries.length === 0) { - content =
+ content =
No servers found
} else { content = entries.map(([name, server]) => ( -
- -
+ )) } } - return content + return
{content}
}; export const Settings = () => { @@ -1359,44 +1363,44 @@ Alternatively, place a \`.voidrules\` file in the root of your workspace. {/* --- Disable System Message Toggle --- */} -
- -
- { - voidSettingsService.setGlobalSetting('disableSystemMessage', newValue); - }} - /> - - {settingsState.globalSettings.disableSystemMessage ? 'Minimal system messages sent' : 'Full system messages sent'} - -
+
+ +
+ { + voidSettingsService.setGlobalSetting('disableSystemMessage', newValue); + }} + /> + + {settingsState.globalSettings.disableSystemMessage ? 'Disable system message' : 'Disable system message'} + +
-
- {`When enabled, Void will not include anything in the system message except for content you specified in voidrules and AI Instructions.`} -
-
+
+ {`When disabled, Void will not include anything in the system message except for content you specified above.`} +
+
-
+

MCP

-
- { await mcpService.revealMCPConfigFile() }}> +
+ { await mcpService.revealMCPConfigFile() }}> Add MCP Server
-
- - - + + + +
From 27c40d84288dd3d08de213e26b0208cf74e4d938 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 30 May 2025 00:28:22 -0700 Subject: [PATCH 98/98] key --- src/vs/workbench/contrib/void/common/voidSettingsTypes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 55c4f81e..778887a8 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -155,7 +155,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'mistral' ? 'api-key...' : providerName === 'googleVertex' ? 'AIzaSy...' : providerName === 'microsoftAzure' ? 'key-...' : - '', + providerName === 'awsBedrock' ? 'key-...' : + '', isPasswordField: true, }