clearer types

This commit is contained in:
Andrew Pareles 2025-05-21 23:29:41 -07:00
parent 57bc32ac9b
commit 89ac548ae9
4 changed files with 91 additions and 109 deletions

View file

@ -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 */}
<div className="mt-1 mx-4">
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto pb-1">
{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 }) => (
<span
key={tool.name}
className="px-2 py-0.5 bg-black/5 dark:bg-white/5 rounded text-xs"

View file

@ -14,14 +14,14 @@ 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, MCPServerObject, MCPToolCallParams, MCPGenericToolResponse } from './mcpServiceTypes.js';
import { MCPServerOfName, MCPConfigFileJSON, MCPAddServerResponse, MCPUpdateServerResponse, MCPDeleteServerResponse, MCPServer, 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 { MCPUserStateOfName } from './voidSettingsTypes.js';
type MCPState = {
type MCPServiceState = {
mcpServerOfName: MCPServerOfName,
error: string | undefined, // global parsing error
}
@ -31,7 +31,7 @@ export interface IMCPService {
revealMCPConfigFile(): Promise<void>;
toggleMCPServer(serverName: string, isOn: boolean): Promise<void>;
readonly state: MCPState; // NOT persisted
readonly state: MCPServiceState; // NOT persisted
onDidChangeState: Event<void>;
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<MCPConfigFileType | null> {
private async _parseMCPConfigFile(): Promise<MCPConfigFileJSON | null> {
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<MCPGenericToolResponse> {

View file

@ -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<string, string>;
}
export interface MCPConfigFileType {
mcpServers: Record<string, MCPConfigFileServerType>;
export interface MCPConfigFileJSON {
mcpServers: Record<string, MCPConfigFileEntryJSON>;
}
// 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<MCPServerObject, 'error'> & { 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<MCPServerSetupParams['onSuccess']>[0]
export type EventMCPServerSetupOnError = Parameters<MCPServerSetupParams['onError']>[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: {

View file

@ -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<MCPServer['status'], 'error'> }
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<MCPGenericToolResponse> {
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