electron-dev-bridge 0.1.1 → 0.2.0

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,29 @@
1
+ import type { CdpTool, ToolContext } from './types.js';
2
+ export interface ConsoleEntry {
3
+ level: string;
4
+ message: string;
5
+ timestamp: number;
6
+ }
7
+ export interface NetworkEntry {
8
+ requestId: string;
9
+ method: string;
10
+ url: string;
11
+ status?: number;
12
+ statusText?: string;
13
+ error?: string;
14
+ startTime: number;
15
+ endTime?: number;
16
+ duration?: number;
17
+ }
18
+ export declare class DevtoolsStore {
19
+ console: ConsoleEntry[];
20
+ network: Map<string, NetworkEntry>;
21
+ private attached;
22
+ attach(client: any): void;
23
+ detach(): void;
24
+ clearConsole(): void;
25
+ clearNetwork(): void;
26
+ clearAll(): void;
27
+ getNetworkEntries(): NetworkEntry[];
28
+ }
29
+ export declare function createDevtoolsTools(ctx: ToolContext): CdpTool[];
@@ -0,0 +1,249 @@
1
+ import { toolResult } from './helpers.js';
2
+ const MAX_CONSOLE_ENTRIES = 1000;
3
+ const MAX_NETWORK_ENTRIES = 500;
4
+ const MAX_BODY_LENGTH = 1024;
5
+ export class DevtoolsStore {
6
+ console = [];
7
+ network = new Map();
8
+ attached = false;
9
+ attach(client) {
10
+ if (this.attached)
11
+ return;
12
+ this.attached = true;
13
+ client.Runtime.consoleAPICalled(({ type, args, timestamp }) => {
14
+ const message = (args || [])
15
+ .map((a) => a.value ?? a.description ?? String(a.type))
16
+ .join(' ');
17
+ this.console.push({ level: type, message, timestamp });
18
+ if (this.console.length > MAX_CONSOLE_ENTRIES) {
19
+ this.console.splice(0, this.console.length - MAX_CONSOLE_ENTRIES);
20
+ }
21
+ });
22
+ client.Network.requestWillBeSent(({ requestId, request, timestamp }) => {
23
+ if (this.network.size >= MAX_NETWORK_ENTRIES) {
24
+ const oldest = this.network.keys().next().value;
25
+ this.network.delete(oldest);
26
+ }
27
+ this.network.set(requestId, {
28
+ requestId,
29
+ method: request.method,
30
+ url: request.url,
31
+ startTime: timestamp,
32
+ });
33
+ });
34
+ client.Network.responseReceived(({ requestId, response }) => {
35
+ const entry = this.network.get(requestId);
36
+ if (entry) {
37
+ entry.status = response.status;
38
+ entry.statusText = response.statusText;
39
+ }
40
+ });
41
+ client.Network.loadingFinished(({ requestId, timestamp }) => {
42
+ const entry = this.network.get(requestId);
43
+ if (entry) {
44
+ entry.endTime = timestamp;
45
+ entry.duration = Math.round((timestamp - entry.startTime) * 1000);
46
+ }
47
+ });
48
+ client.Network.loadingFailed(({ requestId, errorText }) => {
49
+ const entry = this.network.get(requestId);
50
+ if (entry) {
51
+ entry.error = errorText;
52
+ }
53
+ });
54
+ }
55
+ detach() {
56
+ this.attached = false;
57
+ }
58
+ clearConsole() {
59
+ this.console = [];
60
+ }
61
+ clearNetwork() {
62
+ this.network.clear();
63
+ }
64
+ clearAll() {
65
+ this.clearConsole();
66
+ this.clearNetwork();
67
+ }
68
+ getNetworkEntries() {
69
+ return Array.from(this.network.values());
70
+ }
71
+ }
72
+ export function createDevtoolsTools(ctx) {
73
+ const { bridge, state } = ctx;
74
+ return [
75
+ {
76
+ definition: {
77
+ name: 'electron_get_console_logs',
78
+ description: 'Get captured console messages from the Electron app. Captures console.log/info/warn/error/debug calls.',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ level: {
83
+ type: 'string',
84
+ description: 'Filter by level: log, info, warn, error, debug. Comma-separated for multiple.',
85
+ },
86
+ search: {
87
+ type: 'string',
88
+ description: 'Filter by message content (case-insensitive substring match).',
89
+ },
90
+ limit: {
91
+ type: 'number',
92
+ description: 'Max results to return. Default: 100.',
93
+ },
94
+ since: {
95
+ type: 'string',
96
+ description: 'ISO timestamp — only return logs after this time.',
97
+ },
98
+ },
99
+ },
100
+ },
101
+ handler: async ({ level, search, limit = 100, since, } = {}) => {
102
+ bridge.ensureConnected();
103
+ const store = state.devtoolsStore;
104
+ if (!store)
105
+ return toolResult({ logs: [], total: 0 });
106
+ let entries = store.console;
107
+ if (level) {
108
+ const levels = new Set(level.split(',').map(l => l.trim().toLowerCase()));
109
+ entries = entries.filter(e => levels.has(e.level));
110
+ }
111
+ if (search) {
112
+ const lower = search.toLowerCase();
113
+ entries = entries.filter(e => e.message.toLowerCase().includes(lower));
114
+ }
115
+ if (since) {
116
+ const sinceTs = new Date(since).getTime() / 1000;
117
+ entries = entries.filter(e => e.timestamp >= sinceTs);
118
+ }
119
+ const total = entries.length;
120
+ const logs = entries.slice(-limit).map(e => ({
121
+ level: e.level,
122
+ message: e.message.length > MAX_BODY_LENGTH
123
+ ? e.message.slice(0, MAX_BODY_LENGTH) + '...'
124
+ : e.message,
125
+ timestamp: new Date(e.timestamp * 1000).toISOString(),
126
+ }));
127
+ return toolResult({ logs, total, returned: logs.length });
128
+ },
129
+ },
130
+ {
131
+ definition: {
132
+ name: 'electron_get_network_requests',
133
+ description: 'Get captured network requests from the Electron app. Captures all HTTP/HTTPS requests with status, timing, and errors.',
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: {
137
+ urlPattern: {
138
+ type: 'string',
139
+ description: 'Regex pattern to filter by URL.',
140
+ },
141
+ method: {
142
+ type: 'string',
143
+ description: 'HTTP method filter (e.g. GET, POST).',
144
+ },
145
+ errorsOnly: {
146
+ type: 'boolean',
147
+ description: 'Only return failed requests (4xx/5xx or network errors).',
148
+ },
149
+ limit: {
150
+ type: 'number',
151
+ description: 'Max results to return. Default: 50.',
152
+ },
153
+ since: {
154
+ type: 'string',
155
+ description: 'ISO timestamp — only return requests after this time.',
156
+ },
157
+ },
158
+ },
159
+ },
160
+ handler: async ({ urlPattern, method, errorsOnly, limit = 50, since, } = {}) => {
161
+ bridge.ensureConnected();
162
+ const store = state.devtoolsStore;
163
+ if (!store)
164
+ return toolResult({ requests: [], total: 0 });
165
+ let entries = store.getNetworkEntries();
166
+ if (urlPattern) {
167
+ const re = new RegExp(urlPattern, 'i');
168
+ entries = entries.filter(e => re.test(e.url));
169
+ }
170
+ if (method) {
171
+ const upper = method.toUpperCase();
172
+ entries = entries.filter(e => e.method === upper);
173
+ }
174
+ if (errorsOnly) {
175
+ entries = entries.filter(e => e.error || (e.status !== undefined && e.status >= 400));
176
+ }
177
+ if (since) {
178
+ const sinceTs = new Date(since).getTime() / 1000;
179
+ entries = entries.filter(e => e.startTime >= sinceTs);
180
+ }
181
+ const total = entries.length;
182
+ const requests = entries.slice(-limit).map(e => ({
183
+ method: e.method,
184
+ url: e.url.length > MAX_BODY_LENGTH
185
+ ? e.url.slice(0, MAX_BODY_LENGTH) + '...'
186
+ : e.url,
187
+ status: e.status,
188
+ statusText: e.statusText,
189
+ error: e.error,
190
+ duration: e.duration != null ? `${e.duration}ms` : undefined,
191
+ timestamp: new Date(e.startTime * 1000).toISOString(),
192
+ }));
193
+ return toolResult({ requests, total, returned: requests.length });
194
+ },
195
+ },
196
+ {
197
+ definition: {
198
+ name: 'electron_clear_devtools_data',
199
+ description: 'Clear captured console logs and/or network request buffers.',
200
+ inputSchema: {
201
+ type: 'object',
202
+ properties: {
203
+ type: {
204
+ type: 'string',
205
+ enum: ['all', 'console', 'network'],
206
+ description: 'What to clear. Default: all.',
207
+ },
208
+ },
209
+ },
210
+ },
211
+ handler: async ({ type = 'all' } = {}) => {
212
+ const store = state.devtoolsStore;
213
+ if (!store)
214
+ return toolResult({ cleared: type });
215
+ if (type === 'console')
216
+ store.clearConsole();
217
+ else if (type === 'network')
218
+ store.clearNetwork();
219
+ else
220
+ store.clearAll();
221
+ return toolResult({ cleared: type });
222
+ },
223
+ },
224
+ {
225
+ definition: {
226
+ name: 'electron_get_devtools_stats',
227
+ description: 'Get counts of captured console logs and network requests.',
228
+ inputSchema: {
229
+ type: 'object',
230
+ properties: {},
231
+ },
232
+ },
233
+ handler: async () => {
234
+ const store = state.devtoolsStore;
235
+ if (!store)
236
+ return toolResult({ console: 0, network: 0, capturing: false });
237
+ return toolResult({
238
+ console: store.console.length,
239
+ network: store.network.size,
240
+ capturing: true,
241
+ limits: {
242
+ maxConsole: MAX_CONSOLE_ENTRIES,
243
+ maxNetwork: MAX_NETWORK_ENTRIES,
244
+ },
245
+ });
246
+ },
247
+ },
248
+ ];
249
+ }
@@ -1,3 +1,4 @@
1
+ import { createDevtoolsTools } from './devtools.js';
1
2
  import { createDomQueryTools } from './dom-query.js';
