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/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/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..a26e19da3c3 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,23 @@ "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", + "puppeteer-core": "^24.37.3", + "zod": "^4.3.5", + }, + "devDependencies": { + "@superset/typescript": "workspace:*", + "@types/node": "^24.9.1", + "typescript": "^5.9.3", + }, + }, "packages/durable-session": { "name": "@superset/durable-session", "version": "0.0.1", @@ -1563,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=="], @@ -1997,6 +2017,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"], @@ -2197,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=="], @@ -2585,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=="], @@ -2617,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=="], @@ -2727,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=="], @@ -2901,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=="], @@ -2939,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=="], @@ -2959,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=="], @@ -3089,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=="], @@ -3123,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=="], @@ -3225,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=="], @@ -3331,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=="], @@ -3945,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=="], @@ -3983,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=="], @@ -4087,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=="], @@ -4251,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=="], @@ -4259,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=="], @@ -4669,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=="], @@ -4753,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=="], @@ -4863,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=="], @@ -4999,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=="], @@ -5257,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=="], @@ -5425,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=="], @@ -5453,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=="], @@ -5475,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=="], @@ -5709,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=="], @@ -6177,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 new file mode 100644 index 00000000000..6fd4d9b12da --- /dev/null +++ b/packages/desktop-mcp/package.json @@ -0,0 +1,28 @@ +{ + "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", + "puppeteer-core": "^24.37.3", + "zod": "^4.3.5" + }, + "devDependencies": { + "@superset/typescript": "workspace:*", + "@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..6dfee9c83f1 --- /dev/null +++ b/packages/desktop-mcp/src/index.ts @@ -0,0 +1,13 @@ +export { createMcpServer } from "./mcp/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/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/mcp/console-capture/index.ts b/packages/desktop-mcp/src/mcp/console-capture/index.ts new file mode 100644 index 00000000000..2004f3c0cc6 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/console-capture/index.ts @@ -0,0 +1 @@ +export { ConsoleCapture } from "./console-capture.js"; diff --git a/packages/desktop-mcp/src/mcp/dom-inspector/dom-inspector.ts b/packages/desktop-mcp/src/mcp/dom-inspector/dom-inspector.ts new file mode 100644 index 00000000000..35a7334b933 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/dom-inspector/dom-inspector.ts @@ -0,0 +1,92 @@ +/** + * 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 }) { + 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/mcp/dom-inspector/index.ts b/packages/desktop-mcp/src/mcp/dom-inspector/index.ts new file mode 100644 index 00000000000..12729eeb065 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/dom-inspector/index.ts @@ -0,0 +1 @@ +export { DOM_INSPECTOR_SCRIPT } from "./dom-inspector.js"; 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/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..0689f271c8d --- /dev/null +++ b/packages/desktop-mcp/src/mcp/mcp-server.ts @@ -0,0 +1,20 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ConnectionManager } from "./connection/index.js"; +import { registerTools } from "./tools/index.js"; + +export function createMcpServer(): McpServer { + const server = new McpServer( + { name: "desktop-automation", version: "0.1.0" }, + { capabilities: { tools: {} } }, + ); + + 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 new file mode 100644 index 00000000000..6c52b3ee1f2 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/click/click.ts @@ -0,0 +1,130 @@ +import { z } from "zod"; +import type { ToolContext } from "../index.js"; + +/** + * 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", + { + 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 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); + + return { + content: [ + { + type: "text" as const, + text: `Clicked <${info.tag}> "${info.text}"`, + }, + ], + }; + }, + ); +} 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..acabb41e0a3 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/evaluate-js/evaluate-js.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; +import type { ToolContext } from "../index.js"; + +export function register({ server, getPage }: ToolContext) { + 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 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/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..a2bdaa7ad4e --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/get-console-logs/get-console-logs.ts @@ -0,0 +1,67 @@ +import { z } from "zod"; +import type { ToolContext } from "../index.js"; + +const LEVEL_NAMES: Record = { + 0: "DEBUG", + 1: "LOG", + 2: "WARN", + 3: "ERROR", +}; + +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", + { + 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 levelNum = args.level ? LEVEL_MAP[args.level as string] : undefined; + const logs = consoleCapture.getLogs({ + level: levelNum, + limit: args.limit as number | undefined, + }); + + if (args.clear) consoleCapture.clear(); + + 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}`; + }); + + return { + content: [ + { + type: "text" as const, + 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..fd84ffe1f82 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/get-window-info/get-window-info.ts @@ -0,0 +1,42 @@ +import type { ToolContext } from "../index.js"; + +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", + { + description: + "Get information about the Electron app window: bounds, title, URL, focus state, and more.", + inputSchema: {}, + }, + async () => { + 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: ${info.title}`, + `URL: ${info.url}`, + `Viewport: ${viewport?.width ?? info.viewportWidth}x${viewport?.height ?? info.viewportHeight}`, + `Focused: ${info.focused}`, + ]; + + return { + content: [{ type: "text" as const, 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..71785f0ecf4 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/index.ts @@ -0,0 +1,36 @@ +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"; +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"; + +export interface ToolContext { + server: McpServer; + getPage: () => Promise; + consoleCapture: ConsoleCapture; +} + +const allTools = [ + takeScreenshot, + inspectDom, + click, + typeText, + sendKeys, + getConsoleLogs, + evaluateJs, + navigate, + getWindowInfo, +]; + +export function registerTools(ctx: ToolContext) { + for (const register of allTools) { + register(ctx); + } +} 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..a7b9d8fb5a4 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/inspect-dom/inspect-dom.ts @@ -0,0 +1,66 @@ +import { z } from "zod"; +import { DOM_INSPECTOR_SCRIPT } from "../../dom-inspector/index.js"; +import type { ToolContext } from "../index.js"; + +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", + { + 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 page = await getPage(); + const elements = (await page.evaluate( + `(${DOM_INSPECTOR_SCRIPT})(${JSON.stringify({ selector: args.selector, interactiveOnly: args.interactiveOnly })})`, + )) as DomElement[]; + + const lines = 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" as const, + 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..5bd3ed99d19 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/navigate/navigate.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; +import type { ToolContext } from "../index.js"; + +export function register({ server, getPage }: ToolContext) { + 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 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" as const, + text: `Navigated to ${currentUrl}`, + }, + ], + }; + }, + ); +} 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..ac237bda7b3 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/send-keys/send-keys.ts @@ -0,0 +1,91 @@ +import type { KeyInput } from "puppeteer-core"; +import { z } from "zod"; +import type { ToolContext } from "../index.js"; + +/** + * 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", + { + 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 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" as const, + text: `Sent keys: ${(args.keys as string[]).join("+")}`, + }, + ], + }; + }, + ); +} 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..5deafd943b8 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/take-screenshot/take-screenshot.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; +import type { ToolContext } from "../index.js"; + +export function register({ server, getPage }: ToolContext) { + 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 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" as const, + data: base64, + mimeType: "image/png" as const, + }, + ], + }; + }, + ); +} 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..2cef4306dd3 --- /dev/null +++ b/packages/desktop-mcp/src/mcp/tools/type-text/type-text.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import type { ToolContext } from "../index.js"; + +export function register({ server, getPage }: ToolContext) { + 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 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" as const, + text: `Typed "${args.text}"`, + }, + ], + }; + }, + ); +} 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"] +}