Set port dynamically in VSCode extension and read from it in gemini-cli and send initial notification (#4255)
This commit is contained in:
parent
bf51de1a4d
commit
b61016f2a5
|
@ -5699,6 +5699,18 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/get-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
|
|
@ -840,6 +840,7 @@ describe('loadCliConfig ideMode', () => {
|
||||||
// Explicitly delete TERM_PROGRAM and SANDBOX before each test
|
// Explicitly delete TERM_PROGRAM and SANDBOX before each test
|
||||||
delete process.env.TERM_PROGRAM;
|
delete process.env.TERM_PROGRAM;
|
||||||
delete process.env.SANDBOX;
|
delete process.env.SANDBOX;
|
||||||
|
delete process.env.GEMINI_CLI_IDE_SERVER_PORT;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -876,6 +877,7 @@ describe('loadCliConfig ideMode', () => {
|
||||||
process.argv = ['node', 'script.js', '--ide-mode'];
|
process.argv = ['node', 'script.js', '--ide-mode'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments();
|
||||||
process.env.TERM_PROGRAM = 'vscode';
|
process.env.TERM_PROGRAM = 'vscode';
|
||||||
|
process.env.GEMINI_CLI_IDE_SERVER_PORT = '3000';
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getIdeMode()).toBe(true);
|
expect(config.getIdeMode()).toBe(true);
|
||||||
|
@ -885,6 +887,7 @@ describe('loadCliConfig ideMode', () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments();
|
||||||
process.env.TERM_PROGRAM = 'vscode';
|
process.env.TERM_PROGRAM = 'vscode';
|
||||||
|
process.env.GEMINI_CLI_IDE_SERVER_PORT = '3000';
|
||||||
const settings: Settings = { ideMode: true };
|
const settings: Settings = { ideMode: true };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getIdeMode()).toBe(true);
|
expect(config.getIdeMode()).toBe(true);
|
||||||
|
@ -894,6 +897,7 @@ describe('loadCliConfig ideMode', () => {
|
||||||
process.argv = ['node', 'script.js', '--ide-mode'];
|
process.argv = ['node', 'script.js', '--ide-mode'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments();
|
||||||
process.env.TERM_PROGRAM = 'vscode';
|
process.env.TERM_PROGRAM = 'vscode';
|
||||||
|
process.env.GEMINI_CLI_IDE_SERVER_PORT = '3000';
|
||||||
const settings: Settings = { ideMode: false };
|
const settings: Settings = { ideMode: false };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getIdeMode()).toBe(true);
|
expect(config.getIdeMode()).toBe(true);
|
||||||
|
@ -932,6 +936,7 @@ describe('loadCliConfig ideMode', () => {
|
||||||
process.argv = ['node', 'script.js', '--ide-mode'];
|
process.argv = ['node', 'script.js', '--ide-mode'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments();
|
||||||
process.env.TERM_PROGRAM = 'vscode';
|
process.env.TERM_PROGRAM = 'vscode';
|
||||||
|
process.env.GEMINI_CLI_IDE_SERVER_PORT = '3000';
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getIdeMode()).toBe(true);
|
expect(config.getIdeMode()).toBe(true);
|
||||||
|
@ -941,4 +946,16 @@ describe('loadCliConfig ideMode', () => {
|
||||||
expect(mcpServers['_ide_server'].description).toBe('IDE connection');
|
expect(mcpServers['_ide_server'].description).toBe('IDE connection');
|
||||||
expect(mcpServers['_ide_server'].trust).toBe(false);
|
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.",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -306,13 +306,20 @@ export async function loadCliConfig(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ideMode) {
|
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(
|
mcpServers[IDE_SERVER_NAME] = new MCPServerConfig(
|
||||||
undefined, // command
|
undefined, // command
|
||||||
undefined, // args
|
undefined, // args
|
||||||
undefined, // env
|
undefined, // env
|
||||||
undefined, // cwd
|
undefined, // cwd
|
||||||
undefined, // url
|
undefined, // url
|
||||||
'http://localhost:3000/mcp', // httpUrl
|
httpUrl, // httpUrl
|
||||||
undefined, // headers
|
undefined, // headers
|
||||||
undefined, // tcp
|
undefined, // tcp
|
||||||
undefined, // timeout
|
undefined, // timeout
|
||||||
|
|
|
@ -14,21 +14,31 @@ import {
|
||||||
type JSONRPCNotification,
|
type JSONRPCNotification,
|
||||||
} from '@modelcontextprotocol/sdk/types.js';
|
} 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) {
|
export async function startIDEServer(context: vscode.ExtensionContext) {
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
|
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
|
||||||
|
const sessionsWithInitialNotification = new Set<string>();
|
||||||
|
|
||||||
const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
|
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 },
|
|
||||||
};
|
|
||||||
for (const transport of Object.values(transports)) {
|
for (const transport of Object.values(transports)) {
|
||||||
transport.send(notification);
|
sendActiveFileChangedNotification(transport);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
context.subscriptions.push(disposable);
|
context.subscriptions.push(disposable);
|
||||||
|
@ -44,19 +54,12 @@ export async function startIDEServer(context: vscode.ExtensionContext) {
|
||||||
sessionIdGenerator: () => randomUUID(),
|
sessionIdGenerator: () => randomUUID(),
|
||||||
onsessioninitialized: (newSessionId) => {
|
onsessioninitialized: (newSessionId) => {
|
||||||
transports[newSessionId] = transport;
|
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 = () => {
|
transport.onclose = () => {
|
||||||
if (transport.sessionId) {
|
if (transport.sessionId) {
|
||||||
|
sessionsWithInitialNotification.delete(transport.sessionId);
|
||||||
delete transports[transport.sessionId];
|
delete transports[transport.sessionId];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -101,6 +104,7 @@ export async function startIDEServer(context: vscode.ExtensionContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const transport = transports[sessionId];
|
const transport = transports[sessionId];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await transport.handleRequest(req, res);
|
await transport.handleRequest(req, res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -109,20 +113,31 @@ export async function startIDEServer(context: vscode.ExtensionContext) {
|
||||||
res.status(400).send('Bad Request');
|
res.status(400).send('Bad Request');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sessionsWithInitialNotification.has(sessionId)) {
|
||||||
|
sendActiveFileChangedNotification(transport);
|
||||||
|
sessionsWithInitialNotification.add(sessionId);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
app.get('/mcp', handleSessionRequest);
|
app.get('/mcp', handleSessionRequest);
|
||||||
|
|
||||||
// TODO(#3918): Generate dynamically and write to env variable
|
const server = app.listen(0, () => {
|
||||||
const PORT = 3000;
|
const address = (server as Server).address();
|
||||||
app.listen(PORT, (error?: Error) => {
|
if (address && typeof address !== 'string') {
|
||||||
if (error) {
|
const port = address.port;
|
||||||
console.error('Failed to start server:', error);
|
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(
|
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 () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const activeEditor = vscode.window.activeTextEditor;
|
const activeEditor = vscode.window.activeTextEditor;
|
||||||
const filePath = activeEditor
|
const filePath = activeEditor ? activeEditor.document.uri.fsPath : '';
|
||||||
? activeEditor.document.uri.fsPath
|
|
||||||
: undefined;
|
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: `Active file: ${filePath}` }],
|
content: [{ type: 'text', text: `Active file: ${filePath}` }],
|
||||||
|
|
Loading…
Reference in New Issue