Flutter Inspector MCP Server

by Arenukvern
Verified
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { exec } from "child_process"; import * as dotenv from "dotenv"; import { promisify } from "util"; import WebSocket from "ws"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; // Load environment variables dotenv.config(); // Parse command line arguments const argv = yargs(hideBin(process.argv)) .options({ port: { alias: "p", description: "Port to run the server on", type: "number", default: parseInt(process.env.PORT || "3334", 10), }, stdio: { description: "Run in stdio mode instead of HTTP mode", type: "boolean", default: true, }, "log-level": { description: "Logging level", choices: ["error", "warn", "info", "debug"] as const, default: process.env.LOG_LEVEL || "info", }, }) .help() .parseSync(); const execAsync = promisify(exec); interface FlutterPort { port: number; pid: string; command: string; } interface IsolatesResponse { isolates: Array<{ id: string; [key: string]: unknown; }>; } interface FlutterMethodResponse { type?: string; result: unknown; } interface WebSocketRequest { jsonrpc: "2.0"; id: string; method: string; params?: Record<string, unknown>; } interface WebSocketResponse { jsonrpc: "2.0"; id: string; result?: unknown; error?: { code: number; message: string; data?: unknown; }; } type LogLevel = "error" | "warn" | "info" | "debug"; interface IsolateInfo { id: string; name?: string; number?: string; isSystemIsolate?: boolean; isolateGroupId?: string; extensionRPCs?: string[]; } interface VMInfo { isolates: IsolateInfo[]; version?: string; pid?: number; // Add other VM info fields as needed } interface IsolateResponse extends IsolateInfo { extensionRPCs?: string[]; // Add other isolate response fields as needed } enum RPCPrefix { UI = "ext.ui.window", DART_IO = "ext.dart.io", FLUTTER = "ext.flutter", INSPECTOR = "ext.flutter.inspector", ISAR = "ext.isar", } function createRPCMethod(prefix: RPCPrefix, method: string): string { return `${prefix}.${method}`; } // Group RPC methods by functionality const FlutterRPC = { UI: { SCHEDULE_FRAME: createRPCMethod(RPCPrefix.UI, "scheduleFrame"), REINITIALIZE_SHADER: createRPCMethod(RPCPrefix.UI, "reinitializeShader"), IMPELLER_ENABLED: createRPCMethod(RPCPrefix.UI, "impellerEnabled"), }, DartIO: { HTTP_TIMELINE_LOGGING: createRPCMethod( RPCPrefix.DART_IO, "httpEnableTimelineLogging" ), GET_SOCKET_PROFILE: createRPCMethod(RPCPrefix.DART_IO, "getSocketProfile"), SOCKET_PROFILING_ENABLED: createRPCMethod( RPCPrefix.DART_IO, "socketProfilingEnabled" ), CLEAR_SOCKET_PROFILE: createRPCMethod( RPCPrefix.DART_IO, "clearSocketProfile" ), GET_VERSION: createRPCMethod(RPCPrefix.DART_IO, "getVersion"), GET_HTTP_PROFILE: createRPCMethod(RPCPrefix.DART_IO, "getHttpProfile"), GET_HTTP_PROFILE_REQUEST: createRPCMethod( RPCPrefix.DART_IO, "getHttpProfileRequest" ), CLEAR_HTTP_PROFILE: createRPCMethod(RPCPrefix.DART_IO, "clearHttpProfile"), GET_OPEN_FILES: createRPCMethod(RPCPrefix.DART_IO, "getOpenFiles"), GET_OPEN_FILE_BY_ID: createRPCMethod(RPCPrefix.DART_IO, "getOpenFileById"), }, Core: { REASSEMBLE: createRPCMethod(RPCPrefix.FLUTTER, "reassemble"), EXIT: createRPCMethod(RPCPrefix.FLUTTER, "exit"), CONNECTED_VM_SERVICE_URI: createRPCMethod( RPCPrefix.FLUTTER, "connectedVmServiceUri" ), ACTIVE_DEVTOOLS_SERVER_ADDRESS: createRPCMethod( RPCPrefix.FLUTTER, "activeDevToolsServerAddress" ), PLATFORM_OVERRIDE: createRPCMethod(RPCPrefix.FLUTTER, "platformOverride"), BRIGHTNESS_OVERRIDE: createRPCMethod( RPCPrefix.FLUTTER, "brightnessOverride" ), TIME_DILATION: createRPCMethod(RPCPrefix.FLUTTER, "timeDilation"), EVICT: createRPCMethod(RPCPrefix.FLUTTER, "evict"), INVERT_OVERSIZED_IMAGES: createRPCMethod( RPCPrefix.FLUTTER, "invertOversizedImages" ), DID_SEND_FIRST_FRAME_EVENT: createRPCMethod( RPCPrefix.FLUTTER, "didSendFirstFrameEvent" ), DID_SEND_FIRST_FRAME_RASTERIZED_EVENT: createRPCMethod( RPCPrefix.FLUTTER, "didSendFirstFrameRasterizedEvent" ), PROFILE_PLATFORM_CHANNELS: createRPCMethod( RPCPrefix.FLUTTER, "profilePlatformChannels" ), }, Debug: { DUMP_APP: createRPCMethod(RPCPrefix.FLUTTER, "debugDumpApp"), DUMP_RENDER_TREE: createRPCMethod(RPCPrefix.FLUTTER, "debugDumpRenderTree"), DUMP_LAYER_TREE: createRPCMethod(RPCPrefix.FLUTTER, "debugDumpLayerTree"), DUMP_SEMANTICS: createRPCMethod( RPCPrefix.FLUTTER, "debugDumpSemanticsTreeInTraversalOrder" ), DUMP_SEMANTICS_INVERSE: createRPCMethod( RPCPrefix.FLUTTER, "debugDumpSemanticsTreeInInverseHitTestOrder" ), DUMP_FOCUS_TREE: createRPCMethod(RPCPrefix.FLUTTER, "debugDumpFocusTree"), DEBUG_PAINT: createRPCMethod(RPCPrefix.FLUTTER, "debugPaint"), DEBUG_PAINT_BASELINES: createRPCMethod( RPCPrefix.FLUTTER, "debugPaintBaselinesEnabled" ), REPAINT_RAINBOW: createRPCMethod(RPCPrefix.FLUTTER, "repaintRainbow"), DEBUG_DISABLE_CLIP_LAYERS: createRPCMethod( RPCPrefix.FLUTTER, "debugDisableClipLayers" ), DEBUG_DISABLE_PHYSICAL_SHAPE_LAYERS: createRPCMethod( RPCPrefix.FLUTTER, "debugDisablePhysicalShapeLayers" ), DEBUG_DISABLE_OPACITY_LAYERS: createRPCMethod( RPCPrefix.FLUTTER, "debugDisableOpacityLayers" ), DEBUG_ALLOW_BANNER: createRPCMethod(RPCPrefix.FLUTTER, "debugAllowBanner"), DISABLE_PHYSICAL_SHAPE_LAYERS: createRPCMethod( RPCPrefix.FLUTTER, "debugDisablePhysicalShapeLayers" ), }, Inspector: { IS_WIDGET_CREATION_TRACKED: createRPCMethod( RPCPrefix.INSPECTOR, "isWidgetCreationTracked" ), GET_SELECTED_SUMMARY_WIDGET: createRPCMethod( RPCPrefix.INSPECTOR, "getSelectedSummaryWidget" ), GET_SELECTED_WIDGET: createRPCMethod( RPCPrefix.INSPECTOR, "getSelectedWidget" ), GET_DETAILS_SUBTREE: createRPCMethod( RPCPrefix.INSPECTOR, "getDetailsSubtree" ), SCREENSHOT: createRPCMethod(RPCPrefix.INSPECTOR, "screenshot"), GET_ROOT_WIDGET: createRPCMethod(RPCPrefix.INSPECTOR, "getRootWidget"), GET_WIDGET_TREE: createRPCMethod(RPCPrefix.INSPECTOR, "getRootWidgetTree"), GET_PROPERTIES: createRPCMethod(RPCPrefix.INSPECTOR, "getProperties"), GET_CHILDREN: createRPCMethod(RPCPrefix.INSPECTOR, "getChildren"), SET_SELECTION_BY_ID: createRPCMethod( RPCPrefix.INSPECTOR, "setSelectionById" ), GET_PARENT_CHAIN: createRPCMethod(RPCPrefix.INSPECTOR, "getParentChain"), GET_CHILDREN_SUMMARY_TREE: createRPCMethod( RPCPrefix.INSPECTOR, "getChildrenSummaryTree" ), GET_CHILDREN_DETAILS_SUBTREE: createRPCMethod( RPCPrefix.INSPECTOR, "getChildrenDetailsSubtree" ), GET_ROOT_WIDGET_SUMMARY_TREE: createRPCMethod( RPCPrefix.INSPECTOR, "getRootWidgetSummaryTree" ), GET_ROOT_WIDGET_SUMMARY_TREE_WITH_PREVIEWS: createRPCMethod( RPCPrefix.INSPECTOR, "getRootWidgetSummaryTreeWithPreviews" ), TRACK_REBUILDS: createRPCMethod( RPCPrefix.INSPECTOR, "trackRebuildDirtyWidgets" ), STRUCTURED_ERRORS: createRPCMethod(RPCPrefix.INSPECTOR, "structuredErrors"), SHOW: createRPCMethod(RPCPrefix.INSPECTOR, "show"), WIDGET_LOCATION_ID_MAP: createRPCMethod( RPCPrefix.INSPECTOR, "widgetLocationIdMap" ), TRACK_REPAINT_WIDGETS: createRPCMethod( RPCPrefix.INSPECTOR, "trackRepaintWidgets" ), DISPOSE_ALL_GROUPS: createRPCMethod( RPCPrefix.INSPECTOR, "disposeAllGroups" ), DISPOSE_GROUP: createRPCMethod(RPCPrefix.INSPECTOR, "disposeGroup"), IS_WIDGET_TREE_READY: createRPCMethod( RPCPrefix.INSPECTOR, "isWidgetTreeReady" ), DISPOSE_ID: createRPCMethod(RPCPrefix.INSPECTOR, "disposeId"), SET_PUB_ROOT_DIRECTORIES: createRPCMethod( RPCPrefix.INSPECTOR, "setPubRootDirectories" ), ADD_PUB_ROOT_DIRECTORIES: createRPCMethod( RPCPrefix.INSPECTOR, "addPubRootDirectories" ), REMOVE_PUB_ROOT_DIRECTORIES: createRPCMethod( RPCPrefix.INSPECTOR, "removePubRootDirectories" ), GET_PUB_ROOT_DIRECTORIES: createRPCMethod( RPCPrefix.INSPECTOR, "getPubRootDirectories" ), }, Performance: { SHOW_OVERLAY: createRPCMethod(RPCPrefix.FLUTTER, "showPerformanceOverlay"), PROFILE_WIDGETS: createRPCMethod(RPCPrefix.FLUTTER, "profileWidgetBuilds"), PROFILE_USER_WIDGETS: createRPCMethod( RPCPrefix.FLUTTER, "profileUserWidgetBuilds" ), PROFILE_PLATFORM_CHANNELS: createRPCMethod( RPCPrefix.FLUTTER, "profilePlatformChannels" ), PROFILE_RENDER_OBJECT_PAINTS: createRPCMethod( RPCPrefix.FLUTTER, "profileRenderObjectPaints" ), PROFILE_RENDER_OBJECT_LAYOUTS: createRPCMethod( RPCPrefix.FLUTTER, "profileRenderObjectLayouts" ), }, Layout: { GET_EXPLORER_NODE: createRPCMethod( RPCPrefix.INSPECTOR, "getLayoutExplorerNode" ), SET_FLEX_FIT: createRPCMethod(RPCPrefix.INSPECTOR, "setFlexFit"), SET_FLEX_FACTOR: createRPCMethod(RPCPrefix.INSPECTOR, "setFlexFactor"), SET_FLEX_PROPERTIES: createRPCMethod( RPCPrefix.INSPECTOR, "setFlexProperties" ), }, Isar: { LIST_INSTANCES: createRPCMethod(RPCPrefix.ISAR, "listInstances"), GET_SCHEMAS: createRPCMethod(RPCPrefix.ISAR, "getSchemas"), WATCH_INSTANCE: createRPCMethod(RPCPrefix.ISAR, "watchInstance"), EXECUTE_QUERY: createRPCMethod(RPCPrefix.ISAR, "executeQuery"), DELETE_QUERY: createRPCMethod(RPCPrefix.ISAR, "deleteQuery"), IMPORT_JSON: createRPCMethod(RPCPrefix.ISAR, "importJson"), EDIT_PROPERTY: createRPCMethod(RPCPrefix.ISAR, "editProperty"), }, }; class FlutterInspectorServer { private server: Server; private port: number; private logLevel: LogLevel; private wsConnections: Map<number, WebSocket> = new Map(); private pendingRequests: Map< string, { resolve: Function; reject: Function; method: string } > = new Map(); private messageId = 0; constructor() { this.port = argv.port; this.logLevel = argv["log-level"] as LogLevel; this.server = new Server( { name: "flutter-inspector", version: "0.1.0", }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); this.setupErrorHandling(); } private generateId(): string { return `${Date.now()}_${this.messageId++}`; } private async connectWebSocket(port: number): Promise<WebSocket> { if (this.wsConnections.has(port)) { const ws = this.wsConnections.get(port)!; if (ws.readyState === WebSocket.OPEN) { return ws; } this.wsConnections.delete(port); } return new Promise((resolve, reject) => { const wsUrl = `ws://localhost:${port}/ws`; const ws = new WebSocket(wsUrl); ws.onopen = () => { this.log("debug", `WebSocket connected to ${wsUrl}`); this.wsConnections.set(port, ws); resolve(ws); }; ws.onerror = (error) => { this.log("error", `WebSocket error for ${wsUrl}:`, error); reject(error); }; ws.onclose = () => { this.log("debug", `WebSocket closed for ${wsUrl}`); this.wsConnections.delete(port); }; ws.onmessage = (event) => { try { const response = JSON.parse( event.data.toString() ) as WebSocketResponse; if (response.id) { const request = this.pendingRequests.get(response.id); if (request) { if (response.error) { request.reject(new Error(response.error.message)); } else { request.resolve(response.result); } this.pendingRequests.delete(response.id); } } } catch (error) { this.log("error", "Error parsing WebSocket message:", error); } }; }); } private async sendWebSocketRequest( port: number, method: string, params: Record<string, unknown> = {} ): Promise<unknown> { const ws = await this.connectWebSocket(port); const id = this.generateId(); const request: WebSocketRequest = { jsonrpc: "2.0", id, method, params, }; return new Promise((resolve, reject) => { this.pendingRequests.set(id, { resolve, reject, method }); ws.send(JSON.stringify(request)); }); } private setupErrorHandling() { this.server.onerror = (error) => this.log("error", "[MCP Error]", error); process.on("SIGINT", async () => { // Close all WebSocket connections for (const ws of this.wsConnections.values()) { ws.close(); } await this.server.close(); process.exit(0); }); } private log(level: LogLevel, ...args: unknown[]) { const levels: LogLevel[] = ["error", "warn", "info", "debug"]; if (levels.indexOf(level) <= levels.indexOf(this.logLevel)) { switch (level) { case "error": console.error(...args); break; case "warn": console.warn(...args); break; case "info": console.info(...args); break; case "debug": console.debug(...args); break; } } } private async getActivePorts(): Promise<FlutterPort[]> { try { const { stdout } = await execAsync("lsof -i -P -n | grep LISTEN"); const ports: FlutterPort[] = []; const lines = stdout.split("\n"); for (const line of lines) { if ( line.toLowerCase().includes("dart") || line.toLowerCase().includes("flutter") ) { const parts = line.split(/\s+/); const pid = parts[1]; const command = parts[0]; const addressPart = parts[8]; const portMatch = addressPart.match(/:(\d+)$/); if (portMatch) { ports.push({ port: parseInt(portMatch[1], 10), pid, command, }); } } } return ports; } catch (error) { this.log("error", "Error getting active ports:", error); return []; } } private async invokeFlutterMethod( port: number, method: string, params: Record<string, unknown> = {} ): Promise<unknown> { try { const result = await this.sendWebSocketRequest(port, method, params); return result; } catch (error) { this.log("error", `Error invoking Flutter method ${method}:`, error); throw error; } } private async getFlutterIsolate(port: number): Promise<string> { const vmInfo = (await this.invokeFlutterMethod(port, "getVM")) as VMInfo; const isolates = vmInfo.isolates; // Find Flutter isolate by checking for Flutter extension RPCs for (const isolateRef of isolates) { const isolate = (await this.invokeFlutterMethod(port, "getIsolate", { isolateId: isolateRef.id, })) as IsolateResponse; // Check if this isolate has Flutter extensions const extensionRPCs = isolate.extensionRPCs || []; if (extensionRPCs.some((ext: string) => ext.startsWith("ext.flutter"))) { return isolateRef.id; } } throw new McpError( ErrorCode.InternalError, "No Flutter isolate found in the application" ); } private async invokeFlutterExtension( port: number, method: string, params?: Record<string, unknown> ): Promise<unknown> { const isolateId = await this.getFlutterIsolate(port); return this.invokeFlutterMethod(port, method, { ...params, isolateId, }); } private async verifyFlutterDebugMode(port: number): Promise<void> { const vmInfo = (await this.invokeFlutterMethod(port, "getVM")) as VMInfo; const isolateId = await this.getFlutterIsolate(port); const isolateInfo = (await this.invokeFlutterMethod(port, "getIsolate", { isolateId, })) as IsolateResponse; if ( !isolateInfo.extensionRPCs?.includes("ext.flutter.debugDumpRenderTree") ) { throw new McpError( ErrorCode.InternalError, "Flutter app must be running in debug mode to inspect the render tree" ); } } private setupToolHandlers() { // Default port for Flutter/Dart processes const DEFAULT_FLUTTER_PORT = 8181; this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ // Utility Methods (Not direct RPC calls) { name: "get_active_ports", description: "Utility: Get list of ports where Flutter/Dart processes are listening. This is a local utility, not a Flutter RPC method.", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "get_supported_protocols", description: "Utility: Get supported protocols from a Flutter app. This is a VM service method, not a Flutter RPC. Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, }, required: [], }, }, { name: "get_vm_info", description: "Utility: Get VM information from a Flutter app. This is a VM service method, not a Flutter RPC. Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, }, required: [], }, }, { name: "get_extension_rpcs", description: "Utility: List all available extension RPCs in the Flutter app. This is a helper tool for discovering available methods.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, isolateId: { type: "string", description: "Optional specific isolate ID to check. If not provided, checks all isolates", }, isRawResponse: { type: "boolean", description: "If true, returns the raw response from the VM service without processing", default: false, }, }, required: [], }, }, // Debug Methods (ext.flutter.debug*) { name: "debug_dump_render_tree", description: "RPC: Dump the render tree (ext.flutter.debugDumpRenderTree). Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, }, required: [], }, }, { name: "debug_dump_layer_tree", description: "RPC: Dump the layer tree (ext.flutter.debugDumpLayerTree). Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, }, required: [], }, }, { name: "debug_dump_semantics_tree", description: "RPC: Dump the semantics tree (ext.flutter.debugDumpSemanticsTreeInTraversalOrder). Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, }, required: [], }, }, { name: "debug_dump_semantics_tree_inverse", description: "RPC: Dump the semantics tree in inverse hit test order (ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, }, required: [], }, }, { name: "debug_paint_baselines_enabled", description: "RPC: Toggle baseline paint debugging (ext.flutter.debugPaintBaselinesEnabled). Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, enabled: { type: "boolean", description: "Whether to enable or disable baseline paint debugging", }, }, required: ["enabled"], }, }, { name: "debug_dump_focus_tree", description: "RPC: Dump the focus tree (ext.flutter.debugDumpFocusTree)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, }, required: [], }, }, { name: "debug_disable_physical_shape_layers", description: "RPC: Toggle physical shape layers debugging (ext.flutter.debugDisablePhysicalShapeLayers)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, enabled: { type: "boolean", description: "Whether to enable or disable physical shape layers", }, }, required: ["enabled"], }, }, { name: "debug_disable_opacity_layers", description: "RPC: Toggle opacity layers debugging (ext.flutter.debugDisableOpacityLayers). Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, enabled: { type: "boolean", description: "Whether to enable or disable opacity layers", default: false, }, }, required: ["enabled"], }, }, // Inspector Methods (ext.flutter.inspector.*) { name: "inspector_screenshot", description: "RPC: Take a screenshot of the Flutter app (ext.flutter.inspector.screenshot). Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, }, required: [], }, }, { name: "inspector_get_layout_explorer_node", description: "RPC: Get layout explorer information for a widget (ext.flutter.inspector.getLayoutExplorerNode). Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, objectId: { type: "string", description: "ID of the widget to inspect", }, }, required: ["objectId"], }, }, { name: "inspector_track_rebuild_dirty_widgets", description: "RPC: Track widget rebuilds to identify performance issues (ext.flutter.inspector.trackRebuildDirtyWidgets). Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, enabled: { type: "boolean", description: "Whether to enable or disable rebuild tracking", }, }, required: ["enabled"], }, }, { name: "inspector_set_selection_by_id", description: "RPC: Set the selected widget by ID (ext.flutter.inspector.setSelectionById). Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, selectionId: { type: "string", description: "ID of the widget to select", }, }, required: ["selectionId"], }, }, { name: "inspector_get_parent_chain", description: "RPC: Get the parent chain for a widget (ext.flutter.inspector.getParentChain). Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, objectId: { type: "string", description: "ID of the widget to get parent chain for", }, }, required: ["objectId"], }, }, { name: "inspector_get_children_summary_tree", description: "RPC: Get the children summary tree for a widget (ext.flutter.inspector.getChildrenSummaryTree). Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, objectId: { type: "string", description: "ID of the widget to get children summary tree for", }, }, required: ["objectId"], }, }, { name: "inspector_get_children_details_subtree", description: "RPC: Get the children details subtree for a widget (ext.flutter.inspector.getChildrenDetailsSubtree)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, objectId: { type: "string", description: "ID of the widget to get children details subtree for", }, }, required: ["objectId"], }, }, { name: "inspector_get_root_widget_summary_tree", description: "RPC: Get the root widget summary tree (ext.flutter.inspector.getRootWidgetSummaryTree)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, }, required: [], }, }, { name: "inspector_get_root_widget_summary_tree_with_previews", description: "RPC: Get the root widget summary tree with previews from the Flutter app. This provides a hierarchical view of the widget tree with preview information.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, }, required: [], }, }, { name: "inspector_get_details_subtree", description: "RPC: Get the details subtree for a widget. This provides detailed information about the widget and its descendants. Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, objectId: { type: "string", description: "ID of the widget to get details for", }, }, required: ["objectId"], }, }, { name: "inspector_get_selected_widget", description: "RPC: Get information about the currently selected widget in the Flutter app. Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, }, required: [], }, }, { name: "inspector_get_selected_summary_widget", description: "RPC: Get summary information about the currently selected widget in the Flutter app. Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, }, required: [], }, }, { name: "inspector_is_widget_creation_tracked", description: "RPC: Check if widget creation tracking is enabled in the Flutter app.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, }, required: [], }, }, // DartIO Methods (ext.dart.io.*) { name: "dart_io_socket_profiling_enabled", description: "RPC: Enable or disable socket profiling. Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, enabled: { type: "boolean", description: "Whether to enable or disable socket profiling", }, }, required: ["enabled"], }, }, { name: "dart_io_http_enable_timeline_logging", description: "RPC: Enable or disable HTTP timeline logging. Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, enabled: { type: "boolean", description: "Whether to enable or disable HTTP timeline logging", }, }, required: ["enabled"], }, }, { name: "dart_io_get_version", description: "RPC: Get Flutter version information (ext.dart.io.getVersion)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, }, required: [], }, }, { name: "dart_io_get_open_files", description: "RPC: Get list of currently open files in the Flutter app", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, }, required: [], }, }, { name: "dart_io_get_open_file_by_id", description: "RPC: Get details of a specific open file by its ID", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, fileId: { type: "string", description: "ID of the file to get details for", }, }, required: ["fileId"], }, }, // Stream Methods { name: "stream_listen", description: "RPC: Subscribe to a Flutter event stream. This is a VM service method for event monitoring. Connects to the default Flutter debug port (8181) unless specified otherwise.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Optional: Custom port number if not using default Flutter debug port 8181", }, streamId: { type: "string", description: "Stream ID to subscribe to", enum: [ "Debug", "Isolate", "VM", "GC", "Timeline", "Logging", "Service", "HeapSnapshot", ], }, }, required: ["streamId"], }, }, { name: "dart_io_get_http_profile_request", description: "RPC: Get details of a specific HTTP request from the profile", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, requestId: { type: "string", description: "ID of the HTTP request to get details for", }, }, required: ["requestId"], }, }, { name: "flutter_core_invert_oversized_images", description: "RPC: Toggle inverting of oversized images for debugging", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, enabled: { type: "boolean", description: "Whether to enable or disable inverting of oversized images", }, }, required: ["enabled"], }, }, { name: "debug_allow_banner", description: "RPC: Toggle the debug banner in the Flutter app", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, enabled: { type: "boolean", description: "Whether to show or hide the debug banner", }, }, required: ["enabled"], }, }, { name: "flutter_core_did_send_first_frame_event", description: "RPC: Check if the first frame event has been sent", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, }, required: [], }, }, { name: "flutter_core_did_send_first_frame_rasterized_event", description: "RPC: Check if the first frame has been rasterized", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, }, required: [], }, }, { name: "flutter_core_platform_override", description: "RPC: Override the platform for the Flutter app", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, platform: { type: "string", description: "Platform to override to (android, ios, fuchsia, linux, macOS, windows, or null to reset)", enum: [ "android", "ios", "fuchsia", "linux", "macOS", "windows", null, ], }, }, required: ["platform"], }, }, { name: "flutter_core_brightness_override", description: "RPC: Override the brightness for the Flutter app", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, brightness: { type: "string", description: "Brightness to override to (light, dark, or null to reset)", enum: ["light", "dark", null], }, }, required: ["brightness"], }, }, { name: "flutter_core_time_dilation", description: "RPC: Set the time dilation factor for animations in the Flutter app", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, dilation: { type: "number", description: "Time dilation factor (1.0 is normal speed, >1.0 is slower, <1.0 is faster)", minimum: 0, }, }, required: ["dilation"], }, }, { name: "flutter_core_evict", description: "RPC: Evict an asset from the Flutter app's cache", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, asset: { type: "string", description: "Asset path to evict from the cache", }, }, required: ["asset"], }, }, { name: "flutter_core_profile_platform_channels", description: "RPC: Enable or disable profiling of platform channels", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, enabled: { type: "boolean", description: "Whether to enable or disable platform channel profiling", }, }, required: ["enabled"], }, }, { name: "debug_disable_clip_layers", description: "RPC: Toggle disabling of clip layers in the Flutter app", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, enabled: { type: "boolean", description: "Whether to enable or disable clip layers", }, }, required: ["enabled"], }, }, { name: "debug_disable_physical_shape_layers", description: "RPC: Toggle physical shape layers debugging (ext.flutter.debugDisablePhysicalShapeLayers)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, enabled: { type: "boolean", description: "Whether to enable or disable physical shape layers", }, }, required: ["enabled"], }, }, { name: "debug_disable_opacity_layers", description: "RPC: Toggle opacity layers debugging (ext.flutter.debugDisableOpacityLayers)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, enabled: { type: "boolean", description: "Whether to enable or disable opacity layers", }, }, required: ["enabled"], }, }, { name: "repaint_rainbow", description: "RPC: Toggle repaint rainbow debugging (ext.flutter.repaintRainbow)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, enabled: { type: "boolean", description: "Whether to enable or disable repaint rainbow debugging", }, }, required: ["enabled"], }, }, { name: "inspector_structured_errors", description: "RPC: Enable or disable structured error reporting in the Flutter app.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, enabled: { type: "boolean", description: "Whether to enable or disable structured error reporting", }, }, required: ["enabled"], }, }, { name: "inspector_show", description: "RPC: Show specific widget details in the Flutter app inspector.", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, options: { type: "object", description: "Options for showing widget details", properties: { objectId: { type: "string", description: "ID of the widget to show", }, groupName: { type: "string", description: "Optional group name for the widget", }, subtreeDepth: { type: "number", description: "Optional depth to show the widget subtree", }, }, required: ["objectId"], }, }, required: ["options"], }, }, { name: "inspector_widget_location_id_map", description: "RPC: Get a mapping of widget IDs to their source code locations (ext.flutter.inspector.widgetLocationIdMap)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, }, required: [], }, }, { name: "inspector_track_repaint_widgets", description: "RPC: Track widget repaints to identify rendering performance issues (ext.flutter.inspector.trackRepaintWidgets)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, enabled: { type: "boolean", description: "Whether to enable or disable repaint tracking", }, }, required: ["enabled"], }, }, { name: "inspector_dispose_all_groups", description: "RPC: Dispose all inspector groups to free up memory (ext.flutter.inspector.disposeAllGroups)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, }, required: [], }, }, { name: "inspector_dispose_group", description: "RPC: Dispose a specific inspector group to free up memory (ext.flutter.inspector.disposeGroup)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, groupId: { type: "string", description: "ID of the group to dispose", }, }, required: ["groupId"], }, }, { name: "inspector_is_widget_tree_ready", description: "RPC: Check if the widget tree is ready for inspection (ext.flutter.inspector.isWidgetTreeReady)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, }, required: [], }, }, { name: "inspector_dispose_id", description: "RPC: Dispose a specific widget ID to free up memory (ext.flutter.inspector.disposeId)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, id: { type: "string", description: "ID of the widget to dispose", }, }, required: ["id"], }, }, { name: "inspector_set_pub_root_directories", description: "RPC: Set the root directories for pub packages (ext.flutter.inspector.setPubRootDirectories)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, directories: { type: "array", items: { type: "string", }, description: "List of root directories for pub packages", }, }, required: ["directories"], }, }, { name: "inspector_add_pub_root_directories", description: "RPC: Add additional root directories for pub packages (ext.flutter.inspector.addPubRootDirectories)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, directories: { type: "array", items: { type: "string", }, description: "List of root directories to add for pub packages", }, }, required: ["directories"], }, }, { name: "inspector_remove_pub_root_directories", description: "RPC: Remove root directories from pub packages (ext.flutter.inspector.removePubRootDirectories)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, directories: { type: "array", items: { type: "string", }, description: "List of root directories to remove from pub packages", }, }, required: ["directories"], }, }, { name: "inspector_get_pub_root_directories", description: "RPC: Get the list of root directories for pub packages (ext.flutter.inspector.getPubRootDirectories)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, }, required: [], }, }, { name: "layout_set_flex_fit", description: "RPC: Set the flex fit property of a flex child widget (ext.flutter.inspector.setFlexFit)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, objectId: { type: "string", description: "ID of the flex child widget", }, fit: { type: "string", description: "Flex fit value to set (tight or loose)", enum: ["tight", "loose"], }, }, required: ["objectId", "fit"], }, }, { name: "layout_set_flex_factor", description: "RPC: Set the flex factor of a flex child widget (ext.flutter.inspector.setFlexFactor)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, objectId: { type: "string", description: "ID of the flex child widget", }, factor: { type: "number", description: "Flex factor value to set (must be non-negative)", minimum: 0, }, }, required: ["objectId", "factor"], }, }, { name: "layout_set_flex_properties", description: "RPC: Set multiple flex properties of a flex child widget (ext.flutter.inspector.setFlexProperties)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, objectId: { type: "string", description: "ID of the flex child widget", }, properties: { type: "object", description: "Flex properties to set", properties: { fit: { type: "string", description: "Flex fit value (tight or loose)", enum: ["tight", "loose"], }, factor: { type: "number", description: "Flex factor value (must be non-negative)", minimum: 0, }, }, additionalProperties: false, }, }, required: ["objectId", "properties"], }, }, { name: "performance_profile_render_object_paints", description: "RPC: Enable or disable profiling of render object paint operations (ext.flutter.profileRenderObjectPaints)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, enabled: { type: "boolean", description: "Whether to enable or disable render object paint profiling", }, }, required: ["enabled"], }, }, { name: "performance_profile_render_object_layouts", description: "RPC: Enable or disable profiling of render object layout operations (ext.flutter.profileRenderObjectLayouts)", inputSchema: { type: "object", properties: { port: { type: "number", description: "Port number where the Flutter app is running (defaults to 8181)", }, enabled: { type: "boolean", description: "Whether to enable or disable render object layout profiling", }, }, required: ["enabled"], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const handlePortParam = () => { const port = request.params.arguments?.port as number | undefined; return port || DEFAULT_FLUTTER_PORT; }; const wrapResponse = (promise: Promise<unknown>) => { return promise .then((result) => ({ content: [{ type: "text", text: JSON.stringify(result, null, 2) }], })) .catch((error: Error) => ({ content: [{ type: "text", text: `Error: ${error.message}` }], isError: true, })); }; switch (request.params.name) { case "get_active_ports": { const ports = await this.getActivePorts(); return { content: [ { type: "text", text: JSON.stringify(ports, null, 2), }, ], }; } case "get_supported_protocols": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterMethod(port, "getSupportedProtocols") ); } case "get_vm_info": { const port = handlePortParam(); return wrapResponse(this.invokeFlutterMethod(port, "getVM")); } case "debug_dump_render_tree": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Debug.DUMP_RENDER_TREE) ); } case "debug_dump_layer_tree": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Debug.DUMP_LAYER_TREE) ); } case "debug_dump_semantics_tree": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Debug.DUMP_SEMANTICS) ); } case "debug_dump_semantics_tree_inverse": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Debug.DUMP_SEMANTICS_INVERSE ) ); } case "toggle_debug_paint": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean; }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Debug.DEBUG_PAINT, { enabled, }) ); } case "get_flutter_version": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Debug.DUMP_APP) ); } case "stream_listen": { const port = handlePortParam(); const { streamId } = request.params.arguments as { streamId: string; }; return wrapResponse( this.invokeFlutterMethod(port, "streamListen", { streamId }) ); } case "get_widget_tree": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Debug.DUMP_APP) ); } case "get_widget_details": { const port = handlePortParam(); const { objectId } = request.params.arguments as { objectId: string }; if (!objectId) { throw new McpError( ErrorCode.InvalidParams, "objectId parameter is required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.GET_PROPERTIES, { arg: { objectId }, } ) ); } case "get_performance_stats": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); // First enable stats collection await this.invokeFlutterExtension( port, FlutterRPC.Performance.PROFILE_WIDGETS, { enabled: true, } ); // Then get the stats return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Performance.PROFILE_USER_WIDGETS, { enabled: true, } ) ); } case "debug_paint_size": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Debug.DEBUG_PAINT, { enabled, }) ); } case "debug_paint_baselines": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Debug.DEBUG_PAINT_BASELINES, { enabled, } ) ); } case "inspector_track_rebuild_dirty_widgets": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.TRACK_REBUILDS, { enabled, } ) ); } case "get_extension_rpcs": { const port = handlePortParam(); const { isolateId, isRawResponse = false } = (request.params.arguments as { isolateId?: string; isRawResponse?: boolean; }) || {}; const vmInfo = (await this.invokeFlutterMethod( port, "getVM" )) as VMInfo; const isolates = vmInfo.isolates; if (isolateId) { const isolate = (await this.invokeFlutterMethod( port, "getIsolate", { isolateId, } )) as IsolateResponse; if (isRawResponse) { return { content: [ { type: "text", text: JSON.stringify(isolate, null, 2), }, ], }; } return { content: [ { type: "text", text: JSON.stringify(isolate.extensionRPCs || [], null, 2), }, ], }; } if (isRawResponse) { const allIsolates = await Promise.all( isolates.map((isolateRef) => this.invokeFlutterMethod(port, "getIsolate", { isolateId: isolateRef.id, }) ) ); return { content: [ { type: "text", text: JSON.stringify(allIsolates, null, 2), }, ], }; } const allExtensions: string[] = []; for (const isolateRef of isolates) { const isolate = (await this.invokeFlutterMethod( port, "getIsolate", { isolateId: isolateRef.id, } )) as IsolateResponse; if (isolate.extensionRPCs) { allExtensions.push(...isolate.extensionRPCs); } } return { content: [ { type: "text", text: JSON.stringify([...new Set(allExtensions)], null, 2), }, ], }; } case "take_screenshot": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Inspector.SCREENSHOT) ); } case "get_focus_tree": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Debug.DUMP_FOCUS_TREE) ); } case "profile_user_widgets": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Performance.PROFILE_USER_WIDGETS, { enabled, } ) ); } case "get_layout_explorer": { const port = handlePortParam(); const { objectId } = request.params.arguments as { objectId: string }; await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Layout.GET_EXPLORER_NODE, { arg: { objectId }, } ) ); } case "schedule_frame": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.UI.SCHEDULE_FRAME) ); } case "reinitialize_shader": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.UI.REINITIALIZE_SHADER) ); } case "impeller_enabled": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.UI.IMPELLER_ENABLED) ); } case "dart_io_socket_profiling_enabled": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.DartIO.SOCKET_PROFILING_ENABLED, { enabled, } ) ); } case "dart_io_http_enable_timeline_logging": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.DartIO.HTTP_TIMELINE_LOGGING, { enabled, } ) ); } case "dart_io_get_open_files": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.DartIO.GET_OPEN_FILES) ); } case "get_socket_profile": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.DartIO.GET_SOCKET_PROFILE ) ); } case "clear_socket_profile": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.DartIO.CLEAR_SOCKET_PROFILE ) ); } case "get_http_profile": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.DartIO.GET_HTTP_PROFILE ) ); } case "clear_http_profile": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.DartIO.CLEAR_HTTP_PROFILE ) ); } case "list_isar_instances": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Isar.LIST_INSTANCES) ); } case "get_isar_schemas": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Isar.GET_SCHEMAS) ); } case "watch_isar_instance": { const port = handlePortParam(); const { instanceId } = request.params.arguments as { instanceId: string; }; if (!instanceId) { throw new McpError( ErrorCode.InvalidParams, "instanceId parameter is required" ); } return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Isar.WATCH_INSTANCE, { instanceId, }) ); } case "execute_isar_query": { const port = handlePortParam(); const { query } = request.params.arguments as { query: string }; if (!query) { throw new McpError( ErrorCode.InvalidParams, "query parameter is required" ); } return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Isar.EXECUTE_QUERY, { query, }) ); } case "delete_isar_query": { const port = handlePortParam(); const { queryId } = request.params.arguments as { queryId: string }; if (!queryId) { throw new McpError( ErrorCode.InvalidParams, "queryId parameter is required" ); } return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Isar.DELETE_QUERY, { queryId, }) ); } case "import_isar_json": { const port = handlePortParam(); const { json } = request.params.arguments as { json: string }; if (!json) { throw new McpError( ErrorCode.InvalidParams, "json parameter is required" ); } return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Isar.IMPORT_JSON, { json, }) ); } case "edit_isar_property": { const port = handlePortParam(); const { property, value } = request.params.arguments as { property: string; value: unknown; }; if (!property) { throw new McpError( ErrorCode.InvalidParams, "property parameter is required" ); } return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Isar.EDIT_PROPERTY, { property, value, }) ); } case "dart_io_get_open_file_by_id": { const port = handlePortParam(); const { fileId } = request.params.arguments as { fileId: string }; if (!fileId) { throw new McpError( ErrorCode.InvalidParams, "fileId parameter is required" ); } return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.DartIO.GET_OPEN_FILE_BY_ID, { fileId, } ) ); } case "dart_io_get_http_profile_request": { const port = handlePortParam(); const { requestId } = request.params.arguments as { requestId: string; }; if (!requestId) { throw new McpError( ErrorCode.InvalidParams, "requestId parameter is required" ); } return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.DartIO.GET_HTTP_PROFILE_REQUEST, { requestId, } ) ); } case "inspector_screenshot": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Inspector.SCREENSHOT) ); } case "flutter_core_invert_oversized_images": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Core.INVERT_OVERSIZED_IMAGES, { enabled, } ) ); } case "debug_allow_banner": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Debug.DEBUG_ALLOW_BANNER, { enabled, } ) ); } case "flutter_core_did_send_first_frame_event": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Core.DID_SEND_FIRST_FRAME_EVENT ) ); } case "flutter_core_did_send_first_frame_rasterized_event": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Core.DID_SEND_FIRST_FRAME_RASTERIZED_EVENT ) ); } case "flutter_core_platform_override": { const port = handlePortParam(); const { platform } = request.params.arguments as { platform: string | null; }; if ( platform !== null && ![ "android", "ios", "fuchsia", "linux", "macOS", "windows", ].includes(platform) ) { throw new McpError( ErrorCode.InvalidParams, "platform must be one of: android, ios, fuchsia, linux, macOS, windows, or null to reset" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Core.PLATFORM_OVERRIDE, { platform, } ) ); } case "flutter_core_brightness_override": { const port = handlePortParam(); const { brightness } = request.params.arguments as { brightness: string | null; }; if ( brightness !== null && !["light", "dark", null].includes(brightness) ) { throw new McpError( ErrorCode.InvalidParams, "brightness must be one of: light, dark, or null to reset" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Core.BRIGHTNESS_OVERRIDE, { brightness, } ) ); } case "flutter_core_time_dilation": { const port = handlePortParam(); const { dilation } = request.params.arguments as { dilation: number }; if (typeof dilation !== "number" || dilation < 0) { throw new McpError( ErrorCode.InvalidParams, "dilation must be a non-negative number" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Core.TIME_DILATION, { timeDilation: dilation, }) ); } case "flutter_core_evict": { const port = handlePortParam(); const { asset } = request.params.arguments as { asset: string }; if (!asset) { throw new McpError( ErrorCode.InvalidParams, "asset parameter is required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Core.EVICT, { asset, }) ); } case "flutter_core_profile_platform_channels": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Core.PROFILE_PLATFORM_CHANNELS, { enabled, } ) ); } case "debug_disable_clip_layers": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Debug.DEBUG_DISABLE_CLIP_LAYERS, { enabled, } ) ); } case "debug_disable_physical_shape_layers": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Debug.DISABLE_PHYSICAL_SHAPE_LAYERS, { enabled, } ) ); } case "debug_disable_opacity_layers": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Debug.DEBUG_DISABLE_OPACITY_LAYERS, { enabled, } ) ); } case "repaint_rainbow": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Debug.REPAINT_RAINBOW, { enabled, } ) ); } case "inspector_get_layout_explorer_node": { const port = handlePortParam(); const { objectId } = request.params.arguments as { objectId: string }; if (!objectId) { throw new McpError( ErrorCode.InvalidParams, "objectId parameter is required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Layout.GET_EXPLORER_NODE, { arg: { objectId }, } ) ); } case "inspector_set_selection_by_id": { const port = handlePortParam(); const { selectionId } = request.params.arguments as { selectionId: string; }; if (!selectionId) { throw new McpError( ErrorCode.InvalidParams, "selectionId parameter is required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.SET_SELECTION_BY_ID, { arg: { selectionId }, } ) ); } case "inspector_get_parent_chain": { const port = handlePortParam(); const { objectId } = request.params.arguments as { objectId: string }; if (!objectId) { throw new McpError( ErrorCode.InvalidParams, "objectId parameter is required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.GET_PARENT_CHAIN, { arg: { objectId }, } ) ); } case "inspector_get_children_summary_tree": { const port = handlePortParam(); const { objectId } = request.params.arguments as { objectId: string }; if (!objectId) { throw new McpError( ErrorCode.InvalidParams, "objectId parameter is required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.GET_CHILDREN_SUMMARY_TREE, { arg: { objectId }, } ) ); } case "inspector_get_children_details_subtree": { const port = handlePortParam(); const { objectId } = request.params.arguments as { objectId: string }; if (!objectId) { throw new McpError( ErrorCode.InvalidParams, "objectId parameter is required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.GET_CHILDREN_DETAILS_SUBTREE, { arg: { objectId }, } ) ); } case "inspector_get_root_widget_summary_tree": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.GET_ROOT_WIDGET_SUMMARY_TREE ) ); } case "inspector_get_root_widget_summary_tree_with_previews": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.GET_ROOT_WIDGET_SUMMARY_TREE_WITH_PREVIEWS ) ); } case "inspector_get_details_subtree": { const port = handlePortParam(); const { objectId } = request.params.arguments as { objectId: string }; if (!objectId) { throw new McpError( ErrorCode.InvalidParams, "objectId parameter is required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.GET_DETAILS_SUBTREE, { arg: { objectId }, } ) ); } case "inspector_get_selected_widget": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.GET_SELECTED_WIDGET ) ); } case "inspector_get_selected_summary_widget": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.GET_SELECTED_SUMMARY_WIDGET ) ); } case "inspector_is_widget_creation_tracked": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.IS_WIDGET_CREATION_TRACKED ) ); } case "inspector_structured_errors": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.STRUCTURED_ERRORS, { enabled, } ) ); } case "inspector_show": { const port = handlePortParam(); const { options } = request.params.arguments as { options: { objectId: string; groupName?: string; subtreeDepth?: number; }; }; if (!options || !options.objectId) { throw new McpError( ErrorCode.InvalidParams, "options.objectId parameter is required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Inspector.SHOW, { arg: options, }) ); } case "inspector_widget_location_id_map": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.WIDGET_LOCATION_ID_MAP ) ); } case "inspector_track_repaint_widgets": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.TRACK_REPAINT_WIDGETS, { enabled, } ) ); } case "inspector_dispose_all_groups": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.DISPOSE_ALL_GROUPS ) ); } case "inspector_dispose_group": { const port = handlePortParam(); const { groupId } = request.params.arguments as { groupId: string }; if (!groupId) { throw new McpError( ErrorCode.InvalidParams, "groupId parameter is required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.DISPOSE_GROUP, { arg: { groupId }, } ) ); } case "inspector_is_widget_tree_ready": { const port = handlePortParam(); await this.verifyFlutterDebugMode(port); const result = await this.invokeFlutterExtension( port, FlutterRPC.Inspector.IS_WIDGET_TREE_READY ); return wrapResponse(Promise.resolve(result)); } case "inspector_dispose_id": { const port = handlePortParam(); const { id } = request.params.arguments as { id: string }; if (!id) { throw new McpError( ErrorCode.InvalidParams, "id parameter is required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Inspector.DISPOSE_ID, { arg: { id }, }) ); } case "inspector_set_pub_root_directories": { const port = handlePortParam(); const { directories } = request.params.arguments as { directories: string[]; }; if (!directories || !Array.isArray(directories)) { throw new McpError( ErrorCode.InvalidParams, "directories parameter must be an array" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.SET_PUB_ROOT_DIRECTORIES, { directories, } ) ); } case "inspector_add_pub_root_directories": { const port = handlePortParam(); const { directories } = request.params.arguments as { directories: string[]; }; if (!directories || !Array.isArray(directories)) { throw new McpError( ErrorCode.InvalidParams, "directories parameter must be an array" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.ADD_PUB_ROOT_DIRECTORIES, { directories, } ) ); } case "inspector_remove_pub_root_directories": { const port = handlePortParam(); const { directories } = request.params.arguments as { directories: string[]; }; if (!directories || !Array.isArray(directories)) { throw new McpError( ErrorCode.InvalidParams, "directories parameter must be an array" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.REMOVE_PUB_ROOT_DIRECTORIES, { directories, } ) ); } case "inspector_get_pub_root_directories": { const port = handlePortParam(); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Inspector.GET_PUB_ROOT_DIRECTORIES ) ); } case "layout_set_flex_fit": { const port = handlePortParam(); const { objectId, fit } = request.params.arguments as { objectId: string; fit: string; }; if (!objectId || !fit) { throw new McpError( ErrorCode.InvalidParams, "objectId and fit parameters are required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension(port, FlutterRPC.Layout.SET_FLEX_FIT, { objectId, fit, }) ); } case "layout_set_flex_factor": { const port = handlePortParam(); const { objectId, factor } = request.params.arguments as { objectId: string; factor: number; }; if (!objectId || typeof factor !== "number" || factor < 0) { throw new McpError( ErrorCode.InvalidParams, "objectId and factor parameters are required and factor must be non-negative" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Layout.SET_FLEX_FACTOR, { objectId, factor, } ) ); } case "layout_set_flex_properties": { const port = handlePortParam(); const { objectId, properties } = request.params.arguments as { objectId: string; properties: { fit: string; factor: number; }; }; if ( !objectId || !properties || !properties.fit || !properties.factor ) { throw new McpError( ErrorCode.InvalidParams, "objectId and properties parameters are required" ); } await this.verifyFlutterDebugMode(port); return wrapResponse( this.invokeFlutterExtension( port, FlutterRPC.Layout.SET_FLEX_PROPERTIES, { objectId, properties, } ) ); } case "performance_profile_render_object_paints": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); const response = await this.invokeFlutterExtension( port, FlutterRPC.Performance.PROFILE_RENDER_OBJECT_PAINTS, { enabled, } ); return { content: [ { type: "text", text: `Render object paint profiling ${ enabled ? "enabled" : "disabled" }`, }, ], }; } case "performance_profile_render_object_layouts": { const port = handlePortParam(); const { enabled } = request.params.arguments as { enabled: boolean }; if (typeof enabled !== "boolean") { throw new McpError( ErrorCode.InvalidParams, "enabled parameter must be a boolean" ); } await this.verifyFlutterDebugMode(port); const response = await this.invokeFlutterExtension( port, FlutterRPC.Performance.PROFILE_RENDER_OBJECT_LAYOUTS, { enabled, } ); return { content: [ { type: "text", text: `Render object layout profiling ${ enabled ? "enabled" : "disabled" }`, }, ], }; } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); this.log( "info", `Flutter Inspector MCP server running on stdio, port ${this.port}` ); } } const server = new FlutterInspectorServer(); server.run().catch((error) => { console.error("Fatal error:", error); process.exit(1); });