From 47a35a81891f644a72f32d58a23fc0a0ab638ffe Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Fri, 13 Feb 2026 13:40:21 -0800 Subject: [PATCH 1/2] feat(desktop): add desktop automation MCP for Electron dev tooling Generic package that enables AI agents to interact with the running Electron app: take screenshots, inspect DOM, click elements, type text, send keyboard shortcuts, read console logs, and evaluate JS. Two roles in one package: - Library export: any Electron app imports createAutomationServer() in main process to start an HTTP automation server (dev-only) - MCP binary: standalone stdio server translating MCP tool calls into HTTP requests to the automation server Tools: take_screenshot, inspect_dom, click, type_text, send_keys, get_console_logs, evaluate_js, navigate, get_window_info --- .mcp.json | 4 + apps/desktop/package.json | 1 + apps/desktop/src/main/windows/main.ts | 8 + apps/desktop/src/shared/env.shared.ts | 2 + bun.lock | 24 +++ packages/desktop-mcp/package.json | 32 ++++ packages/desktop-mcp/src/bin.ts | 7 + packages/desktop-mcp/src/index.ts | 13 ++ packages/desktop-mcp/src/mcp/client/client.ts | 20 +++ packages/desktop-mcp/src/mcp/client/index.ts | 1 + packages/desktop-mcp/src/mcp/index.ts | 1 + packages/desktop-mcp/src/mcp/mcp-server.ts | 11 ++ .../desktop-mcp/src/mcp/tools/click/click.ts | 50 ++++++ .../desktop-mcp/src/mcp/tools/click/index.ts | 1 + .../src/mcp/tools/evaluate-js/evaluate-js.ts | 34 ++++ .../src/mcp/tools/evaluate-js/index.ts | 1 + .../get-console-logs/get-console-logs.ts | 63 +++++++ .../src/mcp/tools/get-console-logs/index.ts | 1 + .../tools/get-window-info/get-window-info.ts | 31 ++++ .../src/mcp/tools/get-window-info/index.ts | 1 + packages/desktop-mcp/src/mcp/tools/index.ts | 28 +++ .../src/mcp/tools/inspect-dom/index.ts | 1 + .../src/mcp/tools/inspect-dom/inspect-dom.ts | 59 +++++++ .../src/mcp/tools/navigate/index.ts | 1 + .../src/mcp/tools/navigate/navigate.ts | 37 ++++ .../src/mcp/tools/send-keys/index.ts | 1 + .../src/mcp/tools/send-keys/send-keys.ts | 34 ++++ .../src/mcp/tools/take-screenshot/index.ts | 1 + .../tools/take-screenshot/take-screenshot.ts | 44 +++++ .../src/mcp/tools/type-text/index.ts | 1 + .../src/mcp/tools/type-text/type-text.ts | 39 ++++ .../server/console-capture/console-capture.ts | 44 +++++ .../src/server/console-capture/index.ts | 1 + .../src/server/dom-inspector/dom-inspector.ts | 92 ++++++++++ .../src/server/dom-inspector/index.ts | 1 + .../src/server/handlers/click/click.ts | 90 ++++++++++ .../src/server/handlers/click/index.ts | 1 + .../handlers/console-logs/console-logs.ts | 36 ++++ .../src/server/handlers/console-logs/index.ts | 1 + .../src/server/handlers/dom/dom.ts | 26 +++ .../src/server/handlers/dom/index.ts | 1 + .../src/server/handlers/evaluate/evaluate.ts | 28 +++ .../src/server/handlers/evaluate/index.ts | 1 + .../desktop-mcp/src/server/handlers/index.ts | 9 + .../src/server/handlers/navigate/index.ts | 1 + .../src/server/handlers/navigate/navigate.ts | 36 ++++ .../src/server/handlers/screenshot/index.ts | 1 + .../server/handlers/screenshot/screenshot.ts | 37 ++++ .../src/server/handlers/send-keys/index.ts | 1 + .../server/handlers/send-keys/send-keys.ts | 95 ++++++++++ .../src/server/handlers/type/index.ts | 1 + .../src/server/handlers/type/type.ts | 60 +++++++ .../src/server/handlers/window-info/index.ts | 1 + .../handlers/window-info/window-info.ts | 24 +++ packages/desktop-mcp/src/server/index.ts | 1 + packages/desktop-mcp/src/server/server.ts | 51 ++++++ packages/desktop-mcp/src/zod.ts | 167 ++++++++++++++++++ packages/desktop-mcp/tsconfig.json | 11 ++ 58 files changed, 1370 insertions(+) create mode 100644 packages/desktop-mcp/package.json create mode 100755 packages/desktop-mcp/src/bin.ts create mode 100644 packages/desktop-mcp/src/index.ts create mode 100644 packages/desktop-mcp/src/mcp/client/client.ts create mode 100644 packages/desktop-mcp/src/mcp/client/index.ts create mode 100644 packages/desktop-mcp/src/mcp/index.ts create mode 100644 packages/desktop-mcp/src/mcp/mcp-server.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/click/click.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/click/index.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/evaluate-js/evaluate-js.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/evaluate-js/index.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/get-console-logs/get-console-logs.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/get-console-logs/index.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/get-window-info/get-window-info.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/get-window-info/index.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/index.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/inspect-dom/index.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/inspect-dom/inspect-dom.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/navigate/index.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/navigate/navigate.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/send-keys/index.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/send-keys/send-keys.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/take-screenshot/index.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/take-screenshot/take-screenshot.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/type-text/index.ts create mode 100644 packages/desktop-mcp/src/mcp/tools/type-text/type-text.ts create mode 100644 packages/desktop-mcp/src/server/console-capture/console-capture.ts create mode 100644 packages/desktop-mcp/src/server/console-capture/index.ts create mode 100644 packages/desktop-mcp/src/server/dom-inspector/dom-inspector.ts create mode 100644 packages/desktop-mcp/src/server/dom-inspector/index.ts create mode 100644 packages/desktop-mcp/src/server/handlers/click/click.ts create mode 100644 packages/desktop-mcp/src/server/handlers/click/index.ts create mode 100644 packages/desktop-mcp/src/server/handlers/console-logs/console-logs.ts create mode 100644 packages/desktop-mcp/src/server/handlers/console-logs/index.ts create mode 100644 packages/desktop-mcp/src/server/handlers/dom/dom.ts create mode 100644 packages/desktop-mcp/src/server/handlers/dom/index.ts create mode 100644 packages/desktop-mcp/src/server/handlers/evaluate/evaluate.ts create mode 100644 packages/desktop-mcp/src/server/handlers/evaluate/index.ts create mode 100644 packages/desktop-mcp/src/server/handlers/index.ts create mode 100644 packages/desktop-mcp/src/server/handlers/navigate/index.ts create mode 100644 packages/desktop-mcp/src/server/handlers/navigate/navigate.ts create mode 100644 packages/desktop-mcp/src/server/handlers/screenshot/index.ts create mode 100644 packages/desktop-mcp/src/server/handlers/screenshot/screenshot.ts create mode 100644 packages/desktop-mcp/src/server/handlers/send-keys/index.ts create mode 100644 packages/desktop-mcp/src/server/handlers/send-keys/send-keys.ts create mode 100644 packages/desktop-mcp/src/server/handlers/type/index.ts create mode 100644 packages/desktop-mcp/src/server/handlers/type/type.ts create mode 100644 packages/desktop-mcp/src/server/handlers/window-info/index.ts create mode 100644 packages/desktop-mcp/src/server/handlers/window-info/window-info.ts create mode 100644 packages/desktop-mcp/src/server/index.ts create mode 100644 packages/desktop-mcp/src/server/server.ts create mode 100644 packages/desktop-mcp/src/zod.ts create mode 100644 packages/desktop-mcp/tsconfig.json diff --git a/.mcp.json b/.mcp.json index 26823b5c97f..8851a2af1cb 100644 --- a/.mcp.json +++ b/.mcp.json @@ -23,6 +23,10 @@ "sentry": { "type": "http", "url": "https://mcp.sentry.dev/mcp" + }, + "desktop-automation": { + "command": "bun", + "args": ["run", "packages/desktop-mcp/src/bin.ts"] } } } diff --git a/apps/desktop/package.json b/apps/desktop/package.json index c349cf1ee4e..2e469bbc569 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -52,6 +52,7 @@ "@superset/agent": "workspace:*", "@superset/auth": "workspace:*", "@superset/db": "workspace:*", + "@superset/desktop-mcp": "workspace:*", "@superset/durable-session": "workspace:*", "@superset/local-db": "workspace:*", "@superset/shared": "workspace:*", diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts index 65b03ee0ff0..35fe9d9faf2 100644 --- a/apps/desktop/src/main/windows/main.ts +++ b/apps/desktop/src/main/windows/main.ts @@ -134,6 +134,14 @@ export async function MainWindow() { }); } + if (env.NODE_ENV === "development") { + const { createAutomationServer } = await import("@superset/desktop-mcp"); + createAutomationServer({ + getWindow, + port: env.DESKTOP_AUTOMATION_PORT, + }); + } + const server = notificationsApp.listen( env.DESKTOP_NOTIFICATIONS_PORT, "127.0.0.1", diff --git a/apps/desktop/src/shared/env.shared.ts b/apps/desktop/src/shared/env.shared.ts index 6d4959296e0..eef236938b1 100644 --- a/apps/desktop/src/shared/env.shared.ts +++ b/apps/desktop/src/shared/env.shared.ts @@ -20,6 +20,7 @@ const envSchema = z.object({ DESKTOP_VITE_PORT: z.coerce.number().default(5173), DESKTOP_NOTIFICATIONS_PORT: z.coerce.number().default(5174), ELECTRIC_PORT: z.coerce.number().default(5133), + DESKTOP_AUTOMATION_PORT: z.coerce.number().default(9223), // Workspace name for instance isolation SUPERSET_WORKSPACE_NAME: z.string().default("superset"), }); @@ -36,6 +37,7 @@ export const env = envSchema.parse({ DESKTOP_VITE_PORT: process.env.DESKTOP_VITE_PORT, DESKTOP_NOTIFICATIONS_PORT: process.env.DESKTOP_NOTIFICATIONS_PORT, ELECTRIC_PORT: process.env.ELECTRIC_PORT, + DESKTOP_AUTOMATION_PORT: process.env.DESKTOP_AUTOMATION_PORT, SUPERSET_WORKSPACE_NAME: process.env.SUPERSET_WORKSPACE_NAME, }); diff --git a/bun.lock b/bun.lock index caf60e653df..4db2a8b188a 100644 --- a/bun.lock +++ b/bun.lock @@ -124,6 +124,7 @@ "@superset/agent": "workspace:*", "@superset/auth": "workspace:*", "@superset/db": "workspace:*", + "@superset/desktop-mcp": "workspace:*", "@superset/durable-session": "workspace:*", "@superset/local-db": "workspace:*", "@superset/shared": "workspace:*", @@ -597,6 +598,27 @@ "typescript": "^5.9.3", }, }, + "packages/desktop-mcp": { + "name": "@superset/desktop-mcp", + "version": "0.1.0", + "bin": { + "desktop-mcp": "./src/bin.ts", + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.3", + "express": "^5.1.0", + "zod": "^4.3.5", + }, + "devDependencies": { + "@superset/typescript": "workspace:*", + "@types/express": "^5.0.5", + "@types/node": "^24.9.1", + "typescript": "^5.9.3", + }, + "peerDependencies": { + "electron": "*", + }, + }, "packages/durable-session": { "name": "@superset/durable-session", "version": "0.0.1", @@ -1997,6 +2019,8 @@ "@superset/desktop": ["@superset/desktop@workspace:apps/desktop"], + "@superset/desktop-mcp": ["@superset/desktop-mcp@workspace:packages/desktop-mcp"], + "@superset/docs": ["@superset/docs@workspace:apps/docs"], "@superset/durable-session": ["@superset/durable-session@workspace:packages/durable-session"], diff --git a/packages/desktop-mcp/package.json b/packages/desktop-mcp/package.json new file mode 100644 index 00000000000..3f9925f66e8 --- /dev/null +++ b/packages/desktop-mcp/package.json @@ -0,0 +1,32 @@ +{ + "name": "@superset/desktop-mcp", + "version": "0.1.0", + "private": true, + "type": "module", + "bin": { + "desktop-mcp": "./src/bin.ts" + }, + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit --emitDeclarationOnly false" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.3", + "express": "^5.1.0", + "zod": "^4.3.5" + }, + "peerDependencies": { + "electron": "*" + }, + "devDependencies": { + "@superset/typescript": "workspace:*", + "@types/express": "^5.0.5", + "@types/node": "^24.9.1", + "typescript": "^5.9.3" + } +} diff --git a/packages/desktop-mcp/src/bin.ts b/packages/desktop-mcp/src/bin.ts new file mode 100755 index 00000000000..3a5bd61a384 --- /dev/null +++ b/packages/desktop-mcp/src/bin.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { createMcpServer } from "./mcp/index.js"; + +const server = createMcpServer(); +const transport = new StdioServerTransport(); +await server.connect(transport); diff --git a/packages/desktop-mcp/src/index.ts b/packages/desktop-mcp/src/index.ts new file mode 100644 index 00000000000..dc97f2e1f38 --- /dev/null +++ b/packages/desktop-mcp/src/index.ts @@ -0,0 +1,13 @@ +export { createAutomationServer } from "./server/index.js"; +export type { + ClickResponse, + ConsoleLogEntry, + ConsoleLogsResponse, + DomElement, + DomResponse, + EvaluateResponse, + NavigateResponse, + ScreenshotResponse, + TypeResponse, + WindowInfoResponse, +} from "./zod.js"; diff --git a/packages/desktop-mcp/src/mcp/client/client.ts b/packages/desktop-mcp/src/mcp/client/client.ts new file mode 100644 index 00000000000..5cc51aab777 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/client/client.ts @@ -0,0 +1,20 @@ +const BASE_URL = `http://127.0.0.1:${process.env.DESKTOP_AUTOMATION_PORT || 9223}`; + +export async function automationFetch( + path: string, + options?: RequestInit, +): Promise { + const res = await fetch(`${BASE_URL}${path}`, { + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, + }); + if (!res.ok) { + throw new Error( + `Automation server error: ${res.status} ${await res.text()}`, + ); + } + return res.json() as Promise; +} diff --git a/packages/desktop-mcp/src/mcp/client/index.ts b/packages/desktop-mcp/src/mcp/client/index.ts new file mode 100644 index 00000000000..a2e2ce8e52d --- /dev/null +++ b/packages/desktop-mcp/src/mcp/client/index.ts @@ -0,0 +1 @@ +export { automationFetch } from "./client.js"; diff --git a/packages/desktop-mcp/src/mcp/index.ts b/packages/desktop-mcp/src/mcp/index.ts new file mode 100644 index 00000000000..e2015e3ecf5 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/index.ts @@ -0,0 +1 @@ +export { createMcpServer } from "./mcp-server.js"; diff --git a/packages/desktop-mcp/src/mcp/mcp-server.ts b/packages/desktop-mcp/src/mcp/mcp-server.ts new file mode 100644 index 00000000000..12bf26801ad --- /dev/null +++ b/packages/desktop-mcp/src/mcp/mcp-server.ts @@ -0,0 +1,11 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { registerTools } from "./tools/index.js"; + +export function createMcpServer(): McpServer { + const server = new McpServer( + { name: "desktop-automation", version: "0.1.0" }, + { capabilities: { tools: {} } }, + ); + registerTools(server); + return server; +} diff --git a/packages/desktop-mcp/src/mcp/tools/click/click.ts b/packages/desktop-mcp/src/mcp/tools/click/click.ts new file mode 100644 index 00000000000..bc757d950b2 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/click/click.ts @@ -0,0 +1,50 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import type { ClickResponse } from "../../../zod.js"; +import { automationFetch } from "../../client/index.js"; + +export function register(server: McpServer) { + server.registerTool( + "click", + { + description: + "Click on a UI element in the Electron app. Provide at least one targeting method: CSS selector, visible text, data-testid, or x/y coordinates. Use inspect_dom first to find element selectors.", + inputSchema: { + selector: z + .string() + .optional() + .describe("CSS selector of element to click"), + text: z + .string() + .optional() + .describe("Visible text content to find and click"), + testId: z.string().optional().describe("data-testid attribute value"), + x: z.number().optional().describe("X coordinate for click"), + y: z.number().optional().describe("Y coordinate for click"), + index: z + .number() + .int() + .min(0) + .default(0) + .describe("0-based index if multiple elements match (default 0)"), + fuzzy: z + .boolean() + .default(true) + .describe("Use fuzzy/partial text matching (default true)"), + }, + }, + async (args) => { + const data = await automationFetch("/click", { + method: "POST", + body: JSON.stringify(args), + }); + + const desc = data.element + ? `Clicked <${data.element.tag}> "${data.element.text}"` + : "Click sent"; + return { + content: [{ type: "text", text: desc }], + }; + }, + ); +} diff --git a/packages/desktop-mcp/src/mcp/tools/click/index.ts b/packages/desktop-mcp/src/mcp/tools/click/index.ts new file mode 100644 index 00000000000..4e1a05121a0 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/click/index.ts @@ -0,0 +1 @@ +export { register } from "./click.js"; diff --git a/packages/desktop-mcp/src/mcp/tools/evaluate-js/evaluate-js.ts b/packages/desktop-mcp/src/mcp/tools/evaluate-js/evaluate-js.ts new file mode 100644 index 00000000000..3bcff713f96 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/evaluate-js/evaluate-js.ts @@ -0,0 +1,34 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import type { EvaluateResponse } from "../../../zod.js"; +import { automationFetch } from "../../client/index.js"; + +export function register(server: McpServer) { + server.registerTool( + "evaluate_js", + { + description: + "Execute JavaScript code in the Electron app's renderer process and return the result. Use this as an escape hatch for anything not covered by other tools.", + inputSchema: { + code: z.string().describe("JavaScript code to execute in the renderer"), + }, + }, + async (args) => { + const data = await automationFetch("/evaluate", { + method: "POST", + body: JSON.stringify({ code: args.code }), + }); + return { + content: [ + { + type: "text", + text: + typeof data.result === "string" + ? data.result + : JSON.stringify(data.result, null, 2), + }, + ], + }; + }, + ); +} diff --git a/packages/desktop-mcp/src/mcp/tools/evaluate-js/index.ts b/packages/desktop-mcp/src/mcp/tools/evaluate-js/index.ts new file mode 100644 index 00000000000..bcf41a549e9 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/evaluate-js/index.ts @@ -0,0 +1 @@ +export { register } from "./evaluate-js.js"; diff --git a/packages/desktop-mcp/src/mcp/tools/get-console-logs/get-console-logs.ts b/packages/desktop-mcp/src/mcp/tools/get-console-logs/get-console-logs.ts new file mode 100644 index 00000000000..8fc1bd03ecc --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/get-console-logs/get-console-logs.ts @@ -0,0 +1,63 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import type { ConsoleLogsResponse } from "../../../zod.js"; +import { automationFetch } from "../../client/index.js"; + +const LEVEL_NAMES: Record = { + 0: "DEBUG", + 1: "LOG", + 2: "WARN", + 3: "ERROR", +}; + +export function register(server: McpServer) { + server.registerTool( + "get_console_logs", + { + description: + "Get buffered console output from the Electron app renderer process. Shows console.log, console.warn, console.error output. Critical for debugging runtime issues.", + inputSchema: { + level: z + .enum(["log", "warn", "error", "debug"]) + .optional() + .describe("Filter by log level"), + limit: z + .number() + .int() + .min(1) + .default(50) + .describe("Max entries to return (default 50)"), + clear: z + .boolean() + .default(false) + .describe("Clear buffer after reading"), + }, + }, + async (args) => { + const params = new URLSearchParams(); + if (args.level) params.set("level", args.level as string); + if (args.limit) params.set("limit", String(args.limit)); + if (args.clear) params.set("clear", "true"); + const qs = params.toString(); + + const data = await automationFetch( + `/console-logs${qs ? `?${qs}` : ""}`, + ); + + const lines = data.logs.map((log) => { + const level = LEVEL_NAMES[log.level] || String(log.level); + const time = new Date(log.timestamp).toISOString().slice(11, 23); + return `[${time}] ${level}: ${log.message}`; + }); + + return { + content: [ + { + type: "text", + text: lines.length > 0 ? lines.join("\n") : "No console logs", + }, + ], + }; + }, + ); +} diff --git a/packages/desktop-mcp/src/mcp/tools/get-console-logs/index.ts b/packages/desktop-mcp/src/mcp/tools/get-console-logs/index.ts new file mode 100644 index 00000000000..f671f7f0eab --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/get-console-logs/index.ts @@ -0,0 +1 @@ +export { register } from "./get-console-logs.js"; diff --git a/packages/desktop-mcp/src/mcp/tools/get-window-info/get-window-info.ts b/packages/desktop-mcp/src/mcp/tools/get-window-info/get-window-info.ts new file mode 100644 index 00000000000..6369ea7b802 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/get-window-info/get-window-info.ts @@ -0,0 +1,31 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { WindowInfoResponse } from "../../../zod.js"; +import { automationFetch } from "../../client/index.js"; + +export function register(server: McpServer) { + server.registerTool( + "get_window_info", + { + description: + "Get information about the Electron app window: bounds, title, URL, focus state, and more.", + inputSchema: {}, + }, + async () => { + const data = await automationFetch("/window-info"); + + const lines = [ + `Title: ${data.title}`, + `URL: ${data.url}`, + `Bounds: ${data.bounds.x},${data.bounds.y} ${data.bounds.width}x${data.bounds.height}`, + `Focused: ${data.focused}`, + `Maximized: ${data.maximized}`, + `Fullscreen: ${data.fullscreen}`, + `Visible: ${data.visible}`, + ]; + + return { + content: [{ type: "text", text: lines.join("\n") }], + }; + }, + ); +} diff --git a/packages/desktop-mcp/src/mcp/tools/get-window-info/index.ts b/packages/desktop-mcp/src/mcp/tools/get-window-info/index.ts new file mode 100644 index 00000000000..290623d6069 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/get-window-info/index.ts @@ -0,0 +1 @@ +export { register } from "./get-window-info.js"; diff --git a/packages/desktop-mcp/src/mcp/tools/index.ts b/packages/desktop-mcp/src/mcp/tools/index.ts new file mode 100644 index 00000000000..c13e7c27340 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/index.ts @@ -0,0 +1,28 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { register as click } from "./click/index.js"; +import { register as evaluateJs } from "./evaluate-js/index.js"; +import { register as getConsoleLogs } from "./get-console-logs/index.js"; +import { register as getWindowInfo } from "./get-window-info/index.js"; +import { register as inspectDom } from "./inspect-dom/index.js"; +import { register as navigate } from "./navigate/index.js"; +import { register as sendKeys } from "./send-keys/index.js"; +import { register as takeScreenshot } from "./take-screenshot/index.js"; +import { register as typeText } from "./type-text/index.js"; + +const allTools = [ + takeScreenshot, + inspectDom, + click, + typeText, + sendKeys, + getConsoleLogs, + evaluateJs, + navigate, + getWindowInfo, +]; + +export function registerTools(server: McpServer) { + for (const register of allTools) { + register(server); + } +} diff --git a/packages/desktop-mcp/src/mcp/tools/inspect-dom/index.ts b/packages/desktop-mcp/src/mcp/tools/inspect-dom/index.ts new file mode 100644 index 00000000000..8a019fc4bc4 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/inspect-dom/index.ts @@ -0,0 +1 @@ +export { register } from "./inspect-dom.js"; diff --git a/packages/desktop-mcp/src/mcp/tools/inspect-dom/inspect-dom.ts b/packages/desktop-mcp/src/mcp/tools/inspect-dom/inspect-dom.ts new file mode 100644 index 00000000000..b0fb034d605 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/inspect-dom/inspect-dom.ts @@ -0,0 +1,59 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import type { DomResponse } from "../../../zod.js"; +import { automationFetch } from "../../client/index.js"; + +export function register(server: McpServer) { + server.registerTool( + "inspect_dom", + { + description: + "Inspect the DOM of the Electron app. Returns a structured list of visible elements with selectors, text content, bounds, and interactivity info. Use this to understand what's on screen before clicking or typing. If you don't have an up-to-date view of the UI, call this first instead of guessing.", + inputSchema: { + selector: z + .string() + .optional() + .describe("CSS selector to scope inspection to a subtree"), + interactiveOnly: z + .boolean() + .default(false) + .describe( + "If true, only return interactive elements (buttons, inputs, links, etc.)", + ), + }, + }, + async (args) => { + const params = new URLSearchParams(); + if (args.selector) params.set("selector", args.selector as string); + if (args.interactiveOnly) params.set("interactiveOnly", "true"); + const qs = params.toString(); + + const data = await automationFetch( + `/dom${qs ? `?${qs}` : ""}`, + ); + + const lines = data.elements.map((el) => { + const attrs = [ + el.interactive ? "interactive" : "", + el.disabled ? "disabled" : "", + el.focused ? "focused" : "", + el.role ? `role=${el.role}` : "", + el.testId ? `testid=${el.testId}` : "", + ] + .filter(Boolean) + .join(", "); + + return `[${el.tag}] ${el.selector}${el.text ? ` — "${el.text.slice(0, 80)}"` : ""}${attrs ? ` (${attrs})` : ""} @ ${el.bounds.x},${el.bounds.y} ${el.bounds.width}x${el.bounds.height}`; + }); + + return { + content: [ + { + type: "text", + text: lines.length > 0 ? lines.join("\n") : "No elements found", + }, + ], + }; + }, + ); +} diff --git a/packages/desktop-mcp/src/mcp/tools/navigate/index.ts b/packages/desktop-mcp/src/mcp/tools/navigate/index.ts new file mode 100644 index 00000000000..abeebae4134 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/navigate/index.ts @@ -0,0 +1 @@ +export { register } from "./navigate.js"; diff --git a/packages/desktop-mcp/src/mcp/tools/navigate/navigate.ts b/packages/desktop-mcp/src/mcp/tools/navigate/navigate.ts new file mode 100644 index 00000000000..d80a7374fb8 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/navigate/navigate.ts @@ -0,0 +1,37 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import type { NavigateResponse } from "../../../zod.js"; +import { automationFetch } from "../../client/index.js"; + +export function register(server: McpServer) { + server.registerTool( + "navigate", + { + description: + "Navigate the Electron app to a URL or route path. Use 'path' for in-app navigation (hash routing), or 'url' for full URL navigation.", + inputSchema: { + url: z.string().optional().describe("Full URL to navigate to"), + path: z + .string() + .optional() + .describe("Route path for in-app navigation (e.g. '/settings')"), + }, + }, + async (args) => { + const data = await automationFetch("/navigate", { + method: "POST", + body: JSON.stringify(args), + }); + return { + content: [ + { + type: "text", + text: data.success + ? `Navigated to ${data.url}` + : "Navigation failed", + }, + ], + }; + }, + ); +} diff --git a/packages/desktop-mcp/src/mcp/tools/send-keys/index.ts b/packages/desktop-mcp/src/mcp/tools/send-keys/index.ts new file mode 100644 index 00000000000..41562ca2903 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/send-keys/index.ts @@ -0,0 +1 @@ +export { register } from "./send-keys.js"; diff --git a/packages/desktop-mcp/src/mcp/tools/send-keys/send-keys.ts b/packages/desktop-mcp/src/mcp/tools/send-keys/send-keys.ts new file mode 100644 index 00000000000..00f2be5f890 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/send-keys/send-keys.ts @@ -0,0 +1,34 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import type { SendKeysResponse } from "../../../zod.js"; +import { automationFetch } from "../../client/index.js"; + +export function register(server: McpServer) { + server.registerTool( + "send_keys", + { + description: + 'Send keyboard shortcuts or key presses to the Electron app. Provide an array of keys to press simultaneously. Use modifier names like "Meta" (Cmd), "Control", "Alt", "Shift" combined with a key. Examples: ["Meta", "t"] for Cmd+T, ["Meta", "Shift", "p"] for Cmd+Shift+P, ["Escape"] for Esc, ["Enter"] for Enter.', + inputSchema: { + keys: z + .array(z.string()) + .describe( + 'Keys to press simultaneously, e.g. ["Meta", "t"] for Cmd+T', + ), + }, + }, + async (args) => { + const data = await automationFetch("/send-keys", { + method: "POST", + body: JSON.stringify(args), + }); + + const desc = data.success + ? `Sent keys: ${(args.keys as string[]).join("+")}` + : "Failed to send keys"; + return { + content: [{ type: "text", text: desc }], + }; + }, + ); +} diff --git a/packages/desktop-mcp/src/mcp/tools/take-screenshot/index.ts b/packages/desktop-mcp/src/mcp/tools/take-screenshot/index.ts new file mode 100644 index 00000000000..9b37e169262 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/take-screenshot/index.ts @@ -0,0 +1 @@ +export { register } from "./take-screenshot.js"; diff --git a/packages/desktop-mcp/src/mcp/tools/take-screenshot/take-screenshot.ts b/packages/desktop-mcp/src/mcp/tools/take-screenshot/take-screenshot.ts new file mode 100644 index 00000000000..e4394e52ab3 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/take-screenshot/take-screenshot.ts @@ -0,0 +1,44 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import type { ScreenshotResponse } from "../../../zod.js"; +import { automationFetch } from "../../client/index.js"; + +export function register(server: McpServer) { + server.registerTool( + "take_screenshot", + { + description: + "Take a screenshot of the Electron app window. Returns the screenshot as a base64-encoded PNG image. Use this to see what's currently displayed in the app. Always call this or inspect_dom before interacting with the UI.", + inputSchema: { + rect: z + .object({ + x: z.number().describe("X coordinate of capture region"), + y: z.number().describe("Y coordinate of capture region"), + width: z.number().describe("Width of capture region"), + height: z.number().describe("Height of capture region"), + }) + .optional() + .describe( + "Optional region to capture. Omit to capture the full window.", + ), + }, + }, + async (args) => { + const params = args.rect + ? `?rect=${args.rect.x},${args.rect.y},${args.rect.width},${args.rect.height}` + : ""; + const data = await automationFetch( + `/screenshot${params}`, + ); + return { + content: [ + { + type: "image", + data: data.image, + mimeType: "image/png", + }, + ], + }; + }, + ); +} diff --git a/packages/desktop-mcp/src/mcp/tools/type-text/index.ts b/packages/desktop-mcp/src/mcp/tools/type-text/index.ts new file mode 100644 index 00000000000..ac38b9d2412 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/type-text/index.ts @@ -0,0 +1 @@ +export { register } from "./type-text.js"; diff --git a/packages/desktop-mcp/src/mcp/tools/type-text/type-text.ts b/packages/desktop-mcp/src/mcp/tools/type-text/type-text.ts new file mode 100644 index 00000000000..2d46af2abb3 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/type-text/type-text.ts @@ -0,0 +1,39 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import type { TypeResponse } from "../../../zod.js"; +import { automationFetch } from "../../client/index.js"; + +export function register(server: McpServer) { + server.registerTool( + "type_text", + { + description: + "Type text into a focused or selected element in the Electron app. Optionally provide a CSS selector to focus an element first. Use clearFirst to clear existing content before typing.", + inputSchema: { + text: z.string().describe("Text to type"), + selector: z + .string() + .optional() + .describe("CSS selector of element to focus before typing"), + clearFirst: z + .boolean() + .default(false) + .describe("Clear existing content before typing"), + }, + }, + async (args) => { + const data = await automationFetch("/type", { + method: "POST", + body: JSON.stringify(args), + }); + return { + content: [ + { + type: "text", + text: data.success ? `Typed "${args.text}"` : "Failed to type", + }, + ], + }; + }, + ); +} diff --git a/packages/desktop-mcp/src/server/console-capture/console-capture.ts b/packages/desktop-mcp/src/server/console-capture/console-capture.ts new file mode 100644 index 00000000000..d0acffd3cf8 --- /dev/null +++ b/packages/desktop-mcp/src/server/console-capture/console-capture.ts @@ -0,0 +1,44 @@ +import type { WebContents } from "electron"; +import type { ConsoleLogEntry } from "../../zod.js"; + +export class ConsoleCapture { + private logs: ConsoleLogEntry[] = []; + private maxSize = 500; + + attach(webContents: WebContents) { + webContents.on( + "console-message", + (_event, level, message, line, sourceId) => { + this.logs.push({ + level, + message, + source: sourceId, + line, + timestamp: Date.now(), + }); + if (this.logs.length > this.maxSize) this.logs.shift(); + }, + ); + } + + getLogs({ + level, + limit, + }: { + level?: number; + limit?: number; + }): ConsoleLogEntry[] { + let filtered = this.logs; + if (level !== undefined) { + filtered = filtered.filter((log) => log.level === level); + } + if (limit !== undefined) { + filtered = filtered.slice(-limit); + } + return filtered; + } + + clear() { + this.logs = []; + } +} diff --git a/packages/desktop-mcp/src/server/console-capture/index.ts b/packages/desktop-mcp/src/server/console-capture/index.ts new file mode 100644 index 00000000000..2004f3c0cc6 --- /dev/null +++ b/packages/desktop-mcp/src/server/console-capture/index.ts @@ -0,0 +1 @@ +export { ConsoleCapture } from "./console-capture.js"; diff --git a/packages/desktop-mcp/src/server/dom-inspector/dom-inspector.ts b/packages/desktop-mcp/src/server/dom-inspector/dom-inspector.ts new file mode 100644 index 00000000000..081908a3b35 --- /dev/null +++ b/packages/desktop-mcp/src/server/dom-inspector/dom-inspector.ts @@ -0,0 +1,92 @@ +/** + * JavaScript source to inject into the renderer via executeJavaScript(). + * Walks the DOM and returns a flat list of visible elements with metadata. + */ +export const DOM_INSPECTOR_SCRIPT = `function inspectDom({ selector, interactiveOnly }) { + const INTERACTIVE_TAGS = new Set([ + 'A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'DETAILS', 'SUMMARY' + ]); + const INTERACTIVE_ROLES = new Set([ + 'button', 'link', 'checkbox', 'radio', 'tab', 'menuitem', + 'switch', 'textbox', 'combobox', 'listbox', 'option', 'slider', 'spinbutton' + ]); + + const root = selector ? document.querySelector(selector) : document.body; + if (!root) return []; + + const elements = []; + const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + + let node = walker.currentNode; + while (node) { + if (node instanceof HTMLElement) { + const rect = node.getBoundingClientRect(); + const style = window.getComputedStyle(node); + + // Skip invisible elements + if (rect.width === 0 && rect.height === 0) { node = walker.nextNode(); continue; } + if (style.display === 'none' || style.visibility === 'hidden') { node = walker.nextNode(); continue; } + if (parseFloat(style.opacity) === 0) { node = walker.nextNode(); continue; } + + const tag = node.tagName.toLowerCase(); + const role = node.getAttribute('role') || undefined; + const testId = node.getAttribute('data-testid') || undefined; + const isInteractive = INTERACTIVE_TAGS.has(node.tagName) + || INTERACTIVE_ROLES.has(role || '') + || node.hasAttribute('onclick') + || node.getAttribute('tabindex') !== null; + + if (interactiveOnly && !isInteractive) { node = walker.nextNode(); continue; } + + // Build a unique CSS selector + let cssSelector; + if (node.id) { + cssSelector = '#' + CSS.escape(node.id); + } else if (testId) { + cssSelector = '[data-testid="' + testId + '"]'; + } else { + const path = []; + let el = node; + while (el && el !== document.body) { + const parent = el.parentElement; + if (!parent) break; + const siblings = Array.from(parent.children).filter(s => s.tagName === el.tagName); + if (siblings.length > 1) { + const idx = siblings.indexOf(el) + 1; + path.unshift(el.tagName.toLowerCase() + ':nth-of-type(' + idx + ')'); + } else { + path.unshift(el.tagName.toLowerCase()); + } + el = parent; + } + cssSelector = path.join(' > '); + } + + const text = (node.textContent || '').trim().slice(0, 200); + + elements.push({ + tag, + id: node.id || undefined, + classes: Array.from(node.classList), + text, + selector: cssSelector, + bounds: { + x: Math.round(rect.x), + y: Math.round(rect.y), + width: Math.round(rect.width), + height: Math.round(rect.height), + }, + role, + testId, + interactive: isInteractive, + disabled: node.hasAttribute('disabled'), + checked: 'checked' in node ? node.checked : undefined, + focused: document.activeElement === node, + visible: true, + }); + } + node = walker.nextNode(); + } + + return elements; +}`; diff --git a/packages/desktop-mcp/src/server/dom-inspector/index.ts b/packages/desktop-mcp/src/server/dom-inspector/index.ts new file mode 100644 index 00000000000..12729eeb065 --- /dev/null +++ b/packages/desktop-mcp/src/server/dom-inspector/index.ts @@ -0,0 +1 @@ +export { DOM_INSPECTOR_SCRIPT } from "./dom-inspector.js"; diff --git a/packages/desktop-mcp/src/server/handlers/click/click.ts b/packages/desktop-mcp/src/server/handlers/click/click.ts new file mode 100644 index 00000000000..4b6209ad0f6 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/click/click.ts @@ -0,0 +1,90 @@ +import type { BrowserWindow } from "electron"; +import type { RequestHandler } from "express"; +import { ClickRequestSchema } from "../../../zod.js"; + +export function clickHandler( + getWindow: () => BrowserWindow | null, +): RequestHandler { + return async (req, res) => { + const win = getWindow(); + if (!win) { + res.status(503).json({ error: "No window available" }); + return; + } + + const parsed = ClickRequestSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const { selector, text, testId, x, y, index, fuzzy } = parsed.data; + + // Click by coordinates + if (x !== undefined && y !== undefined) { + win.webContents.sendInputEvent({ + type: "mouseDown", + x, + y, + button: "left", + clickCount: 1, + }); + win.webContents.sendInputEvent({ + type: "mouseUp", + x, + y, + button: "left", + clickCount: 1, + }); + res.json({ success: true }); + return; + } + + // Build JS to find and click element + let findScript: string; + if (selector) { + findScript = `document.querySelectorAll(${JSON.stringify(selector)})[${index}]`; + } else if (testId) { + findScript = `document.querySelectorAll('[data-testid="${testId}"]')[${index}]`; + } else if (text) { + const matchExpr = fuzzy + ? `content.toLowerCase().includes(${JSON.stringify(text.toLowerCase())})` + : `content === ${JSON.stringify(text)}`; + findScript = `(() => { + const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); + const matches = []; + let node; + while (node = walker.nextNode()) { + const content = node.textContent.trim(); + if (${matchExpr}) { + matches.push(node.parentElement); + } + } + return matches[${index}]; + })()`; + } else { + res.status(400).json({ + error: "Must provide selector, text, testId, or x/y coordinates", + }); + return; + } + + const result = await win.webContents.executeJavaScript(`(() => { + const el = ${findScript}; + if (!el) return null; + el.click(); + return { + tag: el.tagName.toLowerCase(), + text: (el.textContent || '').trim().slice(0, 100), + selector: el.id ? '#' + el.id : el.tagName.toLowerCase(), + }; + })()`); + + if (!result) { + res.status(404).json({ error: "Element not found" }); + return; + } + + res.json({ success: true, element: result }); + }; +} diff --git a/packages/desktop-mcp/src/server/handlers/click/index.ts b/packages/desktop-mcp/src/server/handlers/click/index.ts new file mode 100644 index 00000000000..14e7defe9aa --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/click/index.ts @@ -0,0 +1 @@ +export { clickHandler } from "./click.js"; diff --git a/packages/desktop-mcp/src/server/handlers/console-logs/console-logs.ts b/packages/desktop-mcp/src/server/handlers/console-logs/console-logs.ts new file mode 100644 index 00000000000..de9fd585484 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/console-logs/console-logs.ts @@ -0,0 +1,36 @@ +import type { RequestHandler } from "express"; +import { ConsoleLogsRequestSchema } from "../../../zod.js"; +import type { ConsoleCapture } from "../../console-capture/index.js"; + +const LEVEL_MAP: Record = { + debug: 0, + log: 1, + info: 1, + warn: 2, + error: 3, +}; + +export function consoleLogsHandler( + consoleCapture: ConsoleCapture, +): RequestHandler { + return (req, res) => { + const parsed = ConsoleLogsRequestSchema.safeParse({ + level: req.query.level, + limit: req.query.limit ? Number(req.query.limit) : undefined, + clear: req.query.clear === "true", + }); + + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const { level, limit, clear } = parsed.data; + const levelNum = level ? LEVEL_MAP[level] : undefined; + const logs = consoleCapture.getLogs({ level: levelNum, limit }); + + if (clear) consoleCapture.clear(); + + res.json({ logs }); + }; +} diff --git a/packages/desktop-mcp/src/server/handlers/console-logs/index.ts b/packages/desktop-mcp/src/server/handlers/console-logs/index.ts new file mode 100644 index 00000000000..b37bc8b933b --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/console-logs/index.ts @@ -0,0 +1 @@ +export { consoleLogsHandler } from "./console-logs.js"; diff --git a/packages/desktop-mcp/src/server/handlers/dom/dom.ts b/packages/desktop-mcp/src/server/handlers/dom/dom.ts new file mode 100644 index 00000000000..34b02c9ac70 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/dom/dom.ts @@ -0,0 +1,26 @@ +import type { BrowserWindow } from "electron"; +import type { RequestHandler } from "express"; +import { DOM_INSPECTOR_SCRIPT } from "../../dom-inspector/index.js"; + +export function domHandler( + getWindow: () => BrowserWindow | null, +): RequestHandler { + return async (req, res) => { + const win = getWindow(); + if (!win) { + res.status(503).json({ error: "No window available" }); + return; + } + + const selector = req.query.selector + ? String(req.query.selector) + : undefined; + const interactiveOnly = req.query.interactiveOnly === "true"; + + const elements = await win.webContents.executeJavaScript( + `(${DOM_INSPECTOR_SCRIPT})(${JSON.stringify({ selector, interactiveOnly })})`, + ); + + res.json({ elements }); + }; +} diff --git a/packages/desktop-mcp/src/server/handlers/dom/index.ts b/packages/desktop-mcp/src/server/handlers/dom/index.ts new file mode 100644 index 00000000000..dcd48aacb98 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/dom/index.ts @@ -0,0 +1 @@ +export { domHandler } from "./dom.js"; diff --git a/packages/desktop-mcp/src/server/handlers/evaluate/evaluate.ts b/packages/desktop-mcp/src/server/handlers/evaluate/evaluate.ts new file mode 100644 index 00000000000..2ffea54ab58 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/evaluate/evaluate.ts @@ -0,0 +1,28 @@ +import type { BrowserWindow } from "electron"; +import type { RequestHandler } from "express"; +import { EvaluateRequestSchema } from "../../../zod.js"; + +export function evaluateHandler( + getWindow: () => BrowserWindow | null, +): RequestHandler { + return async (req, res) => { + const win = getWindow(); + if (!win) { + res.status(503).json({ error: "No window available" }); + return; + } + + const parsed = EvaluateRequestSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + try { + const result = await win.webContents.executeJavaScript(parsed.data.code); + res.json({ result }); + } catch (error) { + res.status(500).json({ error: String(error) }); + } + }; +} diff --git a/packages/desktop-mcp/src/server/handlers/evaluate/index.ts b/packages/desktop-mcp/src/server/handlers/evaluate/index.ts new file mode 100644 index 00000000000..0fdda574ba9 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/evaluate/index.ts @@ -0,0 +1 @@ +export { evaluateHandler } from "./evaluate.js"; diff --git a/packages/desktop-mcp/src/server/handlers/index.ts b/packages/desktop-mcp/src/server/handlers/index.ts new file mode 100644 index 00000000000..eee8fba2651 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/index.ts @@ -0,0 +1,9 @@ +export { clickHandler } from "./click/index.js"; +export { consoleLogsHandler } from "./console-logs/index.js"; +export { domHandler } from "./dom/index.js"; +export { evaluateHandler } from "./evaluate/index.js"; +export { navigateHandler } from "./navigate/index.js"; +export { screenshotHandler } from "./screenshot/index.js"; +export { sendKeysHandler } from "./send-keys/index.js"; +export { typeHandler } from "./type/index.js"; +export { windowInfoHandler } from "./window-info/index.js"; diff --git a/packages/desktop-mcp/src/server/handlers/navigate/index.ts b/packages/desktop-mcp/src/server/handlers/navigate/index.ts new file mode 100644 index 00000000000..a6cd6d09312 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/navigate/index.ts @@ -0,0 +1 @@ +export { navigateHandler } from "./navigate.js"; diff --git a/packages/desktop-mcp/src/server/handlers/navigate/navigate.ts b/packages/desktop-mcp/src/server/handlers/navigate/navigate.ts new file mode 100644 index 00000000000..b475b8e8cc4 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/navigate/navigate.ts @@ -0,0 +1,36 @@ +import type { BrowserWindow } from "electron"; +import type { RequestHandler } from "express"; +import { NavigateRequestSchema } from "../../../zod.js"; + +export function navigateHandler( + getWindow: () => BrowserWindow | null, +): RequestHandler { + return async (req, res) => { + const win = getWindow(); + if (!win) { + res.status(503).json({ error: "No window available" }); + return; + } + + const parsed = NavigateRequestSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const { url, path } = parsed.data; + + if (url) { + await win.webContents.loadURL(url); + } else if (path) { + await win.webContents.executeJavaScript( + `window.location.hash = ${JSON.stringify(`#${path}`)}`, + ); + } else { + res.status(400).json({ error: "Must provide url or path" }); + return; + } + + res.json({ success: true, url: win.webContents.getURL() }); + }; +} diff --git a/packages/desktop-mcp/src/server/handlers/screenshot/index.ts b/packages/desktop-mcp/src/server/handlers/screenshot/index.ts new file mode 100644 index 00000000000..c3c82266573 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/screenshot/index.ts @@ -0,0 +1 @@ +export { screenshotHandler } from "./screenshot.js"; diff --git a/packages/desktop-mcp/src/server/handlers/screenshot/screenshot.ts b/packages/desktop-mcp/src/server/handlers/screenshot/screenshot.ts new file mode 100644 index 00000000000..0053057b58b --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/screenshot/screenshot.ts @@ -0,0 +1,37 @@ +import type { BrowserWindow } from "electron"; +import type { RequestHandler } from "express"; + +export function screenshotHandler( + getWindow: () => BrowserWindow | null, +): RequestHandler { + return async (req, res) => { + const win = getWindow(); + if (!win) { + res.status(503).json({ error: "No window available" }); + return; + } + + let rect: + | { x: number; y: number; width: number; height: number } + | undefined; + if (req.query.rect) { + const parts = String(req.query.rect).split(",").map(Number); + if (parts.length === 4 && parts.every((n) => !Number.isNaN(n))) { + rect = { + x: parts[0] as number, + y: parts[1] as number, + width: parts[2] as number, + height: parts[3] as number, + }; + } + } + + const image = rect + ? await win.webContents.capturePage(rect) + : await win.webContents.capturePage(); + const size = image.getSize(); + const base64 = image.toPNG().toString("base64"); + + res.json({ image: base64, width: size.width, height: size.height }); + }; +} diff --git a/packages/desktop-mcp/src/server/handlers/send-keys/index.ts b/packages/desktop-mcp/src/server/handlers/send-keys/index.ts new file mode 100644 index 00000000000..218b93e336c --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/send-keys/index.ts @@ -0,0 +1 @@ +export { sendKeysHandler } from "./send-keys.js"; diff --git a/packages/desktop-mcp/src/server/handlers/send-keys/send-keys.ts b/packages/desktop-mcp/src/server/handlers/send-keys/send-keys.ts new file mode 100644 index 00000000000..fe7359d2486 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/send-keys/send-keys.ts @@ -0,0 +1,95 @@ +import type { BrowserWindow } from "electron"; +import type { RequestHandler } from "express"; +import { SendKeysRequestSchema } from "../../../zod.js"; + +/** + * Map from human-readable key names to Electron accelerator key codes. + * @see https://www.electronjs.org/docs/latest/api/accelerator + */ +const KEY_MAP: Record = { + meta: "Meta", + cmd: "Meta", + command: "Meta", + ctrl: "Control", + control: "Control", + alt: "Alt", + option: "Alt", + shift: "Shift", + enter: "Return", + return: "Return", + escape: "Escape", + esc: "Escape", + tab: "Tab", + backspace: "Backspace", + delete: "Delete", + space: " ", + arrowup: "Up", + arrowdown: "Down", + arrowleft: "Left", + arrowright: "Right", + up: "Up", + down: "Down", + left: "Left", + right: "Right", +}; + +const MODIFIER_KEYS = new Set(["Meta", "Control", "Alt", "Shift"]); + +function normalizeKey(key: string): string { + return KEY_MAP[key.toLowerCase()] ?? key; +} + +export function sendKeysHandler( + getWindow: () => BrowserWindow | null, +): RequestHandler { + return async (req, res) => { + const win = getWindow(); + if (!win) { + res.status(503).json({ error: "No window available" }); + return; + } + + const parsed = SendKeysRequestSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + // Focus the window so key events are received + if (!win.isFocused()) win.focus(); + + type Modifier = "shift" | "control" | "alt" | "meta"; + + const normalized = parsed.data.keys.map(normalizeKey); + const modifiers: Modifier[] = []; + let keyCode = ""; + + for (const key of normalized) { + if (MODIFIER_KEYS.has(key)) { + modifiers.push(key.toLowerCase() as Modifier); + } else { + keyCode = key; + } + } + + // If no non-modifier key, treat last key as the keyCode + if (!keyCode && normalized.length > 0) { + keyCode = normalized[normalized.length - 1] as string; + modifiers.pop(); + } + + win.webContents.sendInputEvent({ + type: "keyDown", + keyCode, + modifiers, + }); + + win.webContents.sendInputEvent({ + type: "keyUp", + keyCode, + modifiers, + }); + + res.json({ success: true }); + }; +} diff --git a/packages/desktop-mcp/src/server/handlers/type/index.ts b/packages/desktop-mcp/src/server/handlers/type/index.ts new file mode 100644 index 00000000000..54b1b8aeeb5 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/type/index.ts @@ -0,0 +1 @@ +export { typeHandler } from "./type.js"; diff --git a/packages/desktop-mcp/src/server/handlers/type/type.ts b/packages/desktop-mcp/src/server/handlers/type/type.ts new file mode 100644 index 00000000000..b539216b9f5 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/type/type.ts @@ -0,0 +1,60 @@ +import type { BrowserWindow } from "electron"; +import type { RequestHandler } from "express"; +import { TypeRequestSchema } from "../../../zod.js"; + +export function typeHandler( + getWindow: () => BrowserWindow | null, +): RequestHandler { + return async (req, res) => { + const win = getWindow(); + if (!win) { + res.status(503).json({ error: "No window available" }); + return; + } + + const parsed = TypeRequestSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const { text, selector, clearFirst } = parsed.data; + + const elExpr = selector + ? `document.querySelector(${JSON.stringify(selector)})` + : "document.activeElement"; + + const result = await win.webContents.executeJavaScript(`(() => { + const el = ${elExpr}; + if (!el) return { success: false, error: 'Element not found' }; + el.focus(); + ${ + clearFirst + ? ` + el.value = ''; + el.dispatchEvent(new Event('input', { bubbles: true })); + ` + : "" + } + if (el.isContentEditable) { + document.execCommand('insertText', false, ${JSON.stringify(text)}); + } else { + const setter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, 'value' + )?.set || Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, 'value' + )?.set; + if (setter) { + setter.call(el, ${clearFirst ? "" : "el.value + "}${JSON.stringify(text)}); + } else { + el.value ${clearFirst ? "=" : "+="} ${JSON.stringify(text)}; + } + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); + } + return { success: true }; + })()`); + + res.json(result); + }; +} diff --git a/packages/desktop-mcp/src/server/handlers/window-info/index.ts b/packages/desktop-mcp/src/server/handlers/window-info/index.ts new file mode 100644 index 00000000000..184d0cc8a08 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/window-info/index.ts @@ -0,0 +1 @@ +export { windowInfoHandler } from "./window-info.js"; diff --git a/packages/desktop-mcp/src/server/handlers/window-info/window-info.ts b/packages/desktop-mcp/src/server/handlers/window-info/window-info.ts new file mode 100644 index 00000000000..59c611a7013 --- /dev/null +++ b/packages/desktop-mcp/src/server/handlers/window-info/window-info.ts @@ -0,0 +1,24 @@ +import type { BrowserWindow } from "electron"; +import type { RequestHandler } from "express"; + +export function windowInfoHandler( + getWindow: () => BrowserWindow | null, +): RequestHandler { + return (_req, res) => { + const win = getWindow(); + if (!win) { + res.status(503).json({ error: "No window available" }); + return; + } + + res.json({ + bounds: win.getBounds(), + title: win.getTitle(), + url: win.webContents.getURL(), + focused: win.isFocused(), + maximized: win.isMaximized(), + fullscreen: win.isFullScreen(), + visible: win.isVisible(), + }); + }; +} diff --git a/packages/desktop-mcp/src/server/index.ts b/packages/desktop-mcp/src/server/index.ts new file mode 100644 index 00000000000..f1763219721 --- /dev/null +++ b/packages/desktop-mcp/src/server/index.ts @@ -0,0 +1 @@ +export { createAutomationServer } from "./server.js"; diff --git a/packages/desktop-mcp/src/server/server.ts b/packages/desktop-mcp/src/server/server.ts new file mode 100644 index 00000000000..21dd9138995 --- /dev/null +++ b/packages/desktop-mcp/src/server/server.ts @@ -0,0 +1,51 @@ +import type { BrowserWindow } from "electron"; +import express from "express"; +import { ConsoleCapture } from "./console-capture/index.js"; +import { clickHandler } from "./handlers/click/index.js"; +import { consoleLogsHandler } from "./handlers/console-logs/index.js"; +import { domHandler } from "./handlers/dom/index.js"; +import { evaluateHandler } from "./handlers/evaluate/index.js"; +import { navigateHandler } from "./handlers/navigate/index.js"; +import { screenshotHandler } from "./handlers/screenshot/index.js"; +import { sendKeysHandler } from "./handlers/send-keys/index.js"; +import { typeHandler } from "./handlers/type/index.js"; +import { windowInfoHandler } from "./handlers/window-info/index.js"; + +export function createAutomationServer({ + getWindow, + port = 9223, +}: { + getWindow: () => BrowserWindow | null; + port?: number; +}) { + const app = express(); + app.use(express.json()); + + const consoleCapture = new ConsoleCapture(); + + const attachConsole = () => { + const win = getWindow(); + if (win) consoleCapture.attach(win.webContents); + }; + attachConsole(); + + app.get("/health", (_req, res) => { + res.json({ status: "ok" }); + }); + + app.get("/screenshot", screenshotHandler(getWindow)); + app.get("/dom", domHandler(getWindow)); + app.post("/click", clickHandler(getWindow)); + app.post("/type", typeHandler(getWindow)); + app.post("/evaluate", evaluateHandler(getWindow)); + app.get("/console-logs", consoleLogsHandler(consoleCapture)); + app.get("/window-info", windowInfoHandler(getWindow)); + app.post("/navigate", navigateHandler(getWindow)); + app.post("/send-keys", sendKeysHandler(getWindow)); + + const server = app.listen(port, "127.0.0.1", () => { + console.log(`[automation] Listening on http://127.0.0.1:${port}`); + }); + + return { server, consoleCapture, attachConsole }; +} diff --git a/packages/desktop-mcp/src/zod.ts b/packages/desktop-mcp/src/zod.ts new file mode 100644 index 00000000000..c781946f1c6 --- /dev/null +++ b/packages/desktop-mcp/src/zod.ts @@ -0,0 +1,167 @@ +import { z } from "zod"; + +// === Request Schemas === + +export const ScreenshotRequestSchema = z.object({ + rect: z + .object({ + x: z.number(), + y: z.number(), + width: z.number(), + height: z.number(), + }) + .optional(), +}); + +export const DomRequestSchema = z.object({ + selector: z.string().optional(), + interactiveOnly: z.boolean().optional(), +}); + +export const ClickRequestSchema = z.object({ + selector: z.string().optional(), + text: z.string().optional(), + testId: z.string().optional(), + x: z.number().optional(), + y: z.number().optional(), + index: z.number().int().min(0).default(0), + fuzzy: z.boolean().default(true), +}); + +export const TypeRequestSchema = z.object({ + text: z.string(), + selector: z.string().optional(), + clearFirst: z.boolean().default(false), +}); + +export const EvaluateRequestSchema = z.object({ + code: z.string(), +}); + +export const ConsoleLogsRequestSchema = z.object({ + level: z.enum(["log", "warn", "error", "info", "debug"]).optional(), + limit: z.number().int().min(1).optional(), + clear: z.boolean().optional(), +}); + +export const NavigateRequestSchema = z.object({ + url: z.string().optional(), + path: z.string().optional(), +}); + +export const SendKeysRequestSchema = z.object({ + keys: z + .array(z.string()) + .describe("Keys to send, e.g. ['Meta', 't'] for Cmd+T"), +}); + +// === Response Schemas === + +export const ScreenshotResponseSchema = z.object({ + image: z.string(), + width: z.number(), + height: z.number(), +}); + +export const DomElementSchema = z.object({ + tag: z.string(), + id: z.string().optional(), + classes: z.array(z.string()), + text: z.string(), + selector: z.string(), + bounds: z.object({ + x: z.number(), + y: z.number(), + width: z.number(), + height: z.number(), + }), + role: z.string().optional(), + testId: z.string().optional(), + interactive: z.boolean(), + disabled: z.boolean(), + checked: z.boolean().optional(), + focused: z.boolean(), + visible: z.boolean(), +}); + +export const DomResponseSchema = z.object({ + elements: z.array(DomElementSchema), +}); + +export const ClickResponseSchema = z.object({ + success: z.boolean(), + element: z + .object({ + tag: z.string(), + text: z.string(), + selector: z.string(), + }) + .optional(), +}); + +export const TypeResponseSchema = z.object({ + success: z.boolean(), +}); + +export const EvaluateResponseSchema = z.object({ + result: z.unknown(), +}); + +export const ConsoleLogEntrySchema = z.object({ + level: z.number(), + message: z.string(), + source: z.string(), + line: z.number(), + timestamp: z.number(), +}); + +export const ConsoleLogsResponseSchema = z.object({ + logs: z.array(ConsoleLogEntrySchema), +}); + +export const WindowInfoResponseSchema = z.object({ + bounds: z.object({ + x: z.number(), + y: z.number(), + width: z.number(), + height: z.number(), + }), + title: z.string(), + url: z.string(), + focused: z.boolean(), + maximized: z.boolean(), + fullscreen: z.boolean(), + visible: z.boolean(), +}); + +export const NavigateResponseSchema = z.object({ + success: z.boolean(), + url: z.string(), +}); + +export const SendKeysResponseSchema = z.object({ + success: z.boolean(), +}); + +// === Inferred Types === + +export type ScreenshotRequest = z.infer; +export type DomRequest = z.infer; +export type ClickRequest = z.infer; +export type TypeRequest = z.infer; +export type EvaluateRequest = z.infer; +export type ConsoleLogsRequest = z.infer; +export type NavigateRequest = z.infer; +export type SendKeysRequest = z.infer; + +export type ScreenshotResponse = z.infer; +export type DomElement = z.infer; +export type DomResponse = z.infer; +export type ClickResponse = z.infer; +export type TypeResponse = z.infer; +export type EvaluateResponse = z.infer; +export type ConsoleLogEntry = z.infer; +export type ConsoleLogsResponse = z.infer; +export type WindowInfoResponse = z.infer; +export type NavigateResponse = z.infer; +export type SendKeysResponse = z.infer; diff --git a/packages/desktop-mcp/tsconfig.json b/packages/desktop-mcp/tsconfig.json new file mode 100644 index 00000000000..525620cf0a6 --- /dev/null +++ b/packages/desktop-mcp/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@superset/typescript/internal-package.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules"] +} From aa5d31695fc9e4ab88d81fdb6a9d155d1f7fcdf2 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Fri, 13 Feb 2026 17:43:33 -0800 Subject: [PATCH 2/2] refactor(desktop-mcp): migrate from Express HTTP to CDP via puppeteer-core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the Express HTTP server + manual JS execution architecture with direct CDP (Chrome DevTools Protocol) connection via puppeteer-core. This fixes issues with Radix UI interactions and simplifies the overall design. Architecture: Claude Code ←stdio→ MCP Server ←CDP WebSocket→ Electron Key changes: - Add --remote-debugging-port and --remote-allow-origins flags to Electron - New ConnectionManager for lazy CDP connection with auto-reconnect - New FocusLock to suppress blur events between MCP tool calls - New ConsoleCapture using CDP console events instead of preload injection - All 9 tools rewritten to use puppeteer Page API directly - Remove Express server, HTTP client, and all server/ handlers - Swap express dep for puppeteer-core (Bun + Playwright is incompatible) - Set defaultViewport: null to preserve Electron window size --- .../lib/electron-app/factories/app/setup.ts | 7 ++ apps/desktop/src/main/windows/main.ts | 8 -- bun.lock | 78 +++++++++++++- packages/desktop-mcp/package.json | 6 +- packages/desktop-mcp/src/index.ts | 2 +- packages/desktop-mcp/src/mcp/client/client.ts | 20 ---- packages/desktop-mcp/src/mcp/client/index.ts | 1 - .../src/mcp/connection/connection-manager.ts | 59 ++++++++++ .../desktop-mcp/src/mcp/connection/index.ts | 1 + .../mcp/console-capture/console-capture.ts | 53 +++++++++ .../{server => mcp}/console-capture/index.ts | 0 .../dom-inspector/dom-inspector.ts | 2 +- .../{server => mcp}/dom-inspector/index.ts | 0 .../src/mcp/focus-lock/focus-lock.ts | 92 ++++++++++++++++ .../desktop-mcp/src/mcp/focus-lock/index.ts | 1 + packages/desktop-mcp/src/mcp/mcp-server.ts | 11 +- .../desktop-mcp/src/mcp/tools/click/click.ts | 102 ++++++++++++++++-- .../src/mcp/tools/evaluate-js/evaluate-js.ts | 46 ++++---- .../get-console-logs/get-console-logs.ts | 32 +++--- .../tools/get-window-info/get-window-info.ts | 37 ++++--- packages/desktop-mcp/src/mcp/tools/index.ts | 12 ++- .../src/mcp/tools/inspect-dom/inspect-dom.ts | 35 +++--- .../src/mcp/tools/navigate/navigate.ts | 37 ++++--- .../src/mcp/tools/send-keys/send-keys.ts | 83 +++++++++++--- .../tools/take-screenshot/take-screenshot.ts | 31 +++--- .../src/mcp/tools/type-text/type-text.ts | 29 +++-- .../server/console-capture/console-capture.ts | 44 -------- .../src/server/handlers/click/click.ts | 90 ---------------- .../src/server/handlers/click/index.ts | 1 - .../handlers/console-logs/console-logs.ts | 36 ------- .../src/server/handlers/console-logs/index.ts | 1 - .../src/server/handlers/dom/dom.ts | 26 ----- .../src/server/handlers/dom/index.ts | 1 - .../src/server/handlers/evaluate/evaluate.ts | 28 ----- .../src/server/handlers/evaluate/index.ts | 1 - .../desktop-mcp/src/server/handlers/index.ts | 9 -- .../src/server/handlers/navigate/index.ts | 1 - .../src/server/handlers/navigate/navigate.ts | 36 ------- .../src/server/handlers/screenshot/index.ts | 1 - .../server/handlers/screenshot/screenshot.ts | 37 ------- .../src/server/handlers/send-keys/index.ts | 1 - .../server/handlers/send-keys/send-keys.ts | 95 ---------------- .../src/server/handlers/type/index.ts | 1 - .../src/server/handlers/type/type.ts | 60 ----------- .../src/server/handlers/window-info/index.ts | 1 - .../handlers/window-info/window-info.ts | 24 ----- packages/desktop-mcp/src/server/index.ts | 1 - packages/desktop-mcp/src/server/server.ts | 51 --------- 48 files changed, 622 insertions(+), 709 deletions(-) delete mode 100644 packages/desktop-mcp/src/mcp/client/client.ts delete mode 100644 packages/desktop-mcp/src/mcp/client/index.ts create mode 100644 packages/desktop-mcp/src/mcp/connection/connection-manager.ts create mode 100644 packages/desktop-mcp/src/mcp/connection/index.ts create mode 100644 packages/desktop-mcp/src/mcp/console-capture/console-capture.ts rename packages/desktop-mcp/src/{server => mcp}/console-capture/index.ts (100%) rename packages/desktop-mcp/src/{server => mcp}/dom-inspector/dom-inspector.ts (97%) rename packages/desktop-mcp/src/{server => mcp}/dom-inspector/index.ts (100%) create mode 100644 packages/desktop-mcp/src/mcp/focus-lock/focus-lock.ts create mode 100644 packages/desktop-mcp/src/mcp/focus-lock/index.ts delete mode 100644 packages/desktop-mcp/src/server/console-capture/console-capture.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/click/click.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/click/index.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/console-logs/console-logs.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/console-logs/index.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/dom/dom.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/dom/index.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/evaluate/evaluate.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/evaluate/index.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/index.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/navigate/index.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/navigate/navigate.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/screenshot/index.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/screenshot/screenshot.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/send-keys/index.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/send-keys/send-keys.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/type/index.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/type/type.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/window-info/index.ts delete mode 100644 packages/desktop-mcp/src/server/handlers/window-info/window-info.ts delete mode 100644 packages/desktop-mcp/src/server/index.ts delete mode 100644 packages/desktop-mcp/src/server/server.ts diff --git a/apps/desktop/src/lib/electron-app/factories/app/setup.ts b/apps/desktop/src/lib/electron-app/factories/app/setup.ts index 65e833ea801..72df0b9aead 100644 --- a/apps/desktop/src/lib/electron-app/factories/app/setup.ts +++ b/apps/desktop/src/lib/electron-app/factories/app/setup.ts @@ -82,3 +82,10 @@ PLATFORM.IS_WINDOWS && ); app.commandLine.appendSwitch("force-color-profile", "srgb"); + +// Enable CDP for desktop automation MCP (playwright-core connects via this port) +if (env.NODE_ENV === "development") { + const cdpPort = String(process.env.DESKTOP_AUTOMATION_PORT || 9223); + app.commandLine.appendSwitch("remote-debugging-port", cdpPort); + app.commandLine.appendSwitch("remote-allow-origins", "*"); +} diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts index 35fe9d9faf2..65b03ee0ff0 100644 --- a/apps/desktop/src/main/windows/main.ts +++ b/apps/desktop/src/main/windows/main.ts @@ -134,14 +134,6 @@ export async function MainWindow() { }); } - if (env.NODE_ENV === "development") { - const { createAutomationServer } = await import("@superset/desktop-mcp"); - createAutomationServer({ - getWindow, - port: env.DESKTOP_AUTOMATION_PORT, - }); - } - const server = notificationsApp.listen( env.DESKTOP_NOTIFICATIONS_PORT, "127.0.0.1", diff --git a/bun.lock b/bun.lock index 4db2a8b188a..a26e19da3c3 100644 --- a/bun.lock +++ b/bun.lock @@ -606,18 +606,14 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.25.3", - "express": "^5.1.0", + "puppeteer-core": "^24.37.3", "zod": "^4.3.5", }, "devDependencies": { "@superset/typescript": "workspace:*", - "@types/express": "^5.0.5", "@types/node": "^24.9.1", "typescript": "^5.9.3", }, - "peerDependencies": { - "electron": "*", - }, }, "packages/durable-session": { "name": "@superset/durable-session", @@ -1585,6 +1581,8 @@ "@prisma/instrumentation": ["@prisma/instrumentation@7.2.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-Rh9Z4x5kEj1OdARd7U18AtVrnL6rmLSI0qYShaB4W7Wx5BKbgzndWF+QnuzMb7GLfVdlT5aYCXoPQVYuYtVu0g=="], + "@puppeteer/browsers": ["@puppeteer/browsers@2.12.1", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-fXa6uXLxfslBlus3MEpW8S6S9fe5RwmAE5Gd8u3krqOwnkZJV3/lQJiY3LaFdTctLLqJtyMgEUGkbDnRNf6vbQ=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], @@ -2221,6 +2219,8 @@ "@tokenlens/models": ["@tokenlens/models@1.3.0", "", { "dependencies": { "@tokenlens/core": "1.3.0" } }, "sha512-9mx7ZGeewW4ndXAiD7AT1bbCk4OpJeortbjHHyNkgap+pMPPn1chY6R5zqe1ggXIUzZ2l8VOAKfPqOvpcrisJw=="], + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], + "@trpc/client": ["@trpc/client@11.8.1", "", { "peerDependencies": { "@trpc/server": "11.8.1", "typescript": ">=5.7.2" } }, "sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q=="], "@trpc/react-query": ["@trpc/react-query@11.8.1", "", { "peerDependencies": { "@tanstack/react-query": "^5.80.3", "@trpc/client": "11.8.1", "@trpc/server": "11.8.1", "react": ">=18.2.0", "react-dom": ">=18.2.0", "typescript": ">=5.7.2" } }, "sha512-0Vu55ld/oINb4U6nIPPi7eZMhxUop6K+4QUK90RVsfSD5r+957sM80M4c8bjh/JBZUxMFv9JOhxxlWcrgHxHow=="], @@ -2609,6 +2609,8 @@ "axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="], + "b4a": ["b4a@1.7.4", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug=="], + "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], @@ -2641,12 +2643,26 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], + + "bare-fs": ["bare-fs@4.5.4", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA=="], + + "bare-os": ["bare-os@3.6.2", "", {}, "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A=="], + + "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="], + + "bare-stream": ["bare-stream@2.7.0", "", { "dependencies": { "streamx": "^2.21.0" }, "peerDependencies": { "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-buffer", "bare-events"] }, "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A=="], + + "bare-url": ["bare-url@2.3.2", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA=="], + "basic-ftp": ["basic-ftp@5.1.0", "", {}, "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw=="], + "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], @@ -2751,6 +2767,8 @@ "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], + "chromium-bidi": ["chromium-bidi@14.0.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw=="], + "chromium-edge-launcher": ["chromium-edge-launcher@0.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg=="], "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], @@ -2925,6 +2943,8 @@ "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], "date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="], @@ -2963,6 +2983,8 @@ "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], @@ -2983,6 +3005,8 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "devtools-protocol": ["devtools-protocol@0.0.1566079", "", {}, "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ=="], + "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="], @@ -3113,6 +3137,8 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], + "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], @@ -3147,6 +3173,8 @@ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], @@ -3249,6 +3277,8 @@ "fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -3355,6 +3385,8 @@ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], + "getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], @@ -3969,6 +4001,8 @@ "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], @@ -4007,6 +4041,8 @@ "nested-error-stacks": ["nested-error-stacks@2.0.1", "", {}, "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A=="], + "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], + "neverthrow": ["neverthrow@7.2.0", "", {}, "sha512-iGBUfFB7yPczHHtA8dksKTJ9E8TESNTAx1UQWW6TzMF280vo9jdPYpLUXrMN1BCkPdHFdNG3fxOt2CUad8KhAw=="], "next": ["next@16.1.4", "", { "dependencies": { "@next/env": "16.1.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.4", "@next/swc-darwin-x64": "16.1.4", "@next/swc-linux-arm64-gnu": "16.1.4", "@next/swc-linux-arm64-musl": "16.1.4", "@next/swc-linux-x64-gnu": "16.1.4", "@next/swc-linux-x64-musl": "16.1.4", "@next/swc-win32-arm64-msvc": "16.1.4", "@next/swc-win32-x64-msvc": "16.1.4", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ=="], @@ -4111,6 +4147,10 @@ "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], + + "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], @@ -4275,6 +4315,8 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], @@ -4283,6 +4325,8 @@ "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + "puppeteer-core": ["puppeteer-core@24.37.3", "", { "dependencies": { "@puppeteer/browsers": "2.12.1", "chromium-bidi": "14.0.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1566079", "typed-query-selector": "^2.12.0", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.19.0" } }, "sha512-fokQ8gv+hNgsRWqVuP5rUjGp+wzV5aMTP3fcm8ekNabmLGlJdFHas1OdMscAH9Gzq4Qcf7cfI/Pe6wEcAqQhqg=="], + "pure-rand": ["pure-rand@7.0.1", "", {}, "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ=="], "qrcode-terminal": ["qrcode-terminal@0.11.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ=="], @@ -4693,6 +4737,8 @@ "streamdown": ["streamdown@2.2.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.2.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Y51o1I/sjpAy4Yn7j7R4TbUl9gcUZ7BTrHS+68IhrUBoYpNQZ28z06vww1MBFu4mSwvgF8xQIxIH2b9S9IHDyQ=="], + "streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="], + "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -4777,6 +4823,8 @@ "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], + "text-decoder": ["text-decoder@1.2.6", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg=="], + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], @@ -4887,6 +4935,8 @@ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typed-query-selector": ["typed-query-selector@2.12.0", "", {}, "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "ua-is-frozen": ["ua-is-frozen@0.1.2", "", {}, "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw=="], @@ -5023,6 +5073,8 @@ "web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="], + "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.1", "", {}, "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw=="], + "webgl-constants": ["webgl-constants@1.1.1", "", {}, "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="], "webgl-sdf-generator": ["webgl-sdf-generator@1.1.1", "", {}, "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="], @@ -5281,6 +5333,10 @@ "@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], + "@puppeteer/browsers/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@puppeteer/browsers/tar-fs": ["tar-fs@3.1.1", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg=="], + "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-aspect-ratio/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], @@ -5449,6 +5505,8 @@ "cacheable-request/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + "chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "chromium-edge-launcher/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], "clean-stack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -5477,6 +5535,8 @@ "css-tree/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "degenerator/ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], + "dir-compare/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "dmg-builder/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -5499,6 +5559,10 @@ "engine.io/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "escodegen/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], "estree-util-build-jsx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], @@ -5733,6 +5797,8 @@ "prosemirror-markdown/@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], + "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "react-dnd-multi-backend/react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], @@ -6201,6 +6267,8 @@ "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], + "@puppeteer/browsers/tar-fs/tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], + "@react-email/preview-server/next/@next/env": ["@next/env@16.0.7", "", {}, "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw=="], "@react-email/preview-server/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg=="], diff --git a/packages/desktop-mcp/package.json b/packages/desktop-mcp/package.json index 3f9925f66e8..6fd4d9b12da 100644 --- a/packages/desktop-mcp/package.json +++ b/packages/desktop-mcp/package.json @@ -17,15 +17,11 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.25.3", - "express": "^5.1.0", + "puppeteer-core": "^24.37.3", "zod": "^4.3.5" }, - "peerDependencies": { - "electron": "*" - }, "devDependencies": { "@superset/typescript": "workspace:*", - "@types/express": "^5.0.5", "@types/node": "^24.9.1", "typescript": "^5.9.3" } diff --git a/packages/desktop-mcp/src/index.ts b/packages/desktop-mcp/src/index.ts index dc97f2e1f38..6dfee9c83f1 100644 --- a/packages/desktop-mcp/src/index.ts +++ b/packages/desktop-mcp/src/index.ts @@ -1,4 +1,4 @@ -export { createAutomationServer } from "./server/index.js"; +export { createMcpServer } from "./mcp/index.js"; export type { ClickResponse, ConsoleLogEntry, diff --git a/packages/desktop-mcp/src/mcp/client/client.ts b/packages/desktop-mcp/src/mcp/client/client.ts deleted file mode 100644 index 5cc51aab777..00000000000 --- a/packages/desktop-mcp/src/mcp/client/client.ts +++ /dev/null @@ -1,20 +0,0 @@ -const BASE_URL = `http://127.0.0.1:${process.env.DESKTOP_AUTOMATION_PORT || 9223}`; - -export async function automationFetch( - path: string, - options?: RequestInit, -): Promise { - const res = await fetch(`${BASE_URL}${path}`, { - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }); - if (!res.ok) { - throw new Error( - `Automation server error: ${res.status} ${await res.text()}`, - ); - } - return res.json() as Promise; -} diff --git a/packages/desktop-mcp/src/mcp/client/index.ts b/packages/desktop-mcp/src/mcp/client/index.ts deleted file mode 100644 index a2e2ce8e52d..00000000000 --- a/packages/desktop-mcp/src/mcp/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { automationFetch } from "./client.js"; diff --git a/packages/desktop-mcp/src/mcp/connection/connection-manager.ts b/packages/desktop-mcp/src/mcp/connection/connection-manager.ts new file mode 100644 index 00000000000..2c807324ba0 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/connection/connection-manager.ts @@ -0,0 +1,59 @@ +import puppeteer, { type Browser, type Page } from "puppeteer-core"; +import { ConsoleCapture } from "../console-capture/index.js"; +import { FocusLock } from "../focus-lock/index.js"; + +const CDP_PORT = Number(process.env.DESKTOP_AUTOMATION_PORT) || 9223; + +/** + * Manages a CDP connection to the Electron renderer via puppeteer-core. + * + * - Lazy connect on first tool call (Electron might not be running yet) + * - Auto-reconnect if connection drops (Electron restart/hot reload) + * - Re-injects focus lock and console capture on reconnect + */ +export class ConnectionManager { + private browser: Browser | null = null; + private page: Page | null = null; + + readonly consoleCapture = new ConsoleCapture(); + readonly focusLock = new FocusLock(); + + async getPage(): Promise { + if (this.page && this.browser?.connected) { + await this.focusLock.inject(this.page); + return this.page; + } + return this.connect(); + } + + private async connect(): Promise { + this.browser = await puppeteer.connect({ + browserURL: `http://127.0.0.1:${CDP_PORT}`, + protocolTimeout: 60_000, + defaultViewport: null, + }); + const pages = await this.browser.pages(); + + // Find the actual app page, skipping chrome-extension:// background pages + const appPage = pages.find( + (p) => !p.url().startsWith("chrome-extension://"), + ); + if (!appPage) { + throw new Error( + `[desktop-mcp] No app pages found via CDP (found ${pages.length} pages, all extensions)`, + ); + } + this.page = appPage; + + this.consoleCapture.attach(this.page); + this.focusLock.attach(this.page); + await this.focusLock.inject(this.page); + + this.browser.on("disconnected", () => { + this.browser = null; + this.page = null; + }); + + return this.page; + } +} diff --git a/packages/desktop-mcp/src/mcp/connection/index.ts b/packages/desktop-mcp/src/mcp/connection/index.ts new file mode 100644 index 00000000000..964c230fa69 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/connection/index.ts @@ -0,0 +1 @@ +export { ConnectionManager } from "./connection-manager.js"; diff --git a/packages/desktop-mcp/src/mcp/console-capture/console-capture.ts b/packages/desktop-mcp/src/mcp/console-capture/console-capture.ts new file mode 100644 index 00000000000..4c63aa885a7 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/console-capture/console-capture.ts @@ -0,0 +1,53 @@ +import type { ConsoleMessage, Page } from "puppeteer-core"; +import type { ConsoleLogEntry } from "../../zod.js"; + +const LEVEL_MAP: Record = { + verbose: 0, + debug: 0, + info: 1, + log: 1, + warning: 2, + warn: 2, + error: 3, +}; + +export class ConsoleCapture { + private logs: ConsoleLogEntry[] = []; + private maxSize = 500; + + attach(page: Page) { + page.on("console", (msg: ConsoleMessage) => { + const level = LEVEL_MAP[msg.type()] ?? 1; + const location = msg.location(); + this.logs.push({ + level, + message: msg.text(), + source: location.url ?? "", + line: location.lineNumber ?? 0, + timestamp: Date.now(), + }); + if (this.logs.length > this.maxSize) this.logs.shift(); + }); + } + + getLogs({ + level, + limit, + }: { + level?: number; + limit?: number; + }): ConsoleLogEntry[] { + let filtered = this.logs; + if (level !== undefined) { + filtered = filtered.filter((log) => log.level === level); + } + if (limit !== undefined) { + filtered = filtered.slice(-limit); + } + return filtered; + } + + clear() { + this.logs = []; + } +} diff --git a/packages/desktop-mcp/src/server/console-capture/index.ts b/packages/desktop-mcp/src/mcp/console-capture/index.ts similarity index 100% rename from packages/desktop-mcp/src/server/console-capture/index.ts rename to packages/desktop-mcp/src/mcp/console-capture/index.ts diff --git a/packages/desktop-mcp/src/server/dom-inspector/dom-inspector.ts b/packages/desktop-mcp/src/mcp/dom-inspector/dom-inspector.ts similarity index 97% rename from packages/desktop-mcp/src/server/dom-inspector/dom-inspector.ts rename to packages/desktop-mcp/src/mcp/dom-inspector/dom-inspector.ts index 081908a3b35..35a7334b933 100644 --- a/packages/desktop-mcp/src/server/dom-inspector/dom-inspector.ts +++ b/packages/desktop-mcp/src/mcp/dom-inspector/dom-inspector.ts @@ -1,5 +1,5 @@ /** - * JavaScript source to inject into the renderer via executeJavaScript(). + * JavaScript source to inject into the renderer via page.evaluate(). * Walks the DOM and returns a flat list of visible elements with metadata. */ export const DOM_INSPECTOR_SCRIPT = `function inspectDom({ selector, interactiveOnly }) { diff --git a/packages/desktop-mcp/src/server/dom-inspector/index.ts b/packages/desktop-mcp/src/mcp/dom-inspector/index.ts similarity index 100% rename from packages/desktop-mcp/src/server/dom-inspector/index.ts rename to packages/desktop-mcp/src/mcp/dom-inspector/index.ts diff --git a/packages/desktop-mcp/src/mcp/focus-lock/focus-lock.ts b/packages/desktop-mcp/src/mcp/focus-lock/focus-lock.ts new file mode 100644 index 00000000000..6cc533f373d --- /dev/null +++ b/packages/desktop-mcp/src/mcp/focus-lock/focus-lock.ts @@ -0,0 +1,92 @@ +import type { Page } from "puppeteer-core"; + +const IDLE_TIMEOUT_MS = 5000; + +/** + * JS injected into the renderer to suppress focus-loss-triggered UI dismissals. + * + * How it works: + * - Radix UI (and similar) close dropdowns/popovers when they detect focus leaving + * the component via blur/focusout events. + * - When the Electron window loses OS focus (e.g., Claude Code's terminal takes over + * between MCP tool calls), blur fires with `relatedTarget === null`. + * - This script suppresses those blur/focusout events in the capture phase, + * before React/Radix can see them. + * - Important: on macOS, clicking a button does NOT focus it, so in-app clicks also + * produce blur events with `relatedTarget === null`. We distinguish window blur from + * click blur by checking the *original* `document.hasFocus()` — it returns `false` + * only when the OS window has actually lost focus. + */ +const LOCK_SCRIPT = `(() => { + if (window.__AUTOMATION_FOCUS_LOCK__) return; + window.__AUTOMATION_FOCUS_LOCK__ = true; + + const suppress = (e) => { + if (e.relatedTarget === null && !document.hasFocus()) { + e.stopImmediatePropagation(); + } + }; + document.addEventListener('blur', suppress, true); + document.addEventListener('focusout', suppress, true); + + window.__AUTOMATION_FOCUS_LOCK_CLEANUP__ = () => { + document.removeEventListener('blur', suppress, true); + document.removeEventListener('focusout', suppress, true); + delete window.__AUTOMATION_FOCUS_LOCK__; + delete window.__AUTOMATION_FOCUS_LOCK_CLEANUP__; + }; +})()`; + +const UNLOCK_SCRIPT = `(() => { + if (window.__AUTOMATION_FOCUS_LOCK_CLEANUP__) { + window.__AUTOMATION_FOCUS_LOCK_CLEANUP__(); + } +})()`; + +/** + * Manages automatic focus-lock injection for the Electron renderer via CDP. + * + * Activates on the first automation request and auto-deactivates after + * {@link IDLE_TIMEOUT_MS} of inactivity, so normal manual usage is unaffected. + */ +export class FocusLock { + private locked = false; + private timeout: ReturnType | null = null; + + /** Inject the lock script on navigation so it persists across page loads. */ + attach(page: Page) { + page.on("load", async () => { + this.locked = false; + if (this.timeout) { + // Re-inject if we were still in an active automation session + await this.inject(page); + } + }); + } + + /** Activate (or extend) the focus lock. Call on every automation request. */ + async inject(page: Page) { + if (!this.locked) { + await page.evaluate(LOCK_SCRIPT); + this.locked = true; + } + + if (this.timeout) clearTimeout(this.timeout); + this.timeout = setTimeout(() => this.unlock(page), IDLE_TIMEOUT_MS); + } + + /** Deactivate the focus lock and restore normal behavior. */ + async unlock(page: Page) { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + if (!this.locked) return; + try { + await page.evaluate(UNLOCK_SCRIPT); + } catch { + // page may have navigated or been destroyed + } + this.locked = false; + } +} diff --git a/packages/desktop-mcp/src/mcp/focus-lock/index.ts b/packages/desktop-mcp/src/mcp/focus-lock/index.ts new file mode 100644 index 00000000000..76e6b28ea23 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/focus-lock/index.ts @@ -0,0 +1 @@ +export { FocusLock } from "./focus-lock.js"; diff --git a/packages/desktop-mcp/src/mcp/mcp-server.ts b/packages/desktop-mcp/src/mcp/mcp-server.ts index 12bf26801ad..0689f271c8d 100644 --- a/packages/desktop-mcp/src/mcp/mcp-server.ts +++ b/packages/desktop-mcp/src/mcp/mcp-server.ts @@ -1,4 +1,5 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ConnectionManager } from "./connection/index.js"; import { registerTools } from "./tools/index.js"; export function createMcpServer(): McpServer { @@ -6,6 +7,14 @@ export function createMcpServer(): McpServer { { name: "desktop-automation", version: "0.1.0" }, { capabilities: { tools: {} } }, ); - registerTools(server); + + const connection = new ConnectionManager(); + + registerTools({ + server, + getPage: () => connection.getPage(), + consoleCapture: connection.consoleCapture, + }); + return server; } diff --git a/packages/desktop-mcp/src/mcp/tools/click/click.ts b/packages/desktop-mcp/src/mcp/tools/click/click.ts index bc757d950b2..6c52b3ee1f2 100644 --- a/packages/desktop-mcp/src/mcp/tools/click/click.ts +++ b/packages/desktop-mcp/src/mcp/tools/click/click.ts @@ -1,9 +1,46 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import type { ClickResponse } from "../../../zod.js"; -import { automationFetch } from "../../client/index.js"; +import type { ToolContext } from "../index.js"; -export function register(server: McpServer) { +/** + * Script injected into the page to find an element and return its center coordinates. + * The caller then uses page.mouse.click() via CDP for proper event dispatch. + */ +const FIND_ELEMENT_SCRIPT = `(opts) => { + const { selector, text, testId, index, fuzzy } = opts; + let el; + + if (selector) { + el = document.querySelectorAll(selector)[index]; + } else if (testId) { + el = document.querySelectorAll('[data-testid="' + testId + '"]')[index]; + } else if (text) { + const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); + const matches = []; + let node; + while (node = walker.nextNode()) { + const content = node.textContent.trim(); + if (fuzzy + ? content.toLowerCase().includes(text.toLowerCase()) + : content === text) { + matches.push(node.parentElement); + } + } + el = matches[index]; + } + + if (!el) return null; + + el.scrollIntoView({ block: 'nearest' }); + const rect = el.getBoundingClientRect(); + return { + tag: el.tagName.toLowerCase(), + text: (el.textContent || '').trim().slice(0, 100), + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + }; +}`; + +export function register({ server, getPage }: ToolContext) { server.registerTool( "click", { @@ -34,16 +71,59 @@ export function register(server: McpServer) { }, }, async (args) => { - const data = await automationFetch("/click", { - method: "POST", - body: JSON.stringify(args), + const page = await getPage(); + + // Click by coordinates + if (args.x !== undefined && args.y !== undefined) { + await page.mouse.click(args.x as number, args.y as number); + return { + content: [ + { + type: "text" as const, + text: `Clicked at (${args.x}, ${args.y})`, + }, + ], + }; + } + + // Find element, get its center coordinates, then click via CDP mouse + const opts = JSON.stringify({ + selector: (args.selector as string) ?? null, + text: (args.text as string) ?? null, + testId: (args.testId as string) ?? null, + index: (args.index as number) ?? 0, + fuzzy: (args.fuzzy as boolean) ?? true, }); + const result = await page.evaluate(`(${FIND_ELEMENT_SCRIPT})(${opts})`); + const info = result as { + tag: string; + text: string; + x: number; + y: number; + } | null; + + if (!info) { + return { + content: [ + { + type: "text" as const, + text: "Element not found", + }, + ], + isError: true, + }; + } + + // Use CDP mouse click — this dispatches all events correctly + await page.mouse.click(info.x, info.y); - const desc = data.element - ? `Clicked <${data.element.tag}> "${data.element.text}"` - : "Click sent"; return { - content: [{ type: "text", text: desc }], + content: [ + { + type: "text" as const, + text: `Clicked <${info.tag}> "${info.text}"`, + }, + ], }; }, ); diff --git a/packages/desktop-mcp/src/mcp/tools/evaluate-js/evaluate-js.ts b/packages/desktop-mcp/src/mcp/tools/evaluate-js/evaluate-js.ts index 3bcff713f96..acabb41e0a3 100644 --- a/packages/desktop-mcp/src/mcp/tools/evaluate-js/evaluate-js.ts +++ b/packages/desktop-mcp/src/mcp/tools/evaluate-js/evaluate-js.ts @@ -1,9 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import type { EvaluateResponse } from "../../../zod.js"; -import { automationFetch } from "../../client/index.js"; +import type { ToolContext } from "../index.js"; -export function register(server: McpServer) { +export function register({ server, getPage }: ToolContext) { server.registerTool( "evaluate_js", { @@ -14,21 +12,31 @@ export function register(server: McpServer) { }, }, async (args) => { - const data = await automationFetch("/evaluate", { - method: "POST", - body: JSON.stringify({ code: args.code }), - }); - return { - content: [ - { - type: "text", - text: - typeof data.result === "string" - ? data.result - : JSON.stringify(data.result, null, 2), - }, - ], - }; + const page = await getPage(); + try { + const result = await page.evaluate(args.code as string); + return { + content: [ + { + type: "text" as const, + text: + typeof result === "string" + ? result + : JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text" as const, + text: `Error: ${String(error)}`, + }, + ], + isError: true, + }; + } }, ); } diff --git a/packages/desktop-mcp/src/mcp/tools/get-console-logs/get-console-logs.ts b/packages/desktop-mcp/src/mcp/tools/get-console-logs/get-console-logs.ts index 8fc1bd03ecc..a2bdaa7ad4e 100644 --- a/packages/desktop-mcp/src/mcp/tools/get-console-logs/get-console-logs.ts +++ b/packages/desktop-mcp/src/mcp/tools/get-console-logs/get-console-logs.ts @@ -1,7 +1,5 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import type { ConsoleLogsResponse } from "../../../zod.js"; -import { automationFetch } from "../../client/index.js"; +import type { ToolContext } from "../index.js"; const LEVEL_NAMES: Record = { 0: "DEBUG", @@ -10,7 +8,15 @@ const LEVEL_NAMES: Record = { 3: "ERROR", }; -export function register(server: McpServer) { +const LEVEL_MAP: Record = { + debug: 0, + log: 1, + info: 1, + warn: 2, + error: 3, +}; + +export function register({ server, consoleCapture }: ToolContext) { server.registerTool( "get_console_logs", { @@ -34,17 +40,15 @@ export function register(server: McpServer) { }, }, async (args) => { - const params = new URLSearchParams(); - if (args.level) params.set("level", args.level as string); - if (args.limit) params.set("limit", String(args.limit)); - if (args.clear) params.set("clear", "true"); - const qs = params.toString(); + const levelNum = args.level ? LEVEL_MAP[args.level as string] : undefined; + const logs = consoleCapture.getLogs({ + level: levelNum, + limit: args.limit as number | undefined, + }); - const data = await automationFetch( - `/console-logs${qs ? `?${qs}` : ""}`, - ); + if (args.clear) consoleCapture.clear(); - const lines = data.logs.map((log) => { + const lines = logs.map((log) => { const level = LEVEL_NAMES[log.level] || String(log.level); const time = new Date(log.timestamp).toISOString().slice(11, 23); return `[${time}] ${level}: ${log.message}`; @@ -53,7 +57,7 @@ export function register(server: McpServer) { return { content: [ { - type: "text", + type: "text" as const, text: lines.length > 0 ? lines.join("\n") : "No console logs", }, ], diff --git a/packages/desktop-mcp/src/mcp/tools/get-window-info/get-window-info.ts b/packages/desktop-mcp/src/mcp/tools/get-window-info/get-window-info.ts index 6369ea7b802..fd84ffe1f82 100644 --- a/packages/desktop-mcp/src/mcp/tools/get-window-info/get-window-info.ts +++ b/packages/desktop-mcp/src/mcp/tools/get-window-info/get-window-info.ts @@ -1,8 +1,14 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { WindowInfoResponse } from "../../../zod.js"; -import { automationFetch } from "../../client/index.js"; +import type { ToolContext } from "../index.js"; -export function register(server: McpServer) { +const WINDOW_INFO_SCRIPT = `(() => ({ + title: document.title, + url: window.location.href, + viewportWidth: window.innerWidth, + viewportHeight: window.innerHeight, + focused: document.hasFocus(), +}))()`; + +export function register({ server, getPage }: ToolContext) { server.registerTool( "get_window_info", { @@ -11,20 +17,25 @@ export function register(server: McpServer) { inputSchema: {}, }, async () => { - const data = await automationFetch("/window-info"); + const page = await getPage(); + const info = (await page.evaluate(WINDOW_INFO_SCRIPT)) as { + title: string; + url: string; + viewportWidth: number; + viewportHeight: number; + focused: boolean; + }; + const viewport = page.viewport(); const lines = [ - `Title: ${data.title}`, - `URL: ${data.url}`, - `Bounds: ${data.bounds.x},${data.bounds.y} ${data.bounds.width}x${data.bounds.height}`, - `Focused: ${data.focused}`, - `Maximized: ${data.maximized}`, - `Fullscreen: ${data.fullscreen}`, - `Visible: ${data.visible}`, + `Title: ${info.title}`, + `URL: ${info.url}`, + `Viewport: ${viewport?.width ?? info.viewportWidth}x${viewport?.height ?? info.viewportHeight}`, + `Focused: ${info.focused}`, ]; return { - content: [{ type: "text", text: lines.join("\n") }], + content: [{ type: "text" as const, text: lines.join("\n") }], }; }, ); diff --git a/packages/desktop-mcp/src/mcp/tools/index.ts b/packages/desktop-mcp/src/mcp/tools/index.ts index c13e7c27340..71785f0ecf4 100644 --- a/packages/desktop-mcp/src/mcp/tools/index.ts +++ b/packages/desktop-mcp/src/mcp/tools/index.ts @@ -1,4 +1,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Page } from "puppeteer-core"; +import type { ConsoleCapture } from "../console-capture/index.js"; import { register as click } from "./click/index.js"; import { register as evaluateJs } from "./evaluate-js/index.js"; import { register as getConsoleLogs } from "./get-console-logs/index.js"; @@ -9,6 +11,12 @@ import { register as sendKeys } from "./send-keys/index.js"; import { register as takeScreenshot } from "./take-screenshot/index.js"; import { register as typeText } from "./type-text/index.js"; +export interface ToolContext { + server: McpServer; + getPage: () => Promise; + consoleCapture: ConsoleCapture; +} + const allTools = [ takeScreenshot, inspectDom, @@ -21,8 +29,8 @@ const allTools = [ getWindowInfo, ]; -export function registerTools(server: McpServer) { +export function registerTools(ctx: ToolContext) { for (const register of allTools) { - register(server); + register(ctx); } } diff --git a/packages/desktop-mcp/src/mcp/tools/inspect-dom/inspect-dom.ts b/packages/desktop-mcp/src/mcp/tools/inspect-dom/inspect-dom.ts index b0fb034d605..a7b9d8fb5a4 100644 --- a/packages/desktop-mcp/src/mcp/tools/inspect-dom/inspect-dom.ts +++ b/packages/desktop-mcp/src/mcp/tools/inspect-dom/inspect-dom.ts @@ -1,9 +1,20 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import type { DomResponse } from "../../../zod.js"; -import { automationFetch } from "../../client/index.js"; +import { DOM_INSPECTOR_SCRIPT } from "../../dom-inspector/index.js"; +import type { ToolContext } from "../index.js"; -export function register(server: McpServer) { +interface DomElement { + tag: string; + selector: string; + text?: string; + interactive?: boolean; + disabled?: boolean; + focused?: boolean; + role?: string; + testId?: string; + bounds: { x: number; y: number; width: number; height: number }; +} + +export function register({ server, getPage }: ToolContext) { server.registerTool( "inspect_dom", { @@ -23,16 +34,12 @@ export function register(server: McpServer) { }, }, async (args) => { - const params = new URLSearchParams(); - if (args.selector) params.set("selector", args.selector as string); - if (args.interactiveOnly) params.set("interactiveOnly", "true"); - const qs = params.toString(); - - const data = await automationFetch( - `/dom${qs ? `?${qs}` : ""}`, - ); + const page = await getPage(); + const elements = (await page.evaluate( + `(${DOM_INSPECTOR_SCRIPT})(${JSON.stringify({ selector: args.selector, interactiveOnly: args.interactiveOnly })})`, + )) as DomElement[]; - const lines = data.elements.map((el) => { + const lines = elements.map((el) => { const attrs = [ el.interactive ? "interactive" : "", el.disabled ? "disabled" : "", @@ -49,7 +56,7 @@ export function register(server: McpServer) { return { content: [ { - type: "text", + type: "text" as const, text: lines.length > 0 ? lines.join("\n") : "No elements found", }, ], diff --git a/packages/desktop-mcp/src/mcp/tools/navigate/navigate.ts b/packages/desktop-mcp/src/mcp/tools/navigate/navigate.ts index d80a7374fb8..5bd3ed99d19 100644 --- a/packages/desktop-mcp/src/mcp/tools/navigate/navigate.ts +++ b/packages/desktop-mcp/src/mcp/tools/navigate/navigate.ts @@ -1,9 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import type { NavigateResponse } from "../../../zod.js"; -import { automationFetch } from "../../client/index.js"; +import type { ToolContext } from "../index.js"; -export function register(server: McpServer) { +export function register({ server, getPage }: ToolContext) { server.registerTool( "navigate", { @@ -18,17 +16,32 @@ export function register(server: McpServer) { }, }, async (args) => { - const data = await automationFetch("/navigate", { - method: "POST", - body: JSON.stringify(args), - }); + const page = await getPage(); + + if (args.url) { + await page.goto(args.url as string); + } else if (args.path) { + await page.evaluate( + `window.location.hash = ${JSON.stringify(`#${args.path}`)}`, + ); + } else { + return { + content: [ + { + type: "text" as const, + text: "Must provide url or path", + }, + ], + isError: true, + }; + } + + const currentUrl = page.url(); return { content: [ { - type: "text", - text: data.success - ? `Navigated to ${data.url}` - : "Navigation failed", + type: "text" as const, + text: `Navigated to ${currentUrl}`, }, ], }; diff --git a/packages/desktop-mcp/src/mcp/tools/send-keys/send-keys.ts b/packages/desktop-mcp/src/mcp/tools/send-keys/send-keys.ts index 00f2be5f890..ac237bda7b3 100644 --- a/packages/desktop-mcp/src/mcp/tools/send-keys/send-keys.ts +++ b/packages/desktop-mcp/src/mcp/tools/send-keys/send-keys.ts @@ -1,9 +1,45 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { KeyInput } from "puppeteer-core"; import { z } from "zod"; -import type { SendKeysResponse } from "../../../zod.js"; -import { automationFetch } from "../../client/index.js"; +import type { ToolContext } from "../index.js"; -export function register(server: McpServer) { +/** + * Map from human-readable key names to CDP key identifiers. + * @see https://pptr.dev/api/puppeteer.keyinput + */ +const KEY_MAP: Record = { + meta: "Meta", + cmd: "Meta", + command: "Meta", + ctrl: "Control", + control: "Control", + alt: "Alt", + option: "Alt", + shift: "Shift", + enter: "Enter", + return: "Enter", + escape: "Escape", + esc: "Escape", + tab: "Tab", + backspace: "Backspace", + delete: "Delete", + space: " ", + arrowup: "ArrowUp", + arrowdown: "ArrowDown", + arrowleft: "ArrowLeft", + arrowright: "ArrowRight", + up: "ArrowUp", + down: "ArrowDown", + left: "ArrowLeft", + right: "ArrowRight", +}; + +const MODIFIER_KEYS = new Set(["Meta", "Control", "Alt", "Shift"]); + +function normalizeKey(key: string): string { + return KEY_MAP[key.toLowerCase()] ?? key; +} + +export function register({ server, getPage }: ToolContext) { server.registerTool( "send_keys", { @@ -18,16 +54,37 @@ export function register(server: McpServer) { }, }, async (args) => { - const data = await automationFetch("/send-keys", { - method: "POST", - body: JSON.stringify(args), - }); - - const desc = data.success - ? `Sent keys: ${(args.keys as string[]).join("+")}` - : "Failed to send keys"; + const page = await getPage(); + const keys = (args.keys as string[]).map(normalizeKey); + + const modifiers = keys.filter((k) => MODIFIER_KEYS.has(k)); + const nonModifiers = keys.filter((k) => !MODIFIER_KEYS.has(k)); + + // Hold modifiers, press the key, release modifiers + for (const mod of modifiers) { + await page.keyboard.down(mod as KeyInput); + } + + if (nonModifiers.length > 0) { + for (const key of nonModifiers) { + await page.keyboard.press(key as KeyInput); + } + } else if (modifiers.length > 0) { + // All modifiers with no key — press the last modifier + await page.keyboard.press(modifiers[modifiers.length - 1] as KeyInput); + } + + for (const mod of modifiers.reverse()) { + await page.keyboard.up(mod as KeyInput); + } + return { - content: [{ type: "text", text: desc }], + content: [ + { + type: "text" as const, + text: `Sent keys: ${(args.keys as string[]).join("+")}`, + }, + ], }; }, ); diff --git a/packages/desktop-mcp/src/mcp/tools/take-screenshot/take-screenshot.ts b/packages/desktop-mcp/src/mcp/tools/take-screenshot/take-screenshot.ts index e4394e52ab3..5deafd943b8 100644 --- a/packages/desktop-mcp/src/mcp/tools/take-screenshot/take-screenshot.ts +++ b/packages/desktop-mcp/src/mcp/tools/take-screenshot/take-screenshot.ts @@ -1,9 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import type { ScreenshotResponse } from "../../../zod.js"; -import { automationFetch } from "../../client/index.js"; +import type { ToolContext } from "../index.js"; -export function register(server: McpServer) { +export function register({ server, getPage }: ToolContext) { server.registerTool( "take_screenshot", { @@ -24,18 +22,25 @@ export function register(server: McpServer) { }, }, async (args) => { - const params = args.rect - ? `?rect=${args.rect.x},${args.rect.y},${args.rect.width},${args.rect.height}` - : ""; - const data = await automationFetch( - `/screenshot${params}`, - ); + const page = await getPage(); + const base64 = await page.screenshot({ + encoding: "base64", + type: "png", + clip: args.rect + ? { + x: args.rect.x as number, + y: args.rect.y as number, + width: args.rect.width as number, + height: args.rect.height as number, + } + : undefined, + }); return { content: [ { - type: "image", - data: data.image, - mimeType: "image/png", + type: "image" as const, + data: base64, + mimeType: "image/png" as const, }, ], }; diff --git a/packages/desktop-mcp/src/mcp/tools/type-text/type-text.ts b/packages/desktop-mcp/src/mcp/tools/type-text/type-text.ts index 2d46af2abb3..2cef4306dd3 100644 --- a/packages/desktop-mcp/src/mcp/tools/type-text/type-text.ts +++ b/packages/desktop-mcp/src/mcp/tools/type-text/type-text.ts @@ -1,9 +1,7 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import type { TypeResponse } from "../../../zod.js"; -import { automationFetch } from "../../client/index.js"; +import type { ToolContext } from "../index.js"; -export function register(server: McpServer) { +export function register({ server, getPage }: ToolContext) { server.registerTool( "type_text", { @@ -22,15 +20,26 @@ export function register(server: McpServer) { }, }, async (args) => { - const data = await automationFetch("/type", { - method: "POST", - body: JSON.stringify(args), - }); + const page = await getPage(); + + if (args.selector) { + await page.click(args.selector as string); + } + + if (args.clearFirst) { + // Select all then type to replace + await page.keyboard.down("Meta"); + await page.keyboard.press("a"); + await page.keyboard.up("Meta"); + } + + await page.keyboard.type(args.text as string); + return { content: [ { - type: "text", - text: data.success ? `Typed "${args.text}"` : "Failed to type", + type: "text" as const, + text: `Typed "${args.text}"`, }, ], }; diff --git a/packages/desktop-mcp/src/server/console-capture/console-capture.ts b/packages/desktop-mcp/src/server/console-capture/console-capture.ts deleted file mode 100644 index d0acffd3cf8..00000000000 --- a/packages/desktop-mcp/src/server/console-capture/console-capture.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { WebContents } from "electron"; -import type { ConsoleLogEntry } from "../../zod.js"; - -export class ConsoleCapture { - private logs: ConsoleLogEntry[] = []; - private maxSize = 500; - - attach(webContents: WebContents) { - webContents.on( - "console-message", - (_event, level, message, line, sourceId) => { - this.logs.push({ - level, - message, - source: sourceId, - line, - timestamp: Date.now(), - }); - if (this.logs.length > this.maxSize) this.logs.shift(); - }, - ); - } - - getLogs({ - level, - limit, - }: { - level?: number; - limit?: number; - }): ConsoleLogEntry[] { - let filtered = this.logs; - if (level !== undefined) { - filtered = filtered.filter((log) => log.level === level); - } - if (limit !== undefined) { - filtered = filtered.slice(-limit); - } - return filtered; - } - - clear() { - this.logs = []; - } -} diff --git a/packages/desktop-mcp/src/server/handlers/click/click.ts b/packages/desktop-mcp/src/server/handlers/click/click.ts deleted file mode 100644 index 4b6209ad0f6..00000000000 --- a/packages/desktop-mcp/src/server/handlers/click/click.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { BrowserWindow } from "electron"; -import type { RequestHandler } from "express"; -import { ClickRequestSchema } from "../../../zod.js"; - -export function clickHandler( - getWindow: () => BrowserWindow | null, -): RequestHandler { - return async (req, res) => { - const win = getWindow(); - if (!win) { - res.status(503).json({ error: "No window available" }); - return; - } - - const parsed = ClickRequestSchema.safeParse(req.body); - if (!parsed.success) { - res.status(400).json({ error: parsed.error.message }); - return; - } - - const { selector, text, testId, x, y, index, fuzzy } = parsed.data; - - // Click by coordinates - if (x !== undefined && y !== undefined) { - win.webContents.sendInputEvent({ - type: "mouseDown", - x, - y, - button: "left", - clickCount: 1, - }); - win.webContents.sendInputEvent({ - type: "mouseUp", - x, - y, - button: "left", - clickCount: 1, - }); - res.json({ success: true }); - return; - } - - // Build JS to find and click element - let findScript: string; - if (selector) { - findScript = `document.querySelectorAll(${JSON.stringify(selector)})[${index}]`; - } else if (testId) { - findScript = `document.querySelectorAll('[data-testid="${testId}"]')[${index}]`; - } else if (text) { - const matchExpr = fuzzy - ? `content.toLowerCase().includes(${JSON.stringify(text.toLowerCase())})` - : `content === ${JSON.stringify(text)}`; - findScript = `(() => { - const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); - const matches = []; - let node; - while (node = walker.nextNode()) { - const content = node.textContent.trim(); - if (${matchExpr}) { - matches.push(node.parentElement); - } - } - return matches[${index}]; - })()`; - } else { - res.status(400).json({ - error: "Must provide selector, text, testId, or x/y coordinates", - }); - return; - } - - const result = await win.webContents.executeJavaScript(`(() => { - const el = ${findScript}; - if (!el) return null; - el.click(); - return { - tag: el.tagName.toLowerCase(), - text: (el.textContent || '').trim().slice(0, 100), - selector: el.id ? '#' + el.id : el.tagName.toLowerCase(), - }; - })()`); - - if (!result) { - res.status(404).json({ error: "Element not found" }); - return; - } - - res.json({ success: true, element: result }); - }; -} diff --git a/packages/desktop-mcp/src/server/handlers/click/index.ts b/packages/desktop-mcp/src/server/handlers/click/index.ts deleted file mode 100644 index 14e7defe9aa..00000000000 --- a/packages/desktop-mcp/src/server/handlers/click/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { clickHandler } from "./click.js"; diff --git a/packages/desktop-mcp/src/server/handlers/console-logs/console-logs.ts b/packages/desktop-mcp/src/server/handlers/console-logs/console-logs.ts deleted file mode 100644 index de9fd585484..00000000000 --- a/packages/desktop-mcp/src/server/handlers/console-logs/console-logs.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { RequestHandler } from "express"; -import { ConsoleLogsRequestSchema } from "../../../zod.js"; -import type { ConsoleCapture } from "../../console-capture/index.js"; - -const LEVEL_MAP: Record = { - debug: 0, - log: 1, - info: 1, - warn: 2, - error: 3, -}; - -export function consoleLogsHandler( - consoleCapture: ConsoleCapture, -): RequestHandler { - return (req, res) => { - const parsed = ConsoleLogsRequestSchema.safeParse({ - level: req.query.level, - limit: req.query.limit ? Number(req.query.limit) : undefined, - clear: req.query.clear === "true", - }); - - if (!parsed.success) { - res.status(400).json({ error: parsed.error.message }); - return; - } - - const { level, limit, clear } = parsed.data; - const levelNum = level ? LEVEL_MAP[level] : undefined; - const logs = consoleCapture.getLogs({ level: levelNum, limit }); - - if (clear) consoleCapture.clear(); - - res.json({ logs }); - }; -} diff --git a/packages/desktop-mcp/src/server/handlers/console-logs/index.ts b/packages/desktop-mcp/src/server/handlers/console-logs/index.ts deleted file mode 100644 index b37bc8b933b..00000000000 --- a/packages/desktop-mcp/src/server/handlers/console-logs/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { consoleLogsHandler } from "./console-logs.js"; diff --git a/packages/desktop-mcp/src/server/handlers/dom/dom.ts b/packages/desktop-mcp/src/server/handlers/dom/dom.ts deleted file mode 100644 index 34b02c9ac70..00000000000 --- a/packages/desktop-mcp/src/server/handlers/dom/dom.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { BrowserWindow } from "electron"; -import type { RequestHandler } from "express"; -import { DOM_INSPECTOR_SCRIPT } from "../../dom-inspector/index.js"; - -export function domHandler( - getWindow: () => BrowserWindow | null, -): RequestHandler { - return async (req, res) => { - const win = getWindow(); - if (!win) { - res.status(503).json({ error: "No window available" }); - return; - } - - const selector = req.query.selector - ? String(req.query.selector) - : undefined; - const interactiveOnly = req.query.interactiveOnly === "true"; - - const elements = await win.webContents.executeJavaScript( - `(${DOM_INSPECTOR_SCRIPT})(${JSON.stringify({ selector, interactiveOnly })})`, - ); - - res.json({ elements }); - }; -} diff --git a/packages/desktop-mcp/src/server/handlers/dom/index.ts b/packages/desktop-mcp/src/server/handlers/dom/index.ts deleted file mode 100644 index dcd48aacb98..00000000000 --- a/packages/desktop-mcp/src/server/handlers/dom/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { domHandler } from "./dom.js"; diff --git a/packages/desktop-mcp/src/server/handlers/evaluate/evaluate.ts b/packages/desktop-mcp/src/server/handlers/evaluate/evaluate.ts deleted file mode 100644 index 2ffea54ab58..00000000000 --- a/packages/desktop-mcp/src/server/handlers/evaluate/evaluate.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { BrowserWindow } from "electron"; -import type { RequestHandler } from "express"; -import { EvaluateRequestSchema } from "../../../zod.js"; - -export function evaluateHandler( - getWindow: () => BrowserWindow | null, -): RequestHandler { - return async (req, res) => { - const win = getWindow(); - if (!win) { - res.status(503).json({ error: "No window available" }); - return; - } - - const parsed = EvaluateRequestSchema.safeParse(req.body); - if (!parsed.success) { - res.status(400).json({ error: parsed.error.message }); - return; - } - - try { - const result = await win.webContents.executeJavaScript(parsed.data.code); - res.json({ result }); - } catch (error) { - res.status(500).json({ error: String(error) }); - } - }; -} diff --git a/packages/desktop-mcp/src/server/handlers/evaluate/index.ts b/packages/desktop-mcp/src/server/handlers/evaluate/index.ts deleted file mode 100644 index 0fdda574ba9..00000000000 --- a/packages/desktop-mcp/src/server/handlers/evaluate/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { evaluateHandler } from "./evaluate.js"; diff --git a/packages/desktop-mcp/src/server/handlers/index.ts b/packages/desktop-mcp/src/server/handlers/index.ts deleted file mode 100644 index eee8fba2651..00000000000 --- a/packages/desktop-mcp/src/server/handlers/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { clickHandler } from "./click/index.js"; -export { consoleLogsHandler } from "./console-logs/index.js"; -export { domHandler } from "./dom/index.js"; -export { evaluateHandler } from "./evaluate/index.js"; -export { navigateHandler } from "./navigate/index.js"; -export { screenshotHandler } from "./screenshot/index.js"; -export { sendKeysHandler } from "./send-keys/index.js"; -export { typeHandler } from "./type/index.js"; -export { windowInfoHandler } from "./window-info/index.js"; diff --git a/packages/desktop-mcp/src/server/handlers/navigate/index.ts b/packages/desktop-mcp/src/server/handlers/navigate/index.ts deleted file mode 100644 index a6cd6d09312..00000000000 --- a/packages/desktop-mcp/src/server/handlers/navigate/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { navigateHandler } from "./navigate.js"; diff --git a/packages/desktop-mcp/src/server/handlers/navigate/navigate.ts b/packages/desktop-mcp/src/server/handlers/navigate/navigate.ts deleted file mode 100644 index b475b8e8cc4..00000000000 --- a/packages/desktop-mcp/src/server/handlers/navigate/navigate.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { BrowserWindow } from "electron"; -import type { RequestHandler } from "express"; -import { NavigateRequestSchema } from "../../../zod.js"; - -export function navigateHandler( - getWindow: () => BrowserWindow | null, -): RequestHandler { - return async (req, res) => { - const win = getWindow(); - if (!win) { - res.status(503).json({ error: "No window available" }); - return; - } - - const parsed = NavigateRequestSchema.safeParse(req.body); - if (!parsed.success) { - res.status(400).json({ error: parsed.error.message }); - return; - } - - const { url, path } = parsed.data; - - if (url) { - await win.webContents.loadURL(url); - } else if (path) { - await win.webContents.executeJavaScript( - `window.location.hash = ${JSON.stringify(`#${path}`)}`, - ); - } else { - res.status(400).json({ error: "Must provide url or path" }); - return; - } - - res.json({ success: true, url: win.webContents.getURL() }); - }; -} diff --git a/packages/desktop-mcp/src/server/handlers/screenshot/index.ts b/packages/desktop-mcp/src/server/handlers/screenshot/index.ts deleted file mode 100644 index c3c82266573..00000000000 --- a/packages/desktop-mcp/src/server/handlers/screenshot/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { screenshotHandler } from "./screenshot.js"; diff --git a/packages/desktop-mcp/src/server/handlers/screenshot/screenshot.ts b/packages/desktop-mcp/src/server/handlers/screenshot/screenshot.ts deleted file mode 100644 index 0053057b58b..00000000000 --- a/packages/desktop-mcp/src/server/handlers/screenshot/screenshot.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { BrowserWindow } from "electron"; -import type { RequestHandler } from "express"; - -export function screenshotHandler( - getWindow: () => BrowserWindow | null, -): RequestHandler { - return async (req, res) => { - const win = getWindow(); - if (!win) { - res.status(503).json({ error: "No window available" }); - return; - } - - let rect: - | { x: number; y: number; width: number; height: number } - | undefined; - if (req.query.rect) { - const parts = String(req.query.rect).split(",").map(Number); - if (parts.length === 4 && parts.every((n) => !Number.isNaN(n))) { - rect = { - x: parts[0] as number, - y: parts[1] as number, - width: parts[2] as number, - height: parts[3] as number, - }; - } - } - - const image = rect - ? await win.webContents.capturePage(rect) - : await win.webContents.capturePage(); - const size = image.getSize(); - const base64 = image.toPNG().toString("base64"); - - res.json({ image: base64, width: size.width, height: size.height }); - }; -} diff --git a/packages/desktop-mcp/src/server/handlers/send-keys/index.ts b/packages/desktop-mcp/src/server/handlers/send-keys/index.ts deleted file mode 100644 index 218b93e336c..00000000000 --- a/packages/desktop-mcp/src/server/handlers/send-keys/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { sendKeysHandler } from "./send-keys.js"; diff --git a/packages/desktop-mcp/src/server/handlers/send-keys/send-keys.ts b/packages/desktop-mcp/src/server/handlers/send-keys/send-keys.ts deleted file mode 100644 index fe7359d2486..00000000000 --- a/packages/desktop-mcp/src/server/handlers/send-keys/send-keys.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { BrowserWindow } from "electron"; -import type { RequestHandler } from "express"; -import { SendKeysRequestSchema } from "../../../zod.js"; - -/** - * Map from human-readable key names to Electron accelerator key codes. - * @see https://www.electronjs.org/docs/latest/api/accelerator - */ -const KEY_MAP: Record = { - meta: "Meta", - cmd: "Meta", - command: "Meta", - ctrl: "Control", - control: "Control", - alt: "Alt", - option: "Alt", - shift: "Shift", - enter: "Return", - return: "Return", - escape: "Escape", - esc: "Escape", - tab: "Tab", - backspace: "Backspace", - delete: "Delete", - space: " ", - arrowup: "Up", - arrowdown: "Down", - arrowleft: "Left", - arrowright: "Right", - up: "Up", - down: "Down", - left: "Left", - right: "Right", -}; - -const MODIFIER_KEYS = new Set(["Meta", "Control", "Alt", "Shift"]); - -function normalizeKey(key: string): string { - return KEY_MAP[key.toLowerCase()] ?? key; -} - -export function sendKeysHandler( - getWindow: () => BrowserWindow | null, -): RequestHandler { - return async (req, res) => { - const win = getWindow(); - if (!win) { - res.status(503).json({ error: "No window available" }); - return; - } - - const parsed = SendKeysRequestSchema.safeParse(req.body); - if (!parsed.success) { - res.status(400).json({ error: parsed.error.message }); - return; - } - - // Focus the window so key events are received - if (!win.isFocused()) win.focus(); - - type Modifier = "shift" | "control" | "alt" | "meta"; - - const normalized = parsed.data.keys.map(normalizeKey); - const modifiers: Modifier[] = []; - let keyCode = ""; - - for (const key of normalized) { - if (MODIFIER_KEYS.has(key)) { - modifiers.push(key.toLowerCase() as Modifier); - } else { - keyCode = key; - } - } - - // If no non-modifier key, treat last key as the keyCode - if (!keyCode && normalized.length > 0) { - keyCode = normalized[normalized.length - 1] as string; - modifiers.pop(); - } - - win.webContents.sendInputEvent({ - type: "keyDown", - keyCode, - modifiers, - }); - - win.webContents.sendInputEvent({ - type: "keyUp", - keyCode, - modifiers, - }); - - res.json({ success: true }); - }; -} diff --git a/packages/desktop-mcp/src/server/handlers/type/index.ts b/packages/desktop-mcp/src/server/handlers/type/index.ts deleted file mode 100644 index 54b1b8aeeb5..00000000000 --- a/packages/desktop-mcp/src/server/handlers/type/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { typeHandler } from "./type.js"; diff --git a/packages/desktop-mcp/src/server/handlers/type/type.ts b/packages/desktop-mcp/src/server/handlers/type/type.ts deleted file mode 100644 index b539216b9f5..00000000000 --- a/packages/desktop-mcp/src/server/handlers/type/type.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { BrowserWindow } from "electron"; -import type { RequestHandler } from "express"; -import { TypeRequestSchema } from "../../../zod.js"; - -export function typeHandler( - getWindow: () => BrowserWindow | null, -): RequestHandler { - return async (req, res) => { - const win = getWindow(); - if (!win) { - res.status(503).json({ error: "No window available" }); - return; - } - - const parsed = TypeRequestSchema.safeParse(req.body); - if (!parsed.success) { - res.status(400).json({ error: parsed.error.message }); - return; - } - - const { text, selector, clearFirst } = parsed.data; - - const elExpr = selector - ? `document.querySelector(${JSON.stringify(selector)})` - : "document.activeElement"; - - const result = await win.webContents.executeJavaScript(`(() => { - const el = ${elExpr}; - if (!el) return { success: false, error: 'Element not found' }; - el.focus(); - ${ - clearFirst - ? ` - el.value = ''; - el.dispatchEvent(new Event('input', { bubbles: true })); - ` - : "" - } - if (el.isContentEditable) { - document.execCommand('insertText', false, ${JSON.stringify(text)}); - } else { - const setter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, 'value' - )?.set || Object.getOwnPropertyDescriptor( - window.HTMLTextAreaElement.prototype, 'value' - )?.set; - if (setter) { - setter.call(el, ${clearFirst ? "" : "el.value + "}${JSON.stringify(text)}); - } else { - el.value ${clearFirst ? "=" : "+="} ${JSON.stringify(text)}; - } - el.dispatchEvent(new Event('input', { bubbles: true })); - el.dispatchEvent(new Event('change', { bubbles: true })); - } - return { success: true }; - })()`); - - res.json(result); - }; -} diff --git a/packages/desktop-mcp/src/server/handlers/window-info/index.ts b/packages/desktop-mcp/src/server/handlers/window-info/index.ts deleted file mode 100644 index 184d0cc8a08..00000000000 --- a/packages/desktop-mcp/src/server/handlers/window-info/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { windowInfoHandler } from "./window-info.js"; diff --git a/packages/desktop-mcp/src/server/handlers/window-info/window-info.ts b/packages/desktop-mcp/src/server/handlers/window-info/window-info.ts deleted file mode 100644 index 59c611a7013..00000000000 --- a/packages/desktop-mcp/src/server/handlers/window-info/window-info.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { BrowserWindow } from "electron"; -import type { RequestHandler } from "express"; - -export function windowInfoHandler( - getWindow: () => BrowserWindow | null, -): RequestHandler { - return (_req, res) => { - const win = getWindow(); - if (!win) { - res.status(503).json({ error: "No window available" }); - return; - } - - res.json({ - bounds: win.getBounds(), - title: win.getTitle(), - url: win.webContents.getURL(), - focused: win.isFocused(), - maximized: win.isMaximized(), - fullscreen: win.isFullScreen(), - visible: win.isVisible(), - }); - }; -} diff --git a/packages/desktop-mcp/src/server/index.ts b/packages/desktop-mcp/src/server/index.ts deleted file mode 100644 index f1763219721..00000000000 --- a/packages/desktop-mcp/src/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createAutomationServer } from "./server.js"; diff --git a/packages/desktop-mcp/src/server/server.ts b/packages/desktop-mcp/src/server/server.ts deleted file mode 100644 index 21dd9138995..00000000000 --- a/packages/desktop-mcp/src/server/server.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { BrowserWindow } from "electron"; -import express from "express"; -import { ConsoleCapture } from "./console-capture/index.js"; -import { clickHandler } from "./handlers/click/index.js"; -import { consoleLogsHandler } from "./handlers/console-logs/index.js"; -import { domHandler } from "./handlers/dom/index.js"; -import { evaluateHandler } from "./handlers/evaluate/index.js"; -import { navigateHandler } from "./handlers/navigate/index.js"; -import { screenshotHandler } from "./handlers/screenshot/index.js"; -import { sendKeysHandler } from "./handlers/send-keys/index.js"; -import { typeHandler } from "./handlers/type/index.js"; -import { windowInfoHandler } from "./handlers/window-info/index.js"; - -export function createAutomationServer({ - getWindow, - port = 9223, -}: { - getWindow: () => BrowserWindow | null; - port?: number; -}) { - const app = express(); - app.use(express.json()); - - const consoleCapture = new ConsoleCapture(); - - const attachConsole = () => { - const win = getWindow(); - if (win) consoleCapture.attach(win.webContents); - }; - attachConsole(); - - app.get("/health", (_req, res) => { - res.json({ status: "ok" }); - }); - - app.get("/screenshot", screenshotHandler(getWindow)); - app.get("/dom", domHandler(getWindow)); - app.post("/click", clickHandler(getWindow)); - app.post("/type", typeHandler(getWindow)); - app.post("/evaluate", evaluateHandler(getWindow)); - app.get("/console-logs", consoleLogsHandler(consoleCapture)); - app.get("/window-info", windowInfoHandler(getWindow)); - app.post("/navigate", navigateHandler(getWindow)); - app.post("/send-keys", sendKeysHandler(getWindow)); - - const server = app.listen(port, "127.0.0.1", () => { - console.log(`[automation] Listening on http://127.0.0.1:${port}`); - }); - - return { server, consoleCapture, attachConsole }; -}