summaryrefslogtreecommitdiff
path: root/packages/core/src/ide/ide-client.ts
blob: 3f91f3863c2912a335a9c0f01306f4b68f46f36f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/**
 * @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] [IDEClient]', ...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;
    }

    let transport: StreamableHTTPClientTransport | undefined;
    try {
      this.client = new Client({
        name: 'streamable-http-client',
        // TODO(#3487): use the CLI version here.
        version: '1.0.0',
      });
      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);
      if (transport) {
        try {
          await transport.close();
        } catch (closeError) {
          logger.debug('Failed to close transport:', closeError);
        }
      }
    }
  }
}