From 89ac548ae9b2c8953a5b9b7582ab3f74f2191dcc Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 21 May 2025 23:29:41 -0700 Subject: [PATCH] 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