Files
extension/src/mcp/server.ts
T
Hanzo Dev f6eeaacd4f feat: add browser extension with source-map support for click-to-code navigation
- Add browser extension that connects to MCP server via WebSocket
- Support source-map extraction for React, Vue, and Svelte frameworks
- Auto-start browser extension server when MCP initializes
- Create npm package @hanzoai/browser-devtools for easy installation
- Add IDE integration to open files at specific lines when elements are clicked
- Include CLI tool with start, install-extension, and test commands
- Add comprehensive integration tests for Claude Code compatibility
- Support fallback to data-hanzo-id for legacy code without source maps

The browser extension enables Alt+Click on any element to navigate directly
to its source code, leveraging framework-specific debug metadata and source maps.
2025-07-13 17:55:14 -05:00

234 lines
8.1 KiB
TypeScript

import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs/promises';
import { spawn, ChildProcess } from 'child_process';
import { MCPClient } from './client';
import { MCPTools } from './tools';
import { getConfig } from '../config';
import { BrowserExtensionServer } from '../mcp-tools/browser-extension-server';
export class MCPServer {
private context: vscode.ExtensionContext;
private serverProcess?: ChildProcess;
private client?: MCPClient;
private tools: MCPTools;
private config = getConfig();
private browserExtensionServer?: BrowserExtensionServer;
constructor(context: vscode.ExtensionContext) {
this.context = context;
this.tools = new MCPTools(context);
}
async initialize() {
console.log('[MCPServer] Initializing MCP server integration');
// Initialize tools
await this.tools.initialize();
// Start browser extension server if enabled
if (this.config.browserExtension?.enabled !== false) {
await this.startBrowserExtensionServer();
}
// Check if running as Claude Desktop extension
if (this.isClaudeDesktopExtension()) {
await this.initializeForClaudeDesktop();
} else {
// For VS Code/Cursor/Windsurf, start local MCP server
await this.startLocalServer();
}
}
private isClaudeDesktopExtension(): boolean {
// Detect if running as Claude Desktop extension
return process.env.MCP_TRANSPORT === 'stdio' ||
process.env.CLAUDE_DESKTOP === 'true';
}
private async initializeForClaudeDesktop() {
console.log('[MCPServer] Initializing for Claude Desktop');
// When running as Claude Desktop extension, we communicate via stdio
this.client = new MCPClient('stdio');
await this.client.connect();
// Register all tools with Claude Desktop
await this.registerTools();
}
private async startLocalServer() {
console.log('[MCPServer] Starting local MCP server');
const serverPath = path.join(this.context.extensionPath, 'dist', 'mcp-server.js');
// Check if server bundle exists
try {
await fs.access(serverPath);
} catch {
console.error('[MCPServer] MCP server bundle not found. Please build the extension.');
return;
}
// Start the server process
this.serverProcess = spawn('node', [serverPath], {
env: {
...process.env,
HANZO_EXTENSION_PATH: this.context.extensionPath,
HANZO_WORKSPACE: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''
}
});
this.serverProcess.on('error', (error) => {
console.error('[MCPServer] Server process error:', error);
vscode.window.showErrorMessage(`MCP Server failed to start: ${error.message}`);
});
this.serverProcess.stdout?.on('data', (data) => {
console.log('[MCPServer]', data.toString());
});
this.serverProcess.stderr?.on('data', (data) => {
console.error('[MCPServer] Error:', data.toString());
});
// Connect client to local server
this.client = new MCPClient('tcp', { port: this.config.mcp.port || 3000 });
// Wait a bit for server to start
await new Promise(resolve => setTimeout(resolve, 1000));
try {
await this.client.connect();
await this.registerTools();
console.log('[MCPServer] Successfully connected to local server');
} catch (error) {
console.error('[MCPServer] Failed to connect to local server:', error);
}
}
private async registerTools() {
if (!this.client) return;
console.log('[MCPServer] Registering tools');
// Get all available tools from MCPTools
const tools = this.tools.getAllTools();
for (const tool of tools) {
try {
await this.client.registerTool(tool);
console.log(`[MCPServer] Registered tool: ${tool.name}`);
} catch (error) {
console.error(`[MCPServer] Failed to register tool ${tool.name}:`, error);
}
}
}
async executeCommand(command: string, args: any): Promise<any> {
if (!this.client) {
throw new Error('MCP client not initialized');
}
return this.client.executeCommand(command, args);
}
private async startBrowserExtensionServer() {
try {
const port = this.config.browserExtension?.port || 3001;
const projectRoot = vscode.workspace.workspaceFolders?.[0].uri.fsPath || process.cwd();
this.browserExtensionServer = new BrowserExtensionServer(port, projectRoot);
// Handle element selection events
this.browserExtensionServer.on('elementSelected', async (data) => {
console.log('[MCPServer] Element selected:', data);
// Open file in editor
const doc = await vscode.workspace.openTextDocument(data.file);
const editor = await vscode.window.showTextDocument(doc);
// Jump to line
const position = new vscode.Position(data.line - 1, data.column || 0);
editor.selection = new vscode.Selection(position, position);
editor.revealRange(new vscode.Range(position, position));
// Show notification
vscode.window.showInformationMessage(
`Navigated to ${path.basename(data.file)}:${data.line} (${data.framework || 'unknown'} component)`
);
});
console.log(`[MCPServer] Browser extension server started on port ${port}`);
// Show notification with instructions
const action = await vscode.window.showInformationMessage(
'Browser extension server running. Alt+click elements in your browser to navigate to source.',
'Install Extension'
);
if (action === 'Install Extension') {
await this.installBrowserExtension();
}
} catch (error) {
console.error('[MCPServer] Failed to start browser extension server:', error);
}
}
private async installBrowserExtension() {
// Build the extension
const terminal = vscode.window.createTerminal('Install Browser Extension');
terminal.sendText('npm run build:browser-extension');
terminal.show();
// Show instructions
vscode.window.showInformationMessage(
'Browser extension built. Load it via chrome://extensions/ → Developer mode → Load unpacked → Select dist/browser-extension/',
'Open Chrome Extensions'
).then(action => {
if (action === 'Open Chrome Extensions') {
vscode.env.openExternal(vscode.Uri.parse('chrome://extensions/'));
}
});
}
shutdown() {
console.log('[MCPServer] Shutting down');
if (this.browserExtensionServer) {
this.browserExtensionServer.close();
}
if (this.client) {
this.client.disconnect();
}
if (this.serverProcess) {
this.serverProcess.kill();
this.serverProcess = undefined;
}
}
}
// Export types for MCP tools
export interface MCPTool {
name: string;
description: string;
inputSchema: {
type: 'object';
properties: Record<string, any>;
required?: string[];
};
handler: (args: any) => Promise<any>;
}
export interface MCPPrompt {
name: string;
description: string;
arguments?: Array<{
name: string;
description: string;
required: boolean;
}>;
handler: (args: any) => Promise<string>;
}