2
3
  import { createInteractionTools } from './interaction.js';
3
4
  import { createLifecycleTools } from './lifecycle.js';
@@ -13,6 +14,7 @@ export function getCdpTools(bridge, appConfig, screenshotConfig) {
13
14
  state: {
14
15
  screenshotCounter: 0,
15
16
  electronProcess: null,
17
+ devtoolsStore: null,
16
18
  },
17
19
  };
18
20
  return [
@@ -22,5 +24,6 @@ export function getCdpTools(bridge, appConfig, screenshotConfig) {
22
24
  ...createStateTools(ctx),
23
25
  ...createNavigationTools(ctx),
24
26
  ...createVisualTools(ctx),
27
+ ...createDevtoolsTools(ctx),
25
28
  ];
26
29
  }
@@ -1,6 +1,15 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { join, resolve } from 'node:path';
3
+ import { DevtoolsStore } from './devtools.js';
3
4
  import { toolResult } from './helpers.js';
5
+ function attachDevtoolsStore(bridge, state) {
6
+ if (state.devtoolsStore) {
7
+ state.devtoolsStore.detach();
8
+ }
9
+ const store = new DevtoolsStore();
10
+ store.attach(bridge.getRawClient());
11
+ state.devtoolsStore = store;
12
+ }
4
13
  export function createLifecycleTools(ctx) {
5
14
  const { bridge, appConfig, state } = ctx;
6
15
  return [
@@ -45,6 +54,7 @@ export function createLifecycleTools(ctx) {
45
54
  'Check that the app path is correct and Electron is installed.');
46
55
  }
47
56
  await bridge.connect();
57
+ attachDevtoolsStore(bridge, state);
48
58
  return toolResult({
49
59
  pid: child.pid,
50
60
  debugPort,
@@ -71,6 +81,7 @@ export function createLifecycleTools(ctx) {
71
81
  const targetPort = port || appConfig.debugPort || 9229;
72
82
  bridge.setPort(targetPort);
73
83
  await bridge.connect();
84
+ attachDevtoolsStore(bridge, state);
74
85
  return toolResult({ connected: true, port: targetPort });
75
86
  },
76
87
  },
@@ -1,6 +1,7 @@
1
1
  import type { ChildProcess } from 'node:child_process';
2
2
  import type { AppConfig } from '../index.js';
3
3
  import type { CdpBridge } from '../server/cdp-bridge.js';
4
+ import type { DevtoolsStore } from './devtools.js';
4
5
  export interface CdpToolDefinition {
5
6
  name: string;
6
7
  description: string;
@@ -18,5 +19,6 @@ export interface ToolContext {
18
19
  state: {
19
20
  screenshotCounter: number;
20
21
  electronProcess: ChildProcess | null;
22
+ devtoolsStore: DevtoolsStore | null;
21
23
  };
22
24
  }
package/dist/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const VERSION = '0.1.1';
2
+ const VERSION = '0.2.0';
3
3
  const command = process.argv[2];
4
4
  switch (command) {
5
5
  case 'serve':
package/dist/index.d.ts CHANGED
@@ -20,14 +20,28 @@ export interface ScreenshotConfig {
20
20
  dir?: string;
21
21
  format?: 'png' | 'jpeg';
22
22
  }
23
+ export interface CustomTool {
24
+ name: string;
25
+ description: string;
26
+ inputSchema: object;
27
+ handler: (args: Record<string, unknown>) => Promise<{
28
+ content: Array<{
29
+ type: string;
30
+ text: string;
31
+ }>;
32
+ isError?: boolean;
33
+ }>;
34
+ }
23
35
  export interface ElectronMcpConfig {
24
36
  app: AppConfig;
25
37
  tools: Record<string, ToolConfig>;
26
38
  resources?: Record<string, ResourceConfig>;
27
39
  cdpTools?: boolean | string[];
28
40
  screenshots?: ScreenshotConfig;
41
+ customTools?: CustomTool[];
29
42
  }
30
43
  export declare function defineConfig(config: ElectronMcpConfig): ElectronMcpConfig;
31
44
  export { CdpBridge } from './server/cdp-bridge.js';
32
45
  export { getCdpTools } from './cdp-tools/index.js';
46
+ export { startServer } from './server/mcp-server.js';
33
47
  export type { CdpTool, CdpToolDefinition } from './cdp-tools/types.js';
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ export function defineConfig(config) {
3
3
  }
4
4
  export { CdpBridge } from './server/cdp-bridge.js';
5
5
  export { getCdpTools } from './cdp-tools/index.js';
6
+ export { startServer } from './server/mcp-server.js';
@@ -5,7 +5,7 @@ import { CdpBridge } from './cdp-bridge.js';
5
5
  import { buildTools } from './tool-builder.js';
6
6
  import { buildResources } from './resource-builder.js';
7
7
  import { getCdpTools } from '../cdp-tools/index.js';
8
- function registerToolHandlers(server, bridge, ipcTools, cdpToolDefs) {
8
+ function registerToolHandlers(server, bridge, ipcTools, cdpToolDefs, customTools) {
9
9
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
10
10
  tools: [
11
11
  ...ipcTools.map(t => ({
@@ -14,6 +14,11 @@ function registerToolHandlers(server, bridge, ipcTools, cdpToolDefs) {
14
14
  inputSchema: t.inputSchema,
15
15
  })),
16
16
  ...cdpToolDefs.map(t => t.definition),
17
+ ...customTools.map(t => ({
18
+ name: t.name,
19
+ description: t.description,
20
+ inputSchema: t.inputSchema,
21
+ })),
17
22
  ],
18
23
  }));
19
24
  const ipcHandlerMap = new Map();
@@ -24,6 +29,10 @@ function registerToolHandlers(server, bridge, ipcTools, cdpToolDefs) {
24
29
  for (const tool of cdpToolDefs) {
25
30
  cdpHandlerMap.set(tool.definition.name, tool.handler);
26
31
  }
32
+ const customHandlerMap = new Map();
33
+ for (const tool of customTools) {
34
+ customHandlerMap.set(tool.name, tool);
35
+ }
27
36
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
28
37
  const { name, arguments: args } = request.params;
29
38
  const ipcTool = ipcHandlerMap.get(name);
@@ -59,6 +68,18 @@ function registerToolHandlers(server, bridge, ipcTools, cdpToolDefs) {
59
68
  };
60
69
  }
61
70
  }
71
+ const customTool = customHandlerMap.get(name);
72
+ if (customTool) {
73
+ try {
74
+ return await customTool.handler((args || {}));
75
+ }
76
+ catch (err) {
77
+ return {
78
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
79
+ isError: true,
80
+ };
81
+ }
82
+ }
62
83
  return {
63
84
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
64
85
  isError: true,
@@ -112,7 +133,8 @@ export async function startServer(config) {
112
133
  tools: {},
113
134
  ...(resources.length > 0 ? { resources: {} } : {}),
114
135
  } });
115
- registerToolHandlers(server, bridge, ipcTools, cdpToolDefs);
136
+ const customTools = config.customTools || [];
137
+ registerToolHandlers(server, bridge, ipcTools, cdpToolDefs, customTools);
116
138
  registerResourceHandlers(server, bridge, resources);
117
139
  const cleanup = async () => {
118
140
  await bridge.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electron-dev-bridge",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Expose Electron IPC handlers as MCP tools for Claude Code, with 22 built-in CDP tools for DOM automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",