diff --git a/package-lock.json b/package-lock.json index 5c1b7c0a..99269519 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5699,6 +5699,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 4042bf93..6aab4487 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -840,6 +840,7 @@ describe('loadCliConfig ideMode', () => { // Explicitly delete TERM_PROGRAM and SANDBOX before each test delete process.env.TERM_PROGRAM; delete process.env.SANDBOX; + delete process.env.GEMINI_CLI_IDE_SERVER_PORT; }); afterEach(() => { @@ -876,6 +877,7 @@ describe('loadCliConfig ideMode', () => { process.argv = ['node', 'script.js', '--ide-mode']; const argv = await parseArguments(); process.env.TERM_PROGRAM = 'vscode'; + process.env.GEMINI_CLI_IDE_SERVER_PORT = '3000'; const settings: Settings = {}; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getIdeMode()).toBe(true); @@ -885,6 +887,7 @@ describe('loadCliConfig ideMode', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments(); process.env.TERM_PROGRAM = 'vscode'; + process.env.GEMINI_CLI_IDE_SERVER_PORT = '3000'; const settings: Settings = { ideMode: true }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getIdeMode()).toBe(true); @@ -894,6 +897,7 @@ describe('loadCliConfig ideMode', () => { process.argv = ['node', 'script.js', '--ide-mode']; const argv = await parseArguments(); process.env.TERM_PROGRAM = 'vscode'; + process.env.GEMINI_CLI_IDE_SERVER_PORT = '3000'; const settings: Settings = { ideMode: false }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getIdeMode()).toBe(true); @@ -932,6 +936,7 @@ describe('loadCliConfig ideMode', () => { process.argv = ['node', 'script.js', '--ide-mode']; const argv = await parseArguments(); process.env.TERM_PROGRAM = 'vscode'; + process.env.GEMINI_CLI_IDE_SERVER_PORT = '3000'; const settings: Settings = {}; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getIdeMode()).toBe(true); @@ -941,4 +946,16 @@ describe('loadCliConfig ideMode', () => { expect(mcpServers['_ide_server'].description).toBe('IDE connection'); expect(mcpServers['_ide_server'].trust).toBe(false); }); + + it('should throw an error if ideMode is true and no port is set', async () => { + process.argv = ['node', 'script.js', '--ide-mode']; + const argv = await parseArguments(); + process.env.TERM_PROGRAM = 'vscode'; + const settings: Settings = {}; + await expect( + loadCliConfig(settings, [], 'test-session', argv), + ).rejects.toThrow( + "Could not run in ide mode, make sure you're running in vs code integrated terminal. Try running in a fresh terminal.", + ); + }); }); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index bf76fa4c..69708a61 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -306,13 +306,20 @@ export async function loadCliConfig( } if (ideMode) { + const companionPort = process.env.GEMINI_CLI_IDE_SERVER_PORT; + if (!companionPort) { + throw new Error( + "Could not run in ide mode, make sure you're running in vs code integrated terminal. Try running in a fresh terminal.", + ); + } + const httpUrl = `http://localhost:${companionPort}/mcp`; mcpServers[IDE_SERVER_NAME] = new MCPServerConfig( undefined, // command undefined, // args undefined, // env undefined, // cwd undefined, // url - 'http://localhost:3000/mcp', // httpUrl + httpUrl, // httpUrl undefined, // headers undefined, // tcp undefined, // timeout diff --git a/packages/vscode-ide-companion/src/ide-server.ts b/packages/vscode-ide-companion/src/ide-server.ts index 43c55e7d..160cb54a 100644 --- a/packages/vscode-ide-companion/src/ide-server.ts +++ b/packages/vscode-ide-companion/src/ide-server.ts @@ -14,21 +14,31 @@ import { type JSONRPCNotification, } from '@modelcontextprotocol/sdk/types.js'; +import { Server } from 'node:http'; + +function sendActiveFileChangedNotification( + transport: StreamableHTTPServerTransport, +) { + const editor = vscode.window.activeTextEditor; + const filePath = editor ? editor.document.uri.fsPath : ''; + const notification: JSONRPCNotification = { + jsonrpc: '2.0', + method: 'ide/activeFileChanged', + params: { filePath }, + }; + transport.send(notification); +} + export async function startIDEServer(context: vscode.ExtensionContext) { const app = express(); app.use(express.json()); const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; + const sessionsWithInitialNotification = new Set(); - const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => { - const filePath = editor ? editor.document.uri.fsPath : null; - const notification: JSONRPCNotification = { - jsonrpc: '2.0', - method: 'ide/activeFileChanged', - params: { filePath }, - }; + const disposable = vscode.window.onDidChangeActiveTextEditor((_editor) => { for (const transport of Object.values(transports)) { - transport.send(notification); + sendActiveFileChangedNotification(transport); } }); context.subscriptions.push(disposable); @@ -44,19 +54,12 @@ export async function startIDEServer(context: vscode.ExtensionContext) { sessionIdGenerator: () => randomUUID(), onsessioninitialized: (newSessionId) => { transports[newSessionId] = transport; - const editor = vscode.window.activeTextEditor; - const filePath = editor ? editor.document.uri.fsPath : null; - const notification: JSONRPCNotification = { - jsonrpc: '2.0', - method: 'ide/activeFileChanged', - params: { filePath }, - }; - transport.send(notification); }, }); transport.onclose = () => { if (transport.sessionId) { + sessionsWithInitialNotification.delete(transport.sessionId); delete transports[transport.sessionId]; } }; @@ -101,6 +104,7 @@ export async function startIDEServer(context: vscode.ExtensionContext) { } const transport = transports[sessionId]; + try { await transport.handleRequest(req, res); } catch (error) { @@ -109,20 +113,31 @@ export async function startIDEServer(context: vscode.ExtensionContext) { res.status(400).send('Bad Request'); } } + + if (!sessionsWithInitialNotification.has(sessionId)) { + sendActiveFileChangedNotification(transport); + sessionsWithInitialNotification.add(sessionId); + } }; app.get('/mcp', handleSessionRequest); - // TODO(#3918): Generate dynamically and write to env variable - const PORT = 3000; - app.listen(PORT, (error?: Error) => { - if (error) { - console.error('Failed to start server:', error); + const server = app.listen(0, () => { + const address = (server as Server).address(); + if (address && typeof address !== 'string') { + const port = address.port; + context.environmentVariableCollection.replace( + 'GEMINI_CLI_IDE_SERVER_PORT', + port.toString(), + ); + console.log(`MCP Streamable HTTP Server listening on port ${port}`); + } else { + const port = 0; + console.error('Failed to start server:', 'Unknown error'); vscode.window.showErrorMessage( - `Companion server failed to start on port ${PORT}: ${error.message}`, + `Companion server failed to start on port ${port}: Unknown error`, ); } - console.log(`MCP Streamable HTTP Server listening on port ${PORT}`); }); } @@ -144,9 +159,7 @@ const createMcpServer = () => { async () => { try { const activeEditor = vscode.window.activeTextEditor; - const filePath = activeEditor - ? activeEditor.document.uri.fsPath - : undefined; + const filePath = activeEditor ? activeEditor.document.uri.fsPath : ''; if (filePath) { return { content: [{ type: 'text', text: `Active file: ${filePath}` }],