2025-07-25 17:46:55 +00:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-07-28 11:03:22 -04:00
import { ideContext , IdeContextNotificationSchema } from '../ide/ideContext.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' ;
const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2025-07-25 21:57:34 -04:00
debug : ( . . . args : any [ ] ) = > console . debug ( '[DEBUG] [IDEClient]' , . . . args ) ,
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 {
client : Client | undefined = undefined ;
2025-07-28 16:55:00 -04:00
private state : IDEConnectionState = {
status : IDEConnectionStatus.Disconnected ,
} ;
2025-07-25 17:46:55 +00:00
constructor ( ) {
2025-07-28 16:55:00 -04:00
this . init ( ) . catch ( ( err ) = > {
2025-07-25 17:46:55 +00:00
logger . debug ( 'Failed to initialize IdeClient:' , err ) ;
} ) ;
}
2025-07-25 21:57:34 -04:00
2025-07-28 16:55:00 -04:00
getConnectionStatus ( ) : IDEConnectionState {
return this . state ;
}
private setState ( status : IDEConnectionStatus , details? : string ) {
this . state = { status , details } ;
if ( status === IDEConnectionStatus . Disconnected ) {
logger . debug ( 'IDE integration is disconnected. ' , details ) ;
ideContext . clearIdeContext ( ) ;
2025-07-25 17:46:55 +00:00
}
}
2025-07-28 16:55:00 -04:00
private getPortFromEnv ( ) : string | undefined {
const port = process . env [ 'GEMINI_CLI_IDE_SERVER_PORT' ] ;
if ( ! port ) {
this . setState (
IDEConnectionStatus . Disconnected ,
'Gemini CLI Companion extension not found. Install via /ide install and restart the CLI in a fresh terminal window.' ,
) ;
return undefined ;
}
return port ;
}
private validateWorkspacePath ( ) : boolean {
const ideWorkspacePath = process . env [ 'GEMINI_CLI_IDE_WORKSPACE_PATH' ] ;
if ( ! ideWorkspacePath ) {
this . setState (
IDEConnectionStatus . Disconnected ,
'IDE integration requires a single workspace folder to be open in the IDE. Please ensure one folder is open and try again.' ,
2025-07-25 17:46:55 +00:00
) ;
2025-07-28 16:55:00 -04:00
return false ;
}
if ( ideWorkspacePath !== process . cwd ( ) ) {
this . setState (
IDEConnectionStatus . Disconnected ,
` Gemini CLI is running in a different directory ( ${ process . cwd ( ) } ) from the IDE's open workspace ( ${ ideWorkspacePath } ). Please run Gemini CLI in the same directory. ` ,
) ;
return false ;
}
return true ;
}
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 ) = > {
ideContext . setIdeContext ( notification . params ) ;
} ,
) ;
this . client . onerror = ( _error ) = > {
this . setState ( IDEConnectionStatus . Disconnected , 'Client error.' ) ;
} ;
this . client . onclose = ( ) = > {
this . setState ( IDEConnectionStatus . Disconnected , 'Connection closed.' ) ;
} ;
}
private async establishConnection ( port : string ) {
2025-07-25 21:57:34 -04:00
let transport : StreamableHTTPClientTransport | undefined ;
2025-07-25 17:46:55 +00:00
try {
this . client = new Client ( {
name : 'streamable-http-client' ,
// TODO(#3487): use the CLI version here.
version : '1.0.0' ,
} ) ;
2025-07-28 16:55:00 -04:00
2025-07-25 21:57:34 -04:00
transport = new StreamableHTTPClientTransport (
2025-07-28 16:55:00 -04:00
new URL ( ` http://localhost: ${ port } /mcp ` ) ,
2025-07-25 17:46:55 +00:00
) ;
2025-07-28 16:55:00 -04:00
this . registerClientHandlers ( ) ;
2025-07-25 17:46:55 +00:00
await this . client . connect ( transport ) ;
2025-07-25 21:57:34 -04:00
2025-07-28 16:55:00 -04:00
this . setState ( IDEConnectionStatus . Connected ) ;
2025-07-25 17:46:55 +00:00
} catch ( error ) {
2025-07-28 16:55:00 -04:00
this . setState (
IDEConnectionStatus . Disconnected ,
` Failed to connect to IDE server: ${ 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-07-25 17:46:55 +00:00
}
}
2025-07-28 16:55:00 -04:00
async init ( ) : Promise < void > {
if ( this . state . status === IDEConnectionStatus . Connected ) {
return ;
}
this . setState ( IDEConnectionStatus . Connecting ) ;
if ( ! this . validateWorkspacePath ( ) ) {
return ;
}
const port = this . getPortFromEnv ( ) ;
if ( ! port ) {
return ;
}
await this . establishConnection ( port ) ;
}
2025-07-25 17:46:55 +00:00
}