@teapotz/electron-mcp 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ import { toolOk, toolError } from "../protocol/responses.js";
2
+ import { ConnectArgs, LaunchArgs, EmptyArgs } from "../validation/schemas.js";
3
+ export const connectionTools = [
4
+ {
5
+ definition: {
6
+ name: "connect",
7
+ description: "Connect to already running Electron app via CDP (e.g., bun run dev:local with --remote-debugging-port)",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ port: {
12
+ type: "number",
13
+ description: "CDP port (default: 9222)",
14
+ },
15
+ },
16
+ },
17
+ },
18
+ parse: (raw) => ConnectArgs.parse(raw ?? {}),
19
+ handler: async (session, args) => {
20
+ if (session.isConnected) {
21
+ return toolError("Already connected. Disconnect first.");
22
+ }
23
+ const { port } = args;
24
+ try {
25
+ const title = await session.connectCdp(port);
26
+ return toolOk(`Connected via CDP (port ${port}). Window title: ${title}`);
27
+ }
28
+ catch (error) {
29
+ const message = error instanceof Error ? error.message : String(error);
30
+ return toolError(message);
31
+ }
32
+ },
33
+ },
34
+ {
35
+ definition: {
36
+ name: "disconnect",
37
+ description: "Disconnect from CDP (does not close the app)",
38
+ inputSchema: { type: "object", properties: {} },
39
+ },
40
+ parse: (raw) => EmptyArgs.parse(raw ?? {}),
41
+ handler: async (session) => {
42
+ if (!session.isConnected) {
43
+ return toolOk("Not connected.");
44
+ }
45
+ if (session.connectionMode !== "cdp") {
46
+ return toolError("Cannot disconnect in launch mode. Use close instead.");
47
+ }
48
+ await session.disconnect();
49
+ return toolOk("Disconnected from CDP.");
50
+ },
51
+ },
52
+ {
53
+ definition: {
54
+ name: "launch",
55
+ description: "Launch Electron app for testing (builds and runs fresh instance)",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ appPath: {
60
+ type: "string",
61
+ description: "Path to Electron main.js (default: ./out/main/index.js)",
62
+ },
63
+ env: {
64
+ type: "object",
65
+ description: "Environment variables to pass to the app",
66
+ additionalProperties: { type: "string" },
67
+ },
68
+ headless: {
69
+ type: "boolean",
70
+ description: "Run in headless mode (no visible window, for CI/automation)",
71
+ },
72
+ },
73
+ },
74
+ },
75
+ parse: (raw) => LaunchArgs.parse(raw ?? {}),
76
+ handler: async (session, args) => {
77
+ if (session.isConnected) {
78
+ return toolError("Already connected. Disconnect/close first.");
79
+ }
80
+ const { appPath, env, headless } = args;
81
+ try {
82
+ const title = await session.launch({ appPath, env, headless });
83
+ return toolOk(`App launched${headless ? " (headless)" : ""}. Window title: ${title}`);
84
+ }
85
+ catch (error) {
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ return toolError(message);
88
+ }
89
+ },
90
+ },
91
+ {
92
+ definition: {
93
+ name: "close",
94
+ description: "Close the Electron app (only works with launch, not connect)",
95
+ inputSchema: { type: "object", properties: {} },
96
+ },
97
+ parse: (raw) => EmptyArgs.parse(raw ?? {}),
98
+ handler: async (session) => {
99
+ if (session.connectionMode === "cdp") {
100
+ return toolError("Cannot close CDP connection. Use disconnect instead (app keeps running).");
101
+ }
102
+ await session.close();
103
+ return toolOk("App closed.");
104
+ },
105
+ },
106
+ ];
@@ -0,0 +1,3 @@
1
+ import type { ToolSpec } from "./registry.js";
2
+ export declare function withTimeout<T>(promise: Promise<T>, ms?: number): Promise<T>;
3
+ export declare const evaluateTools: ToolSpec[];
@@ -0,0 +1,73 @@
1
+ import { toolOk, toolError } from "../protocol/responses.js";
2
+ import { EvaluateArgs } from "../validation/schemas.js";
3
+ const DEFAULT_TIMEOUT_MS = Number(process.env.ELECTRON_MCP_TIMEOUT_MS ?? 30_000);
4
+ export function withTimeout(promise, ms = DEFAULT_TIMEOUT_MS) {
5
+ let timer;
6
+ return Promise.race([
7
+ promise,
8
+ new Promise((_, reject) => {
9
+ timer = setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms);
10
+ }),
11
+ ]).finally(() => clearTimeout(timer));
12
+ }
13
+ export const evaluateTools = [
14
+ {
15
+ definition: {
16
+ name: "evaluate",
17
+ description: "Execute JavaScript in the renderer process",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ script: {
22
+ type: "string",
23
+ description: "JavaScript code to execute",
24
+ },
25
+ },
26
+ required: ["script"],
27
+ },
28
+ },
29
+ parse: (raw) => EvaluateArgs.parse(raw),
30
+ handler: async (session, args) => {
31
+ const p = session.requirePage();
32
+ const { script } = args;
33
+ const result = await withTimeout(p.evaluate(script));
34
+ const resultText = result === undefined ? "undefined" : JSON.stringify(result, null, 2);
35
+ return toolOk(resultText);
36
+ },
37
+ },
38
+ {
39
+ definition: {
40
+ name: "evaluateMain",
41
+ description: "Execute code in the Electron main process",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ script: {
46
+ type: "string",
47
+ description: "Code to execute (receives { app, ipcMain, dialog, BrowserWindow })",
48
+ },
49
+ },
50
+ required: ["script"],
51
+ },
52
+ },
53
+ parse: (raw) => EvaluateArgs.parse(raw),
54
+ handler: async (session, args) => {
55
+ if (session.connectionMode === "cdp") {
56
+ return toolError("evaluateMain is not available in CDP mode. Main process access requires launch mode.");
57
+ }
58
+ const app = session.requireElectronApp();
59
+ const { script } = args;
60
+ const result = await withTimeout(app.evaluate((electron, source) => {
61
+ // biome-ignore lint/security/noGlobalEval: intentional eval inside Electron main process
62
+ // biome-ignore lint/style/noCommaOperator: indirect eval pattern
63
+ const fn = (0, eval)(`(${source})`);
64
+ if (typeof fn !== "function") {
65
+ throw new Error("evaluateMain script must be a function expression");
66
+ }
67
+ return fn(electron);
68
+ }, script));
69
+ const resultText = result === undefined ? "undefined" : JSON.stringify(result, null, 2);
70
+ return toolOk(resultText);
71
+ },
72
+ },
73
+ ];
@@ -0,0 +1,2 @@
1
+ import type { ToolSpec } from "./registry.js";
2
+ export declare const inspectionTools: ToolSpec[];
@@ -0,0 +1,120 @@
1
+ import { toolOk, toolImage } from "../protocol/responses.js";
2
+ import { ScreenshotArgs, EmptyArgs, SelectorArgs, GetAttributeArgs, } from "../validation/schemas.js";
3
+ export const inspectionTools = [
4
+ {
5
+ definition: {
6
+ name: "screenshot",
7
+ description: "Take a screenshot of the current window",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ fullPage: {
12
+ type: "boolean",
13
+ description: "Capture full scrollable page",
14
+ },
15
+ },
16
+ },
17
+ },
18
+ parse: (raw) => ScreenshotArgs.parse(raw ?? {}),
19
+ handler: async (session, args) => {
20
+ const p = session.requirePage();
21
+ const { fullPage } = args;
22
+ const buffer = await p.screenshot({ fullPage });
23
+ return toolImage(buffer.toString("base64"));
24
+ },
25
+ },
26
+ {
27
+ definition: {
28
+ name: "snapshot",
29
+ description: "Get accessibility tree snapshot of the page (for finding elements)",
30
+ inputSchema: { type: "object", properties: {} },
31
+ },
32
+ parse: (raw) => EmptyArgs.parse(raw ?? {}),
33
+ handler: async (session) => {
34
+ const p = session.requirePage();
35
+ const snapshot = await p.locator("body").ariaSnapshot();
36
+ return toolOk(snapshot);
37
+ },
38
+ },
39
+ {
40
+ definition: {
41
+ name: "getText",
42
+ description: "Get text content of an element",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ selector: { type: "string", description: "CSS selector" },
47
+ },
48
+ required: ["selector"],
49
+ },
50
+ },
51
+ parse: (raw) => SelectorArgs.parse(raw),
52
+ handler: async (session, args) => {
53
+ const p = session.requirePage();
54
+ const { selector } = args;
55
+ const text = await p.locator(selector).textContent();
56
+ return toolOk(text || "(empty)");
57
+ },
58
+ },
59
+ {
60
+ definition: {
61
+ name: "getAttribute",
62
+ description: "Get attribute value of an element",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ selector: { type: "string", description: "CSS selector" },
67
+ attribute: { type: "string", description: "Attribute name" },
68
+ },
69
+ required: ["selector", "attribute"],
70
+ },
71
+ },
72
+ parse: (raw) => GetAttributeArgs.parse(raw),
73
+ handler: async (session, args) => {
74
+ const p = session.requirePage();
75
+ const { selector, attribute } = args;
76
+ const value = await p.locator(selector).getAttribute(attribute);
77
+ return toolOk(value ?? "(null)");
78
+ },
79
+ },
80
+ {
81
+ definition: {
82
+ name: "isVisible",
83
+ description: "Check if element is visible",
84
+ inputSchema: {
85
+ type: "object",
86
+ properties: {
87
+ selector: { type: "string", description: "CSS selector" },
88
+ },
89
+ required: ["selector"],
90
+ },
91
+ },
92
+ parse: (raw) => SelectorArgs.parse(raw),
93
+ handler: async (session, args) => {
94
+ const p = session.requirePage();
95
+ const { selector } = args;
96
+ const visible = await p.locator(selector).isVisible();
97
+ return toolOk(String(visible));
98
+ },
99
+ },
100
+ {
101
+ definition: {
102
+ name: "count",
103
+ description: "Count elements matching selector",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ selector: { type: "string", description: "CSS selector" },
108
+ },
109
+ required: ["selector"],
110
+ },
111
+ },
112
+ parse: (raw) => SelectorArgs.parse(raw),
113
+ handler: async (session, args) => {
114
+ const p = session.requirePage();
115
+ const { selector } = args;
116
+ const count = await p.locator(selector).count();
117
+ return toolOk(String(count));
118
+ },
119
+ },
120
+ ];
@@ -0,0 +1,2 @@
1
+ import type { ToolSpec } from "./registry.js";
2
+ export declare const interactionTools: ToolSpec[];
@@ -0,0 +1,233 @@
1
+ import { toolOk } from "../protocol/responses.js";
2
+ import { SelectorArgs, FillArgs, TypeArgs, PressArgs, WaitArgs, DragArgs, SelectOptionArgs, } from "../validation/schemas.js";
3
+ function normalizeWaitArgs(raw) {
4
+ if (!raw || typeof raw !== "object")
5
+ return raw;
6
+ const args = raw;
7
+ if (args.waitTimeoutMs === undefined && args.timeout !== undefined) {
8
+ return { ...args, waitTimeoutMs: args.timeout };
9
+ }
10
+ return raw;
11
+ }
12
+ export const interactionTools = [
13
+ {
14
+ definition: {
15
+ name: "click",
16
+ description: "Click an element",
17
+ inputSchema: {
18
+ type: "object",
19
+ properties: {
20
+ selector: {
21
+ type: "string",
22
+ description: 'CSS selector or text selector (e.g., "text=Submit")',
23
+ },
24
+ },
25
+ required: ["selector"],
26
+ },
27
+ },
28
+ parse: (raw) => SelectorArgs.parse(raw),
29
+ handler: async (session, args) => {
30
+ const p = session.requirePage();
31
+ const { selector } = args;
32
+ await p.click(selector);
33
+ return toolOk(`Clicked: ${selector}`);
34
+ },
35
+ },
36
+ {
37
+ definition: {
38
+ name: "fill",
39
+ description: "Fill text into an input field",
40
+ inputSchema: {
41
+ type: "object",
42
+ properties: {
43
+ selector: {
44
+ type: "string",
45
+ description: "CSS selector for the input",
46
+ },
47
+ text: { type: "string", description: "Text to fill" },
48
+ },
49
+ required: ["selector", "text"],
50
+ },
51
+ },
52
+ parse: (raw) => FillArgs.parse(raw),
53
+ handler: async (session, args) => {
54
+ const p = session.requirePage();
55
+ const { selector, text } = args;
56
+ await p.fill(selector, text);
57
+ return toolOk(`Filled "${text}" into ${selector}`);
58
+ },
59
+ },
60
+ {
61
+ definition: {
62
+ name: "type",
63
+ description: "Type text character by character (triggers key events)",
64
+ inputSchema: {
65
+ type: "object",
66
+ properties: {
67
+ selector: {
68
+ type: "string",
69
+ description: "CSS selector for the input",
70
+ },
71
+ text: { type: "string", description: "Text to type" },
72
+ },
73
+ required: ["selector", "text"],
74
+ },
75
+ },
76
+ parse: (raw) => TypeArgs.parse(raw),
77
+ handler: async (session, args) => {
78
+ const p = session.requirePage();
79
+ const { selector, text } = args;
80
+ await p.locator(selector).pressSequentially(text);
81
+ return toolOk(`Typed "${text}" into ${selector}`);
82
+ },
83
+ },
84
+ {
85
+ definition: {
86
+ name: "hover",
87
+ description: "Hover over an element",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: {
91
+ selector: { type: "string", description: "CSS selector" },
92
+ },
93
+ required: ["selector"],
94
+ },
95
+ },
96
+ parse: (raw) => SelectorArgs.parse(raw),
97
+ handler: async (session, args) => {
98
+ const p = session.requirePage();
99
+ const { selector } = args;
100
+ await p.hover(selector);
101
+ return toolOk(`Hovered: ${selector}`);
102
+ },
103
+ },
104
+ {
105
+ definition: {
106
+ name: "press",
107
+ description: "Press a keyboard key",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ key: {
112
+ type: "string",
113
+ description: 'Key to press (e.g., "Enter", "Escape", "Control+S")',
114
+ },
115
+ },
116
+ required: ["key"],
117
+ },
118
+ },
119
+ parse: (raw) => PressArgs.parse(raw),
120
+ handler: async (session, args) => {
121
+ const p = session.requirePage();
122
+ const { key } = args;
123
+ await p.keyboard.press(key);
124
+ return toolOk(`Pressed: ${key}`);
125
+ },
126
+ },
127
+ {
128
+ definition: {
129
+ name: "wait",
130
+ description: "Wait for an element or condition",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ selector: {
135
+ type: "string",
136
+ description: "CSS selector to wait for",
137
+ },
138
+ state: {
139
+ type: "string",
140
+ enum: ["visible", "hidden", "attached", "detached"],
141
+ description: "State to wait for (default: visible)",
142
+ },
143
+ waitTimeoutMs: {
144
+ type: "number",
145
+ description: "Timeout in ms (default: 5000)",
146
+ },
147
+ },
148
+ required: ["selector"],
149
+ },
150
+ },
151
+ parse: (raw) => WaitArgs.parse(normalizeWaitArgs(raw)),
152
+ handler: async (session, args) => {
153
+ const p = session.requirePage();
154
+ const { selector, state, waitTimeoutMs } = args;
155
+ await p.locator(selector).waitFor({ state, timeout: waitTimeoutMs });
156
+ return toolOk(`Element ${selector} is ${state}`);
157
+ },
158
+ },
159
+ {
160
+ definition: {
161
+ name: "drag",
162
+ description: "Drag and drop from source to target element",
163
+ inputSchema: {
164
+ type: "object",
165
+ properties: {
166
+ source: {
167
+ type: "string",
168
+ description: "CSS selector for source element to drag",
169
+ },
170
+ target: {
171
+ type: "string",
172
+ description: "CSS selector for target drop zone",
173
+ },
174
+ },
175
+ required: ["source", "target"],
176
+ },
177
+ },
178
+ parse: (raw) => DragArgs.parse(raw),
179
+ handler: async (session, args) => {
180
+ const p = session.requirePage();
181
+ const { source, target } = args;
182
+ await p.dragAndDrop(source, target);
183
+ return toolOk(`Dragged ${source} to ${target}`);
184
+ },
185
+ },
186
+ {
187
+ definition: {
188
+ name: "selectOption",
189
+ description: "Select option(s) from a dropdown",
190
+ inputSchema: {
191
+ type: "object",
192
+ properties: {
193
+ selector: {
194
+ type: "string",
195
+ description: "CSS selector for select element",
196
+ },
197
+ value: {
198
+ type: "string",
199
+ description: "Value or label to select",
200
+ },
201
+ },
202
+ required: ["selector", "value"],
203
+ },
204
+ },
205
+ parse: (raw) => SelectOptionArgs.parse(raw),
206
+ handler: async (session, args) => {
207
+ const p = session.requirePage();
208
+ const { selector, value } = args;
209
+ await p.selectOption(selector, value);
210
+ return toolOk(`Selected "${value}" in ${selector}`);
211
+ },
212
+ },
213
+ {
214
+ definition: {
215
+ name: "dblclick",
216
+ description: "Double-click an element",
217
+ inputSchema: {
218
+ type: "object",
219
+ properties: {
220
+ selector: { type: "string", description: "CSS selector" },
221
+ },
222
+ required: ["selector"],
223
+ },
224
+ },
225
+ parse: (raw) => SelectorArgs.parse(raw),
226
+ handler: async (session, args) => {
227
+ const p = session.requirePage();
228
+ const { selector } = args;
229
+ await p.dblclick(selector);
230
+ return toolOk(`Double-clicked: ${selector}`);
231
+ },
232
+ },
233
+ ];
@@ -0,0 +1,2 @@
1
+ import type { ToolSpec } from "./registry.js";
2
+ export declare const mouseTools: ToolSpec[];
@@ -0,0 +1,105 @@
1
+ import { toolOk } from "../protocol/responses.js";
2
+ import { MouseMoveArgs, MouseButtonArgs, MouseClickArgs, } from "../validation/schemas.js";
3
+ export const mouseTools = [
4
+ {
5
+ definition: {
6
+ name: "mouseMove",
7
+ description: "Move mouse cursor to specific coordinates",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ x: { type: "number", description: "X coordinate" },
12
+ y: { type: "number", description: "Y coordinate" },
13
+ steps: {
14
+ type: "number",
15
+ description: "Number of intermediate steps for smooth movement (default: 1)",
16
+ },
17
+ },
18
+ required: ["x", "y"],
19
+ },
20
+ },
21
+ parse: (raw) => MouseMoveArgs.parse(raw),
22
+ handler: async (session, args) => {
23
+ const p = session.requirePage();
24
+ const { x, y, steps } = args;
25
+ await p.mouse.move(x, y, { steps });
26
+ return toolOk(`Mouse moved to (${x}, ${y})`);
27
+ },
28
+ },
29
+ {
30
+ definition: {
31
+ name: "mouseDown",
32
+ description: "Press mouse button down",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {
36
+ button: {
37
+ type: "string",
38
+ enum: ["left", "right", "middle"],
39
+ description: "Mouse button (default: left)",
40
+ },
41
+ },
42
+ },
43
+ },
44
+ parse: (raw) => MouseButtonArgs.parse(raw ?? {}),
45
+ handler: async (session, args) => {
46
+ const p = session.requirePage();
47
+ const { button } = args;
48
+ await p.mouse.down({ button });
49
+ return toolOk(`Mouse ${button} button pressed`);
50
+ },
51
+ },
52
+ {
53
+ definition: {
54
+ name: "mouseUp",
55
+ description: "Release mouse button",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ button: {
60
+ type: "string",
61
+ enum: ["left", "right", "middle"],
62
+ description: "Mouse button (default: left)",
63
+ },
64
+ },
65
+ },
66
+ },
67
+ parse: (raw) => MouseButtonArgs.parse(raw ?? {}),
68
+ handler: async (session, args) => {
69
+ const p = session.requirePage();
70
+ const { button } = args;
71
+ await p.mouse.up({ button });
72
+ return toolOk(`Mouse ${button} button released`);
73
+ },
74
+ },
75
+ {
76
+ definition: {
77
+ name: "mouseClick",
78
+ description: "Click at specific coordinates (without selector)",
79
+ inputSchema: {
80
+ type: "object",
81
+ properties: {
82
+ x: { type: "number", description: "X coordinate" },
83
+ y: { type: "number", description: "Y coordinate" },
84
+ button: {
85
+ type: "string",
86
+ enum: ["left", "right", "middle"],
87
+ description: "Mouse button (default: left)",
88
+ },
89
+ clickCount: {
90
+ type: "number",
91
+ description: "Number of clicks (default: 1, use 2 for double-click)",
92
+ },
93
+ },
94
+ required: ["x", "y"],
95
+ },
96
+ },
97
+ parse: (raw) => MouseClickArgs.parse(raw),
98
+ handler: async (session, args) => {
99
+ const p = session.requirePage();
100
+ const { x, y, button, clickCount } = args;
101
+ await p.mouse.click(x, y, { button, clickCount });
102
+ return toolOk(`Mouse clicked at (${x}, ${y})`);
103
+ },
104
+ },
105
+ ];
@@ -0,0 +1,10 @@
1
+ import type { Tool, CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { SessionController } from "../session/SessionController.js";
3
+ export interface ToolSpec<TArgs = unknown> {
4
+ definition: Tool;
5
+ parse: (raw: unknown) => TArgs;
6
+ handler: (session: SessionController, args: TArgs) => Promise<CallToolResult>;
7
+ }
8
+ export type ToolRegistry = Map<string, ToolSpec>;
9
+ export declare function buildRegistry(): ToolRegistry;
10
+ export declare function dispatch(registry: ToolRegistry, session: SessionController, name: string, rawArgs: unknown): Promise<CallToolResult>;