Introduce VSCode companion extension (#3917)

This commit is contained in:
christine betts
2025-07-14 15:34:44 +00:00
committed by GitHub
parent 64f1d80b26
commit e9d680e8a4
12 changed files with 5806 additions and 6 deletions

View File

@@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
}
]
}

View File

@@ -0,0 +1,18 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View File

@@ -0,0 +1,9 @@
# IDE Companion
## Local Development
To test the extension locally, follow these steps:
1. Open the `packages/vscode-ide-companion` directory in VS Code.
2. Run `npm install`.
3. Run the extension development host via Run + Debug -> Extension

View File

@@ -0,0 +1,62 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const esbuild = require('esbuild');
const production = process.argv.includes('--production');
const watch = process.argv.includes('--watch');
/**
* @type {import('esbuild').Plugin}
*/
const esbuildProblemMatcherPlugin = {
name: 'esbuild-problem-matcher',
setup(build) {
build.onStart(() => {
console.log('[watch] build started');
});
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`);
console.error(
` ${location.file}:${location.line}:${location.column}:`,
);
});
console.log('[watch] build finished');
});
},
};
async function main() {
const ctx = await esbuild.context({
entryPoints: ['src/extension.ts'],
bundle: true,
format: 'cjs',
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: 'node',
outfile: 'dist/extension.js',
external: ['vscode'],
logLevel: 'silent',
plugins: [
/* add to the end of plugins array */
esbuildProblemMatcherPlugin,
],
});
if (watch) {
await ctx.watch();
} else {
await ctx.rebuild();
await ctx.dispose();
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -0,0 +1,34 @@
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
export default [
{
files: ['**/*.ts'],
},
{
plugins: {
'@typescript-eslint': typescriptEslint,
},
languageOptions: {
parser: tsParser,
ecmaVersion: 2022,
sourceType: 'module',
},
rules: {
'@typescript-eslint/naming-convention': [
'warn',
{
selector: 'import',
format: ['camelCase', 'PascalCase'],
},
],
curly: 'warn',
eqeqeq: 'warn',
'no-throw-literal': 'warn',
semi: 'warn',
},
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
{
"name": "gemini-cli-vscode-ide-companion",
"displayName": "Gemini CLI VSCode IDE Companion",
"description": "",
"version": "0.0.1",
"engines": {
"vscode": "^1.101.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onStartupFinished"
],
"main": "./dist/extension.js",
"scripts": {
"vscode:prepublish": "npm run package",
"build": "npm run compile",
"compile": "npm run check-types && npm run lint && node esbuild.js",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run lint && node esbuild.js --production",
"check-types": "tsc --noEmit",
"lint": "eslint src"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/node": "20.x",
"@types/vscode": "^1.101.0",
"@typescript-eslint/eslint-plugin": "^8.31.1",
"@typescript-eslint/parser": "^8.31.1",
"esbuild": "^0.25.3",
"eslint": "^9.25.1",
"npm-run-all": "^4.1.5",
"typescript": "^5.8.3"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",
"cors": "^2.8.5",
"express": "^5.1.0",
"zod": "^3.25.76"
}
}

View File

@@ -0,0 +1,14 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode';
import { startIDEServer } from './ide-server';
export async function activate(context: vscode.ExtensionContext) {
startIDEServer(context);
}
export function deactivate() {}

View File

@@ -0,0 +1,109 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express, { Request, Response } from 'express';
export async function startIDEServer(_context: vscode.ExtensionContext) {
const app = express();
app.use(express.json());
const mcpServer = createMcpServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
});
mcpServer.connect(transport);
app.post('/mcp', async (req: Request, res: Response) => {
console.log('Received MCP request:', req.body);
try {
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
});
}
}
});
// Handle GET requests for SSE streams
app.get('/mcp', async (req: Request, res: Response) => {
res.status(405).set('Allow', 'POST').send('Method Not Allowed');
});
// Start the server
// TODO(#3918): Generate dynamically and write to env variable
const PORT = 3000;
app.listen(PORT, (error) => {
if (error) {
console.error('Failed to start server:', error);
vscode.window.showErrorMessage(
`Companion server failed to start on port ${PORT}: ${error.message}`,
);
}
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
});
}
const createMcpServer = () => {
const server = new McpServer({
name: 'vscode-ide-server',
version: '1.0.0',
});
server.registerTool(
'getActiveFile',
{
description:
'(IDE Tool) Get the path of the file currently active in VS Code.',
inputSchema: {},
},
async () => {
try {
const activeEditor = vscode.window.activeTextEditor;
const filePath = activeEditor
? activeEditor.document.uri.fsPath
: undefined;
if (filePath) {
return {
content: [{ type: 'text', text: `Active file: ${filePath}` }],
};
} else {
return {
content: [
{
type: 'text',
text: 'No file is currently active in the editor.',
},
],
};
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Failed to get active file: ${
(error as Error).message || 'Unknown error'
}`,
},
],
};
}
},
);
return server;
};

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "Node16",
"target": "ES2022",
"lib": ["ES2022", "dom"],
"sourceMap": true,
"rootDir": "src",
"strict": true /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
}
}