2025-07-25 17:46:55 +00:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-09-18 15:23:24 -04:00
import { detectIde , type IdeInfo } from '../ide/detect-ide.js' ;
2025-09-12 11:44:24 -04:00
import { ideContextStore } from './ideContext.js' ;
2025-08-06 17:36:05 +00:00
import {
2025-09-12 11:44:24 -04:00
IdeContextNotificationSchema ,
2025-08-06 17:36:05 +00:00
IdeDiffAcceptedNotificationSchema ,
IdeDiffClosedNotificationSchema ,
2025-09-12 11:44:24 -04:00
IdeDiffRejectedNotificationSchema ,
} from './types.js' ;
2025-08-25 11:39:57 -07:00
import { getIdeProcessInfo } from './process-utils.js' ;
2025-07-25 17:46:55 +00:00
import { Client } from '@modelcontextprotocol/sdk/client/index.js' ;
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' ;
2025-08-21 15:00:05 -07:00
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' ;
2025-09-12 11:44:24 -04:00
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js' ;
2025-09-11 16:17:57 -04:00
import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js' ;
2025-09-16 18:38:17 -04:00
import { IDE_REQUEST_TIMEOUT_MS } from './constants.js' ;
2025-10-21 16:35:22 -04:00
import { debugLogger } from '../utils/debugLogger.js' ;
2026-02-02 19:42:29 -05:00
import {
getConnectionConfigFromFile ,
getIdeServerHost ,
getPortFromEnv ,
getStdioConfigFromEnv ,
validateWorkspacePath ,
createProxyAwareFetch ,
type StdioConfig ,
} from './ide-connection-utils.js' ;
2025-07-25 17:46:55 +00:00
const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2025-10-21 16:35:22 -04:00
debug : ( . . . args : any [ ] ) = > debugLogger . debug ( '[DEBUG] [IDEClient]' , . . . args ) ,
2025-08-07 17:19:31 -04:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2025-12-29 15:46:10 -05:00
error : ( . . . args : any [ ] ) = > debugLogger . error ( '[ERROR] [IDEClient]' , . . . args ) ,
2025-07-25 17:46:55 +00:00
} ;
2025-09-12 11:44:24 -04:00
export type DiffUpdateResult =
| {
status : 'accepted' ;
content? : string ;
}
| {
status : 'rejected' ;
content : undefined ;
} ;
2025-07-25 17:46:55 +00:00
export type IDEConnectionState = {
status : IDEConnectionStatus ;
2025-07-28 16:55:00 -04:00
details? : string ; // User-facing
2025-07-25 17:46:55 +00:00
} ;
export enum IDEConnectionStatus {
Connected = 'connected' ,
Disconnected = 'disconnected' ,
Connecting = 'connecting' ,
}
/ * *
* Manages the connection to and interaction with the IDE server .
* /
export class IdeClient {
2025-09-04 09:32:09 -07:00
private static instancePromise : Promise < IdeClient > | null = null ;
2025-08-05 18:52:58 -04:00
private client : Client | undefined = undefined ;
2025-07-28 16:55:00 -04:00
private state : IDEConnectionState = {
status : IDEConnectionStatus.Disconnected ,
2025-08-05 18:52:58 -04:00
details :
'IDE integration is currently disabled. To enable it, run /ide enable.' ,
2025-07-28 16:55:00 -04:00
} ;
2025-09-18 15:23:24 -04:00
private currentIde : IdeInfo | undefined ;
2025-08-25 11:39:57 -07:00
private ideProcessInfo : { pid : number ; command : string } | undefined ;
2026-02-02 19:42:29 -05:00
2025-08-06 17:36:05 +00:00
private diffResponses = new Map < string , ( result : DiffUpdateResult ) = > void > ( ) ;
2025-08-19 10:24:58 -07:00
private statusListeners = new Set < ( state : IDEConnectionState ) = > void > ( ) ;
2025-09-03 11:44:26 -07:00
private trustChangeListeners = new Set < ( isTrusted : boolean ) = > void > ( ) ;
2025-09-11 16:17:57 -04:00
private availableTools : string [ ] = [ ] ;
2025-09-10 12:21:46 -06:00
/ * *
* A mutex to ensure that only one diff view is open in the IDE at a time .
* This prevents race conditions and UI issues in IDEs like VSCode that
* can ' t handle multiple diff views being opened simultaneously .
* /
private diffMutex = Promise . resolve ( ) ;
2025-07-25 17:46:55 +00:00
2025-08-25 11:39:57 -07:00
private constructor ( ) { }
2025-07-25 21:57:34 -04:00
2025-09-04 09:32:09 -07:00
static getInstance ( ) : Promise < IdeClient > {
if ( ! IdeClient . instancePromise ) {
IdeClient . instancePromise = ( async ( ) = > {
const client = new IdeClient ( ) ;
client . ideProcessInfo = await getIdeProcessInfo ( ) ;
2026-02-02 19:42:29 -05:00
const connectionConfig = client . ideProcessInfo
? await getConnectionConfigFromFile ( client . ideProcessInfo . pid )
: undefined ;
2025-09-21 20:54:18 -04:00
client . currentIde = detectIde (
client . ideProcessInfo ,
2026-02-02 19:42:29 -05:00
connectionConfig ? . ideInfo ,
2025-09-21 20:54:18 -04:00
) ;
2025-09-04 09:32:09 -07:00
return client ;
} ) ( ) ;
2025-07-30 21:26:31 +00:00
}
2025-09-04 09:32:09 -07:00
return IdeClient . instancePromise ;
2025-07-30 21:26:31 +00:00
}
2025-08-19 10:24:58 -07:00
addStatusChangeListener ( listener : ( state : IDEConnectionState ) = > void ) {
this . statusListeners . add ( listener ) ;
}
removeStatusChangeListener ( listener : ( state : IDEConnectionState ) = > void ) {
this . statusListeners . delete ( listener ) ;
}
2025-09-03 11:44:26 -07:00
addTrustChangeListener ( listener : ( isTrusted : boolean ) = > void ) {
this . trustChangeListeners . add ( listener ) ;
}
removeTrustChangeListener ( listener : ( isTrusted : boolean ) = > void ) {
this . trustChangeListeners . delete ( listener ) ;
}
2025-11-18 12:01:16 -05:00
async connect ( options : { logToConsole? : boolean } = { } ) : Promise < void > {
const logError = options . logToConsole ? ? true ;
2025-09-18 15:23:24 -04:00
if ( ! this . currentIde ) {
2025-08-07 17:19:31 -04:00
this . setState (
IDEConnectionStatus . Disconnected ,
2025-11-18 12:01:16 -05:00
` IDE integration is not supported in your current environment. To use this feature, run Gemini CLI in one of these supported IDEs: Antigravity, VS Code, or VS Code forks. ` ,
2025-08-11 15:57:56 -04:00
false ,
2025-08-07 17:19:31 -04:00
) ;
2025-08-05 18:52:58 -04:00
return ;
}
2025-08-11 15:57:56 -04:00
this . setState ( IDEConnectionStatus . Connecting ) ;
2026-02-02 19:42:29 -05:00
const connectionConfig = this . ideProcessInfo
? await getConnectionConfigFromFile ( this . ideProcessInfo . pid )
: undefined ;
const authToken =
connectionConfig ? . authToken ? ? process . env [ 'GEMINI_CLI_IDE_AUTH_TOKEN' ] ;
2025-12-09 14:41:32 -05:00
2025-08-20 14:11:31 -07:00
const workspacePath =
2026-02-02 19:42:29 -05:00
connectionConfig ? . workspacePath ? ?
2025-08-20 14:11:31 -07:00
process . env [ 'GEMINI_CLI_IDE_WORKSPACE_PATH' ] ;
2026-02-02 19:42:29 -05:00
const { isValid , error } = validateWorkspacePath (
2025-08-20 14:11:31 -07:00
workspacePath ,
2025-08-14 20:12:57 +00:00
process . cwd ( ) ,
) ;
if ( ! isValid ) {
2025-11-18 12:01:16 -05:00
this . setState ( IDEConnectionStatus . Disconnected , error , logError ) ;
2025-08-05 18:52:58 -04:00
return ;
}
2026-02-02 19:42:29 -05:00
if ( connectionConfig ) {
if ( connectionConfig . port ) {
2025-08-21 15:00:05 -07:00
const connected = await this . establishHttpConnection (
2026-02-02 19:42:29 -05:00
connectionConfig . port ,
authToken ,
2025-08-21 15:00:05 -07:00
) ;
if ( connected ) {
return ;
}
}
2026-02-02 19:42:29 -05:00
if ( connectionConfig . stdio ) {
2025-08-21 15:00:05 -07:00
const connected = await this . establishStdioConnection (
2026-02-02 19:42:29 -05:00
connectionConfig . stdio ,
2025-08-21 15:00:05 -07:00
) ;
if ( connected ) {
return ;
}
2025-08-14 18:09:19 +00:00
}
2025-08-05 18:52:58 -04:00
}
2026-02-02 19:42:29 -05:00
const portFromEnv = getPortFromEnv ( ) ;
2025-08-14 18:09:19 +00:00
if ( portFromEnv ) {
2026-02-02 19:42:29 -05:00
const connected = await this . establishHttpConnection (
portFromEnv ,
authToken ,
) ;
2025-08-21 15:00:05 -07:00
if ( connected ) {
return ;
}
}
2026-02-02 19:42:29 -05:00
const stdioConfigFromEnv = getStdioConfigFromEnv ( ) ;
2025-08-21 15:00:05 -07:00
if ( stdioConfigFromEnv ) {
const connected = await this . establishStdioConnection ( stdioConfigFromEnv ) ;
2025-08-14 18:09:19 +00:00
if ( connected ) {
return ;
}
}
this . setState (
IDEConnectionStatus . Disconnected ,
2025-09-18 15:23:24 -04:00
` Failed to connect to IDE companion extension in ${ this . currentIde . displayName } . Please ensure the extension is running. To install the extension, run /ide install. ` ,
2025-11-18 12:01:16 -05:00
logError ,
2025-08-14 18:09:19 +00:00
) ;
2025-08-05 18:52:58 -04:00
}
2025-08-06 17:36:05 +00:00
/ * *
2025-09-12 11:44:24 -04:00
* Opens a diff view in the IDE , allowing the user to review and accept or
* reject changes .
2025-08-06 17:36:05 +00:00
*
2025-09-12 11:44:24 -04:00
* This method sends a request to the IDE to display a diff between the
* current content of a file and the new content provided . It then waits for
* a notification from the IDE indicating that the user has either accepted
* ( potentially with manual edits ) or rejected the diff .
*
* A mutex ensures that only one diff view can be open at a time to prevent
* race conditions .
*
* @param filePath The absolute path to the file to be diffed .
* @param newContent The proposed new content for the file .
* @returns A promise that resolves with a ` DiffUpdateResult ` , indicating
* whether the diff was 'accepted' or 'rejected' and including the final
* content if accepted .
2025-08-06 17:36:05 +00:00
* /
async openDiff (
filePath : string ,
2025-09-12 11:44:24 -04:00
newContent : string ,
2025-08-06 17:36:05 +00:00
) : Promise < DiffUpdateResult > {
2025-09-10 12:21:46 -06:00
const release = await this . acquireMutex ( ) ;
const promise = new Promise < DiffUpdateResult > ( ( resolve , reject ) = > {
if ( ! this . client ) {
// The promise will be rejected, and the finally block below will release the mutex.
return reject ( new Error ( 'IDE client is not connected.' ) ) ;
}
2025-08-06 17:36:05 +00:00
this . diffResponses . set ( filePath , resolve ) ;
this . client
2025-09-16 18:38:17 -04:00
. request (
{
method : 'tools/call' ,
params : {
name : ` openDiff ` ,
arguments : {
filePath ,
newContent ,
} ,
} ,
2025-08-06 17:36:05 +00:00
} ,
2025-09-16 18:38:17 -04:00
CallToolResultSchema ,
{ timeout : IDE_REQUEST_TIMEOUT_MS } ,
)
. then ( ( parsedResultData ) = > {
if ( parsedResultData . isError ) {
const textPart = parsedResultData . content . find (
2025-09-12 11:44:24 -04:00
( part ) = > part . type === 'text' ,
) ;
const errorMessage =
textPart ? . text ? ? ` Tool 'openDiff' reported an error. ` ;
logger . debug (
2025-09-16 18:38:17 -04:00
` Request for openDiff ${ filePath } failed with isError: ` ,
2025-09-12 11:44:24 -04:00
errorMessage ,
) ;
this . diffResponses . delete ( filePath ) ;
reject ( new Error ( errorMessage ) ) ;
}
} )
2025-08-06 17:36:05 +00:00
. catch ( ( err ) = > {
2025-09-16 18:38:17 -04:00
logger . debug ( ` Request for openDiff ${ filePath } failed: ` , err ) ;
2025-09-10 12:21:46 -06:00
this . diffResponses . delete ( filePath ) ;
2025-08-06 17:36:05 +00:00
reject ( err ) ;
} ) ;
} ) ;
2025-09-10 12:21:46 -06:00
// Ensure the mutex is released only after the diff interaction is complete.
2025-12-05 16:12:49 -08:00
// eslint-disable-next-line @typescript-eslint/no-floating-promises
2025-09-10 12:21:46 -06:00
promise . finally ( release ) ;
return promise ;
}
/ * *
* Acquires a lock to ensure sequential execution of critical sections .
*
* This method implements a promise - based mutex . It works by chaining promises .
* Each call to ` acquireMutex ` gets the current ` diffMutex ` promise . It then
* creates a * new * promise ( ` newMutex ` ) that will be resolved when the caller
* invokes the returned ` release ` function . The ` diffMutex ` is immediately
* updated to this ` newMutex ` .
*
* The method returns a promise that resolves with the ` release ` function only
* * after * the * previous * ` diffMutex ` promise has resolved . This creates a
* queue where each subsequent operation must wait for the previous one to release
* the lock .
*
* @returns A promise that resolves to a function that must be called to
* release the lock .
* /
private acquireMutex ( ) : Promise < ( ) = > void > {
let release : ( ) = > void ;
const newMutex = new Promise < void > ( ( resolve ) = > {
release = resolve ;
} ) ;
const oldMutex = this . diffMutex ;
this . diffMutex = newMutex ;
return oldMutex . then ( ( ) = > release ) ;
2025-08-06 17:36:05 +00:00
}
2025-09-03 15:53:57 -05:00
async closeDiff (
filePath : string ,
options ? : { suppressNotification? : boolean } ,
) : Promise < string | undefined > {
2025-08-06 17:36:05 +00:00
try {
2025-09-16 18:38:17 -04:00
if ( ! this . client ) {
2025-09-12 11:44:24 -04:00
return undefined ;
}
2025-09-16 18:38:17 -04:00
const resultData = await this . client . request (
{
method : 'tools/call' ,
params : {
name : ` closeDiff ` ,
arguments : {
filePath ,
suppressNotification : options?.suppressNotification ,
} ,
} ,
} ,
CallToolResultSchema ,
{ timeout : IDE_REQUEST_TIMEOUT_MS } ,
) ;
2025-09-12 11:44:24 -04:00
2025-09-16 18:38:17 -04:00
if ( ! resultData ) {
2025-09-12 11:44:24 -04:00
return undefined ;
}
2025-09-16 18:38:17 -04:00
if ( resultData . isError ) {
const textPart = resultData . content . find (
2025-09-12 11:44:24 -04:00
( part ) = > part . type === 'text' ,
) ;
const errorMessage =
textPart ? . text ? ? ` Tool 'closeDiff' reported an error. ` ;
logger . debug (
2025-09-16 18:38:17 -04:00
` Request for closeDiff ${ filePath } failed with isError: ` ,
2025-09-12 11:44:24 -04:00
errorMessage ,
) ;
return undefined ;
}
2025-09-16 18:38:17 -04:00
const textPart = resultData . content . find ( ( part ) = > part . type === 'text' ) ;
2025-09-12 11:44:24 -04:00
if ( textPart ? . text ) {
try {
const parsedJson = JSON . parse ( textPart . text ) ;
if ( parsedJson && typeof parsedJson . content === 'string' ) {
return parsedJson . content ;
}
if ( parsedJson && parsedJson . content === null ) {
return undefined ;
}
} catch ( _e ) {
logger . debug (
` Invalid JSON in closeDiff response for ${ filePath } : ` ,
textPart . text ,
) ;
}
2025-08-06 17:36:05 +00:00
}
} catch ( err ) {
2025-09-16 18:38:17 -04:00
logger . debug ( ` Request for closeDiff ${ filePath } failed: ` , err ) ;
2025-08-06 17:36:05 +00:00
}
2025-09-12 11:44:24 -04:00
return undefined ;
2025-08-06 17:36:05 +00:00
}
// Closes the diff. Instead of waiting for a notification,
// manually resolves the diff resolver as the desired outcome.
async resolveDiffFromCli ( filePath : string , outcome : 'accepted' | 'rejected' ) {
const resolver = this . diffResponses . get ( filePath ) ;
2025-09-03 15:53:57 -05:00
const content = await this . closeDiff ( filePath , {
// Suppress notification to avoid race where closing the diff rejects the
// request.
suppressNotification : true ,
} ) ;
2025-08-06 17:36:05 +00:00
if ( resolver ) {
if ( outcome === 'accepted' ) {
resolver ( { status : 'accepted' , content } ) ;
} else {
resolver ( { status : 'rejected' , content : undefined } ) ;
}
this . diffResponses . delete ( filePath ) ;
}
}
2025-08-08 15:38:30 +00:00
async disconnect() {
if ( this . state . status === IDEConnectionStatus . Disconnected ) {
return ;
}
for ( const filePath of this . diffResponses . keys ( ) ) {
await this . closeDiff ( filePath ) ;
}
this . diffResponses . clear ( ) ;
2025-08-05 18:52:58 -04:00
this . setState (
IDEConnectionStatus . Disconnected ,
'IDE integration disabled. To enable it again, run /ide enable.' ,
) ;
2025-12-05 16:12:49 -08:00
// eslint-disable-next-line @typescript-eslint/no-floating-promises
2025-08-05 18:52:58 -04:00
this . client ? . close ( ) ;
}
2025-09-18 15:23:24 -04:00
getCurrentIde ( ) : IdeInfo | undefined {
2025-07-30 21:26:31 +00:00
return this . currentIde ;
}
2025-07-28 16:55:00 -04:00
getConnectionStatus ( ) : IDEConnectionState {
return this . state ;
}
2025-08-05 18:52:58 -04:00
getDetectedIdeDisplayName ( ) : string | undefined {
2025-09-18 15:23:24 -04:00
return this . currentIde ? . displayName ;
2025-08-05 18:52:58 -04:00
}
2025-09-11 16:17:57 -04:00
isDiffingEnabled ( ) : boolean {
return (
! ! this . client &&
this . state . status === IDEConnectionStatus . Connected &&
this . availableTools . includes ( 'openDiff' ) &&
this . availableTools . includes ( 'closeDiff' )
) ;
}
private async discoverTools ( ) : Promise < void > {
if ( ! this . client ) {
return ;
}
try {
logger . debug ( 'Discovering tools from IDE...' ) ;
const response = await this . client . request (
{ method : 'tools/list' , params : { } } ,
ListToolsResultSchema ,
) ;
// Map the array of tool objects to an array of tool names (strings)
this . availableTools = response . tools . map ( ( tool ) = > tool . name ) ;
if ( this . availableTools . length > 0 ) {
logger . debug (
` Discovered ${ this . availableTools . length } tools from IDE: ${ this . availableTools . join ( ', ' ) } ` ,
) ;
} else {
logger . debug (
'IDE supports tool discovery, but no tools are available.' ,
) ;
}
} catch ( error ) {
// It's okay if this fails, the IDE might not support it.
// Don't log an error if the method is not found, which is a common case.
if (
error instanceof Error &&
! error . message ? . includes ( 'Method not found' )
) {
logger . error ( ` Error discovering tools from IDE: ${ error . message } ` ) ;
} else {
logger . debug ( 'IDE does not support tool discovery.' ) ;
}
this . availableTools = [ ] ;
}
}
2025-08-07 17:19:31 -04:00
private setState (
status : IDEConnectionStatus ,
details? : string ,
logToConsole = false ,
) {
2025-08-05 18:52:58 -04:00
const isAlreadyDisconnected =
this . state . status === IDEConnectionStatus . Disconnected &&
status === IDEConnectionStatus . Disconnected ;
2025-08-08 17:48:02 -04:00
// Only update details & log to console if the state wasn't already
// disconnected, so that the first detail message is preserved.
2025-08-05 18:52:58 -04:00
if ( ! isAlreadyDisconnected ) {
this . state = { status , details } ;
2025-08-19 10:24:58 -07:00
for ( const listener of this . statusListeners ) {
listener ( this . state ) ;
}
2025-08-11 12:27:45 -04:00
if ( details ) {
if ( logToConsole ) {
logger . error ( details ) ;
} else {
// We only want to log disconnect messages to debug
// if they are not already being logged to the console.
logger . debug ( details ) ;
}
2025-08-07 17:19:31 -04:00
}
2025-08-08 17:48:02 -04:00
}
if ( status === IDEConnectionStatus . Disconnected ) {
2025-09-11 11:22:20 -07:00
ideContextStore . clear ( ) ;
2025-07-25 17:46:55 +00:00
}
}
2025-07-28 16:55:00 -04:00
private registerClientHandlers() {
if ( ! this . client ) {
2025-07-25 17:46:55 +00:00
return ;
}
2025-07-28 16:55:00 -04:00
this . client . setNotificationHandler (
IdeContextNotificationSchema ,
( notification ) = > {
2025-09-11 11:22:20 -07:00
ideContextStore . set ( notification . params ) ;
2025-09-03 11:44:26 -07:00
const isTrusted = notification . params . workspaceState ? . isTrusted ;
if ( isTrusted !== undefined ) {
for ( const listener of this . trustChangeListeners ) {
listener ( isTrusted ) ;
}
}
2025-07-28 16:55:00 -04:00
} ,
) ;
this . client . onerror = ( _error ) = > {
2025-09-16 18:38:17 -04:00
const errorMessage = _error instanceof Error ? _error . message : ` _error ` ;
2025-08-05 18:52:58 -04:00
this . setState (
IDEConnectionStatus . Disconnected ,
2025-09-16 18:38:17 -04:00
` IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable \ n ${ errorMessage } ` ,
2025-08-07 17:19:31 -04:00
true ,
2025-08-05 18:52:58 -04:00
) ;
2025-07-28 16:55:00 -04:00
} ;
this . client . onclose = ( ) = > {
2025-08-05 18:52:58 -04:00
this . setState (
IDEConnectionStatus . Disconnected ,
2025-09-16 18:38:17 -04:00
` IDE connection closed. To reconnect, run /ide enable. ` ,
2025-08-07 17:19:31 -04:00
true ,
2025-08-05 18:52:58 -04:00
) ;
2025-07-28 16:55:00 -04:00
} ;
2025-08-06 17:36:05 +00:00
this . client . setNotificationHandler (
IdeDiffAcceptedNotificationSchema ,
( notification ) = > {
const { filePath , content } = notification . params ;
const resolver = this . diffResponses . get ( filePath ) ;
if ( resolver ) {
resolver ( { status : 'accepted' , content } ) ;
this . diffResponses . delete ( filePath ) ;
} else {
logger . debug ( ` No resolver found for ${ filePath } ` ) ;
}
} ,
) ;
2025-09-12 11:44:24 -04:00
this . client . setNotificationHandler (
IdeDiffRejectedNotificationSchema ,
( notification ) = > {
const { filePath } = notification . params ;
const resolver = this . diffResponses . get ( filePath ) ;
if ( resolver ) {
resolver ( { status : 'rejected' , content : undefined } ) ;
this . diffResponses . delete ( filePath ) ;
} else {
logger . debug ( ` No resolver found for ${ filePath } ` ) ;
}
} ,
) ;
2025-11-07 22:51:56 +07:00
// For backwards compatibility. Newer extension versions will only send
2025-09-12 11:44:24 -04:00
// IdeDiffRejectedNotificationSchema.
2025-08-06 17:36:05 +00:00
this . client . setNotificationHandler (
IdeDiffClosedNotificationSchema ,
( notification ) = > {
const { filePath } = notification . params ;
const resolver = this . diffResponses . get ( filePath ) ;
if ( resolver ) {
resolver ( { status : 'rejected' , content : undefined } ) ;
this . diffResponses . delete ( filePath ) ;
} else {
logger . debug ( ` No resolver found for ${ filePath } ` ) ;
}
} ,
) ;
2025-07-28 16:55:00 -04:00
}
2026-02-02 19:42:29 -05:00
private async establishHttpConnection (
port : string ,
authToken : string | undefined ,
) : Promise < boolean > {
2025-07-25 21:57:34 -04:00
let transport : StreamableHTTPClientTransport | undefined ;
2025-07-25 17:46:55 +00:00
try {
2026-01-22 20:19:04 +01:00
const ideServerHost = getIdeServerHost ( ) ;
const portNumber = parseInt ( port , 10 ) ;
// validate port to prevent Server-Side Request Forgery (SSRF) vulnerability
if ( isNaN ( portNumber ) || portNumber <= 0 || portNumber > 65535 ) {
return false ;
}
const serverUrl = ` http:// ${ ideServerHost } : ${ portNumber } /mcp ` ;
2025-08-21 15:00:05 -07:00
logger . debug ( 'Attempting to connect to IDE via HTTP SSE' ) ;
2026-01-22 20:19:04 +01:00
logger . debug ( ` Server URL: ${ serverUrl } ` ) ;
2025-07-25 17:46:55 +00:00
this . client = new Client ( {
name : 'streamable-http-client' ,
// TODO(#3487): use the CLI version here.
version : '1.0.0' ,
} ) ;
2026-01-22 20:19:04 +01:00
transport = new StreamableHTTPClientTransport ( new URL ( serverUrl ) , {
2026-02-02 19:42:29 -05:00
fetch : await createProxyAwareFetch ( ideServerHost ) ,
2026-01-22 20:19:04 +01:00
requestInit : {
2026-02-02 19:42:29 -05:00
headers : authToken ? { Authorization : ` Bearer ${ authToken } ` } : { } ,
2025-08-20 08:32:08 +08:00
} ,
2026-01-22 20:19:04 +01:00
} ) ;
2025-07-25 17:46:55 +00:00
await this . client . connect ( transport ) ;
2025-08-05 18:52:58 -04:00
this . registerClientHandlers ( ) ;
2025-09-11 16:17:57 -04:00
await this . discoverTools ( ) ;
2025-07-28 16:55:00 -04:00
this . setState ( IDEConnectionStatus . Connected ) ;
2025-08-14 18:09:19 +00:00
return true ;
2025-08-05 18:52:58 -04:00
} catch ( _error ) {
2025-07-25 21:57:34 -04:00
if ( transport ) {
try {
await transport . close ( ) ;
} catch ( closeError ) {
logger . debug ( 'Failed to close transport:' , closeError ) ;
}
}
2025-08-14 18:09:19 +00:00
return false ;
2025-07-25 17:46:55 +00:00
}
}
2025-08-21 15:00:05 -07:00
private async establishStdioConnection ( {
command ,
args ,
} : StdioConfig ) : Promise < boolean > {
let transport : StdioClientTransport | undefined ;
try {
logger . debug ( 'Attempting to connect to IDE via stdio' ) ;
this . client = new Client ( {
name : 'stdio-client' ,
// TODO(#3487): use the CLI version here.
version : '1.0.0' ,
} ) ;
transport = new StdioClientTransport ( {
command ,
args ,
} ) ;
await this . client . connect ( transport ) ;
this . registerClientHandlers ( ) ;
2025-09-11 16:17:57 -04:00
await this . discoverTools ( ) ;
2025-08-21 15:00:05 -07:00
this . setState ( IDEConnectionStatus . Connected ) ;
return true ;
} catch ( _error ) {
if ( transport ) {
try {
await transport . close ( ) ;
} catch ( closeError ) {
logger . debug ( 'Failed to close transport:' , closeError ) ;
}
}
return false ;
}
}
2025-07-25 17:46:55 +00:00
}