From b18ac4b6c0def5cf5db334ab5a6c0847b1cf0108 Mon Sep 17 00:00:00 2001 From: Joaquin Coromina Date: Thu, 15 May 2025 22:26:11 -0400 Subject: [PATCH] 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() + // } +} +