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,