diff options
Diffstat (limited to 'packages/core/src')
| -rw-r--r-- | packages/core/src/config/config.ts | 8 | ||||
| -rw-r--r-- | packages/core/src/core/client.test.ts | 4 | ||||
| -rw-r--r-- | packages/core/src/core/client.ts | 2 | ||||
| -rw-r--r-- | packages/core/src/ide/ide-client.ts | 100 | ||||
| -rw-r--r-- | packages/core/src/ide/ideContext.test.ts (renamed from packages/core/src/services/ideContext.test.ts) | 0 | ||||
| -rw-r--r-- | packages/core/src/ide/ideContext.ts (renamed from packages/core/src/services/ideContext.ts) | 4 | ||||
| -rw-r--r-- | packages/core/src/index.ts | 5 | ||||
| -rw-r--r-- | packages/core/src/tools/mcp-client.ts | 18 |
8 files changed, 115 insertions, 26 deletions
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 485a56c4..96b6f2cb 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -45,6 +45,7 @@ import { import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js'; import { shouldAttemptBrowserLaunch } from '../utils/browser.js'; import { MCPOAuthConfig } from '../mcp/oauth-provider.js'; +import { IdeClient } from '../ide/ide-client.js'; // Re-export OAuth config type export type { MCPOAuthConfig }; @@ -180,6 +181,7 @@ export interface ConfigParameters { noBrowser?: boolean; summarizeToolOutput?: Record<string, SummarizeToolOutputSettings>; ideMode?: boolean; + ideClient?: IdeClient; } export class Config { @@ -221,6 +223,7 @@ export class Config { private readonly extensionContextFilePaths: string[]; private readonly noBrowser: boolean; private readonly ideMode: boolean; + private readonly ideClient: IdeClient | undefined; private modelSwitchedDuringSession: boolean = false; private readonly maxSessionTurns: number; private readonly listExtensions: boolean; @@ -286,6 +289,7 @@ export class Config { this.noBrowser = params.noBrowser ?? false; this.summarizeToolOutput = params.summarizeToolOutput; this.ideMode = params.ideMode ?? false; + this.ideClient = params.ideClient; if (params.contextFileName) { setGeminiMdFilename(params.contextFileName); @@ -574,6 +578,10 @@ export class Config { return this.ideMode; } + getIdeClient(): IdeClient | undefined { + return this.ideClient; + } + async getGitService(): Promise<GitService> { if (!this.gitService) { this.gitService = new GitService(this.targetDir); diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index 44b19f56..25ea9bc1 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -23,7 +23,7 @@ import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { setSimulate429 } from '../utils/testUtils.js'; import { tokenLimit } from './tokenLimits.js'; -import { ideContext } from '../services/ideContext.js'; +import { ideContext } from '../ide/ideContext.js'; // --- Mocks --- const mockChatCreateFn = vi.fn(); @@ -72,7 +72,7 @@ vi.mock('../telemetry/index.js', () => ({ logApiResponse: vi.fn(), logApiError: vi.fn(), })); -vi.mock('../services/ideContext.js'); +vi.mock('../ide/ideContext.js'); describe('findIndexAfterFraction', () => { const history: Content[] = [ diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 6f482307..77683a45 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -42,7 +42,7 @@ import { import { ProxyAgent, setGlobalDispatcher } from 'undici'; import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js'; import { LoopDetectionService } from '../services/loopDetectionService.js'; -import { ideContext } from '../services/ideContext.js'; +import { ideContext } from '../ide/ideContext.js'; import { logFlashDecidedToContinue } from '../telemetry/loggers.js'; import { FlashDecidedToContinueEvent } from '../telemetry/types.js'; diff --git a/packages/core/src/ide/ide-client.ts b/packages/core/src/ide/ide-client.ts new file mode 100644 index 00000000..eeed60b2 --- /dev/null +++ b/packages/core/src/ide/ide-client.ts @@ -0,0 +1,100 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ideContext, OpenFilesNotificationSchema } from '../ide/ideContext.js'; +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 + debug: (...args: any[]) => + console.debug('[DEBUG] [ImportProcessor]', ...args), +}; + +export type IDEConnectionState = { + status: IDEConnectionStatus; + details?: string; +}; + +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; + connectionStatus: IDEConnectionStatus = IDEConnectionStatus.Disconnected; + + constructor() { + this.connectToMcpServer().catch((err) => { + logger.debug('Failed to initialize IdeClient:', err); + }); + } + getConnectionStatus(): { + status: IDEConnectionStatus; + details?: string; + } { + let details: string | undefined; + if (this.connectionStatus === IDEConnectionStatus.Disconnected) { + if (!process.env['GEMINI_CLI_IDE_SERVER_PORT']) { + details = 'GEMINI_CLI_IDE_SERVER_PORT environment variable is not set.'; + } + } + return { + status: this.connectionStatus, + details, + }; + } + + async connectToMcpServer(): Promise<void> { + this.connectionStatus = IDEConnectionStatus.Connecting; + const idePort = process.env['GEMINI_CLI_IDE_SERVER_PORT']; + if (!idePort) { + logger.debug( + 'Unable to connect to IDE mode MCP server. GEMINI_CLI_IDE_SERVER_PORT environment variable is not set.', + ); + this.connectionStatus = IDEConnectionStatus.Disconnected; + return; + } + + try { + this.client = new Client({ + name: 'streamable-http-client', + // TODO(#3487): use the CLI version here. + version: '1.0.0', + }); + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${idePort}/mcp`), + ); + await this.client.connect(transport); + this.client.setNotificationHandler( + OpenFilesNotificationSchema, + (notification) => { + ideContext.setOpenFilesContext(notification.params); + }, + ); + this.client.onerror = (error) => { + logger.debug('IDE MCP client error:', error); + this.connectionStatus = IDEConnectionStatus.Disconnected; + ideContext.clearOpenFilesContext(); + }; + this.client.onclose = () => { + logger.debug('IDE MCP client connection closed.'); + this.connectionStatus = IDEConnectionStatus.Disconnected; + ideContext.clearOpenFilesContext(); + }; + + this.connectionStatus = IDEConnectionStatus.Connected; + } catch (error) { + this.connectionStatus = IDEConnectionStatus.Disconnected; + logger.debug('Failed to connect to MCP server:', error); + } + } +} diff --git a/packages/core/src/services/ideContext.test.ts b/packages/core/src/ide/ideContext.test.ts index 1cb09c53..1cb09c53 100644 --- a/packages/core/src/services/ideContext.test.ts +++ b/packages/core/src/ide/ideContext.test.ts diff --git a/packages/core/src/services/ideContext.ts b/packages/core/src/ide/ideContext.ts index f8a50f12..bc7383a1 100644 --- a/packages/core/src/services/ideContext.ts +++ b/packages/core/src/ide/ideContext.ts @@ -7,10 +7,6 @@ import { z } from 'zod'; /** - * The reserved server name for the IDE's MCP server. - */ -export const IDE_SERVER_NAME = '_ide_server'; -/** * Zod schema for validating a cursor position. */ export const CursorSchema = z.object({ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f560afb4..9d87ce32 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -40,7 +40,10 @@ export * from './utils/systemEncoding.js'; // Export services export * from './services/fileDiscoveryService.js'; export * from './services/gitService.js'; -export * from './services/ideContext.js'; + +// Export IDE specific logic +export * from './ide/ide-client.js'; +export * from './ide/ideContext.js'; // Export base tool definitions export * from './tools/tools.js'; diff --git a/packages/core/src/tools/mcp-client.ts b/packages/core/src/tools/mcp-client.ts index 3c482100..c59b1592 100644 --- a/packages/core/src/tools/mcp-client.ts +++ b/packages/core/src/tools/mcp-client.ts @@ -24,11 +24,6 @@ import { ToolRegistry } from './tool-registry.js'; import { MCPOAuthProvider } from '../mcp/oauth-provider.js'; import { OAuthUtils } from '../mcp/oauth-utils.js'; import { MCPOAuthTokenStorage } from '../mcp/oauth-token-storage.js'; -import { - OpenFilesNotificationSchema, - IDE_SERVER_NAME, - ideContext, -} from '../services/ideContext.js'; import { getErrorMessage } from '../utils/errors.js'; export const MCP_DEFAULT_TIMEOUT_MSEC = 10 * 60 * 1000; // default to 10 minutes @@ -379,24 +374,11 @@ export async function connectAndDiscover( ); try { updateMCPServerStatus(mcpServerName, MCPServerStatus.CONNECTED); - mcpClient.onerror = (error) => { console.error(`MCP ERROR (${mcpServerName}):`, error.toString()); updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED); - if (mcpServerName === IDE_SERVER_NAME) { - ideContext.clearOpenFilesContext(); - } }; - if (mcpServerName === IDE_SERVER_NAME) { - mcpClient.setNotificationHandler( - OpenFilesNotificationSchema, - (notification) => { - ideContext.setOpenFilesContext(notification.params); - }, - ); - } - const tools = await discoverTools( mcpServerName, mcpServerConfig, |
