Skip to main content
Glama
extension.ts13.4 kB
import * as vscode from "vscode"; import * as path from "path"; import * as fs from "fs"; import * as cp from "child_process"; import * as os from "os"; const SERVER_ID = "imagen"; const API_KEY_SECRET = "imagenMcp.apiKey"; const DEFAULT_MODEL = "gemini-3-pro-image-preview"; const VENV_DIR_NAME = ".imagen-venv"; // Python dependencies needed by the server const PYTHON_DEPENDENCIES = [ "fastmcp>=2.0.0", "Pillow>=10.4.0", "pillow-heif>=0.18.0", ]; function getWorkspaceRoot(): string | undefined { const folder = vscode.workspace.workspaceFolders?.[0]; return folder?.uri.fsPath; } async function getApiKey(secretStorage: vscode.SecretStorage): Promise<string | undefined> { return secretStorage.get(API_KEY_SECRET); } function getConfiguredApiKey(): string | undefined { const config = vscode.workspace.getConfiguration(); const raw = config.get<string>("imagenMcp.apiKey"); const trimmed = raw?.trim(); return trimmed ? trimmed : undefined; } /** * Get the path to the extension's bundled server directory. */ function getBundledServerPath(context: vscode.ExtensionContext): string { return path.join(context.extensionPath, "server"); } /** * Get the path where we store the virtual environment. * Uses VS Code's global storage path so it persists across workspaces. */ function getVenvPath(context: vscode.ExtensionContext): string { return path.join(context.globalStorageUri.fsPath, VENV_DIR_NAME); } /** * Get the Python executable path within the virtual environment. */ function getVenvPython(context: vscode.ExtensionContext): string { const venvPath = getVenvPath(context); const isWindows = os.platform() === "win32"; return isWindows ? path.join(venvPath, "Scripts", "python.exe") : path.join(venvPath, "bin", "python"); } /** * Find a Python 3 interpreter on the system. */ async function findPython3(): Promise<string | undefined> { const candidates = os.platform() === "win32" ? ["python", "python3", "py -3"] : ["python3", "python"]; for (const candidate of candidates) { try { const result = await execCommand(`${candidate} --version`); if (result.includes("Python 3")) { return candidate.split(" ")[0]; // Return just "python3" or "python", not "py -3" } } catch { // Try next candidate } } return undefined; } /** * Execute a command and return stdout. */ function execCommand(command: string, cwd?: string): Promise<string> { return new Promise((resolve, reject) => { cp.exec(command, { cwd, timeout: 120000 }, (error, stdout, stderr) => { if (error) { reject(new Error(`${error.message}\n${stderr}`)); } else { resolve(stdout); } }); }); } /** * Check if the virtual environment exists and is valid. */ async function isVenvValid(context: vscode.ExtensionContext): Promise<boolean> { const pythonPath = getVenvPython(context); try { await fs.promises.access(pythonPath, fs.constants.X_OK); return true; } catch { return false; } } /** * Check if a marker file indicates dependencies are installed. */ async function areDependenciesInstalled(context: vscode.ExtensionContext): Promise<boolean> { const markerPath = path.join(getVenvPath(context), ".deps-installed"); try { await fs.promises.access(markerPath, fs.constants.F_OK); return true; } catch { return false; } } /** * Write a marker file to indicate dependencies are installed. */ async function markDependenciesInstalled(context: vscode.ExtensionContext): Promise<void> { const markerPath = path.join(getVenvPath(context), ".deps-installed"); await fs.promises.writeFile(markerPath, new Date().toISOString(), "utf8"); } /** * Ensure the Python environment is set up with all dependencies. */ async function ensurePythonEnvironment(context: vscode.ExtensionContext): Promise<boolean> { const venvPath = getVenvPath(context); const pythonPath = getVenvPython(context); // Ensure global storage directory exists await fs.promises.mkdir(context.globalStorageUri.fsPath, { recursive: true }); // Check if venv already exists and is valid if (await isVenvValid(context) && await areDependenciesInstalled(context)) { return true; } // Find Python 3 const systemPython = await findPython3(); if (!systemPython) { vscode.window.showErrorMessage( "Python 3 is required but not found. Please install Python 3 and try again." ); return false; } // Show progress while setting up return vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: "Imagen MCP: Setting up Python environment...", cancellable: false, }, async (progress) => { try { // Create virtual environment if needed if (!(await isVenvValid(context))) { progress.report({ message: "Creating virtual environment..." }); await execCommand(`${systemPython} -m venv "${venvPath}"`); } // Upgrade pip progress.report({ message: "Upgrading pip..." }); await execCommand(`"${pythonPath}" -m pip install --upgrade pip`); // Install dependencies progress.report({ message: "Installing dependencies..." }); for (const dep of PYTHON_DEPENDENCIES) { await execCommand(`"${pythonPath}" -m pip install "${dep}"`); } // Mark dependencies as installed await markDependenciesInstalled(context); vscode.window.showInformationMessage("Imagen MCP: Python environment ready."); return true; } catch (error) { vscode.window.showErrorMessage( `Imagen MCP: Failed to set up Python environment: ${error}` ); return false; } } ); } /** * Get the server command and args for the MCP config. * Uses the bundled server from the extension. */ function getServerConfig(context: vscode.ExtensionContext): { command: string; args: string[] } { const pythonPath = getVenvPython(context); const serverPath = path.join(getBundledServerPath(context), "run_server.py"); return { command: pythonPath, args: [serverPath], }; } async function syncApiKeyFromSettings(context: vscode.ExtensionContext): Promise<string | undefined> { const configured = getConfiguredApiKey(); if (configured) { await context.secrets.store(API_KEY_SECRET, configured); return configured; } // Keep any previously stored secret so extension updates or missing settings // do not wipe the API key. Users can explicitly clear via the command or by // overwriting the setting. return getApiKey(context.secrets); } async function setApiKey(context: vscode.ExtensionContext) { const apiKey = await vscode.window.showInputBox({ prompt: "Enter your Google AI API key", password: true, ignoreFocusOut: true, placeHolder: "GOOGLE_AI_API_KEY" }); if (!apiKey) { return; } await context.secrets.store(API_KEY_SECRET, apiKey.trim()); await vscode.workspace.getConfiguration().update("imagenMcp.apiKey", apiKey.trim(), vscode.ConfigurationTarget.Workspace); vscode.window.showInformationMessage("Imagen MCP API key saved to VS Code secrets."); } async function promptForApiKeyIfMissing(context: vscode.ExtensionContext) { const existing = await getApiKey(context.secrets); if (existing) { return; } const choice = await vscode.window.showWarningMessage( "Imagen MCP API key is not set. The server will fail without it.", "Set API Key" ); if (choice === "Set API Key") { await setApiKey(context); await ensureMcpConfig(context, { force: true, notify: false }); } } function updateStatusBar(status: vscode.StatusBarItem) { const config = vscode.workspace.getConfiguration(); const current = config.get<string>("imagenMcp.modelId") || DEFAULT_MODEL; status.text = `$(rocket) Imagen: ${current}`; } async function setModel(context: vscode.ExtensionContext, status: vscode.StatusBarItem) { const config = vscode.workspace.getConfiguration(); const current = config.get<string>("imagenMcp.modelId") || DEFAULT_MODEL; const picks = [ "gemini-3-pro-image-preview", "gemini-2.5-flash-image", "gemini-2.0-flash-exp-image-generation", "imagen-4.0-generate-001", "imagen-4.0-ultra-generate-001" ]; const selection = await vscode.window.showQuickPick(picks, { placeHolder: "Select default image model", canPickMany: false }); if (!selection) { return; } await config.update("imagenMcp.modelId", selection, vscode.ConfigurationTarget.Workspace); vscode.window.showInformationMessage(`Imagen MCP model set to ${selection}.`); updateStatusBar(status); await ensureMcpConfig(context, { force: true, notify: false }); } async function writeMcpConfig(context: vscode.ExtensionContext) { await syncApiKeyFromSettings(context); await ensureMcpConfig(context, { force: true, notify: true }); } /** * Reinstall the Python environment from scratch. */ async function reinstallEnvironment(context: vscode.ExtensionContext) { const venvPath = getVenvPath(context); // Remove existing venv try { await fs.promises.rm(venvPath, { recursive: true, force: true }); } catch { // Ignore errors if directory doesn't exist } // Reinstall const success = await ensurePythonEnvironment(context); if (success) { // Regenerate MCP config with new paths await ensureMcpConfig(context, { force: true, notify: true }); } } async function ensureMcpConfig( context: vscode.ExtensionContext, options: { force?: boolean; notify?: boolean } = {} ) { const root = getWorkspaceRoot(); if (!root) { vscode.window.showErrorMessage("No workspace folder open."); return; } const vscodeDir = path.join(root, ".vscode"); const mcpPath = path.join(vscodeDir, "mcp.json"); // Check if we need to migrate from old workspace-based config let needsMigration = false; if (fs.existsSync(mcpPath)) { try { const existing = JSON.parse(await fs.promises.readFile(mcpPath, "utf8")); const imagenServer = existing?.servers?.[SERVER_ID]; if (imagenServer) { const cmd = imagenServer.command || ""; // Detect old workspace-based configs that need migration if (cmd.includes("${workspaceFolder}") || cmd.includes("run_with_venv.sh") || cmd.includes("run_server.py")) { needsMigration = true; } } } catch { // If we can't parse, let's regenerate needsMigration = true; } } if (!options.force && !needsMigration && fs.existsSync(mcpPath)) { return; // already present and doesn't need migration } // Ensure the Python environment is set up before writing the MCP config const envReady = await ensurePythonEnvironment(context); if (!envReady) { return; // Error already shown by ensurePythonEnvironment } const config = vscode.workspace.getConfiguration(); const apiKey = await getApiKey(context.secrets); const modelId = config.get<string>("imagenMcp.modelId") || DEFAULT_MODEL; // Get the bundled server configuration const serverConfig = getServerConfig(context); const mcpConfig = { servers: { [SERVER_ID]: { command: serverConfig.command, args: serverConfig.args, env: { ...(apiKey ? { GOOGLE_AI_API_KEY: apiKey } : {}), IMAGEN_MODEL_ID: modelId, }, }, }, }; await fs.promises.mkdir(vscodeDir, { recursive: true }); await fs.promises.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf8"); if (options.notify || needsMigration) { vscode.window.showInformationMessage( needsMigration ? `Imagen MCP config migrated to use bundled server: ${mcpPath}` : `MCP config written to ${mcpPath}` ); } } export function activate(context: vscode.ExtensionContext) { const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10); status.text = `$(rocket) Imagen MCP`; status.tooltip = "Imagen MCP Server"; status.command = "imagenMcp.setModel"; status.show(); context.subscriptions.push( vscode.commands.registerCommand("imagenMcp.setApiKey", () => setApiKey(context)), vscode.commands.registerCommand("imagenMcp.setModel", () => setModel(context, status)), vscode.commands.registerCommand("imagenMcp.writeMcpConfig", () => writeMcpConfig(context)), vscode.commands.registerCommand("imagenMcp.reinstallEnvironment", () => reinstallEnvironment(context)), status ); updateStatusBar(status); context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(async (event) => { if (event.affectsConfiguration("imagenMcp.apiKey")) { await syncApiKeyFromSettings(context); await promptForApiKeyIfMissing(context); } if (event.affectsConfiguration("imagenMcp.modelId")) { updateStatusBar(status); } if (event.affectsConfiguration("imagenMcp")) { await ensureMcpConfig(context, { force: true, notify: false }); } }) ); // Auto-create MCP config if missing so the server works out of the box void (async () => { await syncApiKeyFromSettings(context); await ensureMcpConfig(context, { force: false, notify: false }); await promptForApiKeyIfMissing(context); })(); } export function deactivate() {}

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vipincr/imagen-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server