electron-verify-mcp 0.0.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +172 -0
  5. package/dist/core/config.d.ts +74 -0
  6. package/dist/core/config.js +55 -0
  7. package/dist/core/index.d.ts +16 -0
  8. package/dist/core/index.js +15 -0
  9. package/dist/core/interact.d.ts +40 -0
  10. package/dist/core/interact.js +98 -0
  11. package/dist/core/launch.d.ts +29 -0
  12. package/dist/core/launch.js +98 -0
  13. package/dist/core/readiness.d.ts +23 -0
  14. package/dist/core/readiness.js +109 -0
  15. package/dist/core/screenshot.d.ts +30 -0
  16. package/dist/core/screenshot.js +176 -0
  17. package/dist/core/session.d.ts +54 -0
  18. package/dist/core/session.js +148 -0
  19. package/dist/core/signals/blankScreen.d.ts +30 -0
  20. package/dist/core/signals/blankScreen.js +106 -0
  21. package/dist/core/signals/collect.d.ts +21 -0
  22. package/dist/core/signals/collect.js +38 -0
  23. package/dist/core/signals/console.d.ts +62 -0
  24. package/dist/core/signals/console.js +335 -0
  25. package/dist/core/signals/layout.d.ts +14 -0
  26. package/dist/core/signals/layout.js +32 -0
  27. package/dist/core/signals/mainProcess.d.ts +17 -0
  28. package/dist/core/signals/mainProcess.js +61 -0
  29. package/dist/core/signals/resources.d.ts +24 -0
  30. package/dist/core/signals/resources.js +91 -0
  31. package/dist/core/signals/staleBuild.d.ts +18 -0
  32. package/dist/core/signals/staleBuild.js +109 -0
  33. package/dist/core/types.d.ts +80 -0
  34. package/dist/core/types.js +5 -0
  35. package/dist/core/verdict.d.ts +23 -0
  36. package/dist/core/verdict.js +87 -0
  37. package/dist/core/verify.d.ts +52 -0
  38. package/dist/core/verify.js +182 -0
  39. package/dist/core/windowCapture.d.ts +27 -0
  40. package/dist/core/windowCapture.js +236 -0
  41. package/dist/core/windows.d.ts +29 -0
  42. package/dist/core/windows.js +94 -0
  43. package/dist/mcp.d.ts +2 -0
  44. package/dist/mcp.js +578 -0
  45. package/package.json +74 -0
package/dist/mcp.js ADDED
@@ -0,0 +1,578 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Thin stdio MCP server over core.
4
+ * Holds ONE AppSession; all real logic lives in src/core/*.
5
+ * Logging → stderr only (stdout is the JSON-RPC channel).
6
+ */
7
+ import { createRequire } from "node:module";
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { z } from "zod";
11
+ import { PNG } from "pngjs";
12
+ import { AppSession } from "./core/session.js";
13
+ import { verify, scaleHolesToPhysical } from "./core/verify.js";
14
+ import { waitUntilReady } from "./core/readiness.js";
15
+ import { annotateHoles, captureRawScreenshot, downscaleIfNeeded, } from "./core/screenshot.js";
16
+ import { captureOsWindow, captureWindow } from "./core/windowCapture.js";
17
+ import { detectHoles } from "./core/windows.js";
18
+ import { clickEl, typeEl, waitForSel, evalJs } from "./core/interact.js";
19
+ const require = createRequire(import.meta.url);
20
+ const pkg = require("../package.json");
21
+ const SERVER_VERSION = pkg.version ?? "0.0.0";
22
+ // ─── Session state ───────────────────────────────────────────────────────────
23
+ let session = null;
24
+ /** Serialize tool handlers so concurrent calls never race one Electron app. */
25
+ let lock = Promise.resolve();
26
+ function withLock(fn) {
27
+ const run = lock.then(fn, fn);
28
+ lock = run.then(() => undefined, () => undefined);
29
+ return run;
30
+ }
31
+ const URL_MAX = 120;
32
+ /** Default readiness budget for app_screenshot (verify keeps the longer gate). */
33
+ const SCREENSHOT_READINESS_MS = 1_500;
34
+ function truncateUrl(url, max = URL_MAX) {
35
+ if (url.length <= max)
36
+ return url;
37
+ return url.slice(0, max - 1) + "…";
38
+ }
39
+ function isDeadSessionError(err) {
40
+ const msg = err instanceof Error ? err.message : String(err);
41
+ return /target closed|has been closed|browser has been closed|session closed|crashed|Target page, context or browser has been closed/i.test(msg);
42
+ }
43
+ function noSessionResult() {
44
+ const message = "no active session — call app_launch";
45
+ return {
46
+ content: [{ type: "text", text: message }],
47
+ structuredContent: { error: "no_session", message },
48
+ isError: true,
49
+ };
50
+ }
51
+ function deadSessionResult() {
52
+ session = null;
53
+ const message = "session died — relaunch with app_launch";
54
+ return {
55
+ content: [{ type: "text", text: message }],
56
+ structuredContent: { error: "session_dead", message },
57
+ isError: true,
58
+ };
59
+ }
60
+ function toolError(err) {
61
+ if (isDeadSessionError(err))
62
+ return deadSessionResult();
63
+ const message = err instanceof Error ? err.message : String(err);
64
+ console.error(`[electron-verify-mcp] tool error: ${message}`);
65
+ return {
66
+ content: [{ type: "text", text: message }],
67
+ structuredContent: { error: "tool_error", message },
68
+ isError: true,
69
+ };
70
+ }
71
+ async function requireSession() {
72
+ return session;
73
+ }
74
+ async function listWindows(s) {
75
+ const pages = s.app.windows();
76
+ return Promise.all(pages.map(async (p) => {
77
+ let title = "";
78
+ try {
79
+ title = await p.title();
80
+ }
81
+ catch {
82
+ /* closed */
83
+ }
84
+ return { title, url: truncateUrl(p.url()) };
85
+ }));
86
+ }
87
+ function pidOf(s) {
88
+ try {
89
+ return s.app.process()?.pid ?? null;
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ async function closeSession() {
96
+ if (!session)
97
+ return;
98
+ const s = session;
99
+ session = null;
100
+ try {
101
+ await s.close();
102
+ }
103
+ catch (err) {
104
+ console.error(`[electron-verify-mcp] close error: ${err instanceof Error ? err.message : String(err)}`);
105
+ }
106
+ }
107
+ async function captureForScreenshot(s, opts) {
108
+ const page = s.page();
109
+ const pageUrl = page.url();
110
+ const maxWidth = opts.maxWidth ?? 1280;
111
+ const want = opts.captureMode ?? "window";
112
+ const readinessMs = opts.readinessTimeoutMs ?? SCREENSHOT_READINESS_MS;
113
+ await waitUntilReady(page, { timeoutMs: readinessMs });
114
+ // Content capture (always for annotation / both mode).
115
+ const dipHoles = await detectHoles(s.app, pageUrl);
116
+ let dpr = 1;
117
+ try {
118
+ dpr = (await page.evaluate(() => window.devicePixelRatio || 1));
119
+ if (typeof dpr !== "number" || dpr <= 0)
120
+ dpr = 1;
121
+ }
122
+ catch {
123
+ dpr = 1;
124
+ }
125
+ const holes = scaleHolesToPhysical(dipHoles, dpr);
126
+ const rawPng = await captureRawScreenshot(page);
127
+ const rawDecoded = PNG.sync.read(rawPng);
128
+ // Hole annotation only for content / fallback (not composited window mode).
129
+ const annotated = holes.length > 0 ? annotateHoles(rawDecoded, holes) : rawDecoded;
130
+ let contentPng = PNG.sync.write(annotated);
131
+ contentPng = downscaleIfNeeded(contentPng, maxWidth);
132
+ if (want === "content") {
133
+ return { primary: contentPng, content: contentPng, captureMode: "content" };
134
+ }
135
+ if (want === "os-window") {
136
+ const os = await captureOsWindow(s.app, pageUrl);
137
+ let osPng = os.png;
138
+ if (osPng)
139
+ osPng = downscaleIfNeeded(osPng, maxWidth);
140
+ if (osPng) {
141
+ return { primary: osPng, window: osPng, captureMode: "os-window" };
142
+ }
143
+ const win = await captureWindow(s.app, pageUrl);
144
+ let windowPng = win.png;
145
+ if (windowPng)
146
+ windowPng = downscaleIfNeeded(windowPng, maxWidth);
147
+ if (windowPng) {
148
+ return {
149
+ primary: windowPng,
150
+ window: windowPng,
151
+ captureMode: "os-window-fallback:window",
152
+ captureNote: os.note ?? "os-window capture unavailable; used window",
153
+ };
154
+ }
155
+ return {
156
+ primary: contentPng,
157
+ content: contentPng,
158
+ captureMode: "content-fallback",
159
+ captureNote: [os.note, win.note].filter(Boolean).join("; ") ||
160
+ "os-window and window capture unavailable",
161
+ };
162
+ }
163
+ const win = await captureWindow(s.app, pageUrl);
164
+ let windowPng = win.png;
165
+ if (windowPng)
166
+ windowPng = downscaleIfNeeded(windowPng, maxWidth);
167
+ if (want === "both") {
168
+ if (windowPng) {
169
+ return {
170
+ primary: windowPng,
171
+ content: contentPng,
172
+ window: windowPng,
173
+ captureMode: "window",
174
+ };
175
+ }
176
+ return {
177
+ primary: contentPng,
178
+ content: contentPng,
179
+ captureMode: "content-fallback",
180
+ captureNote: win.note,
181
+ };
182
+ }
183
+ // window (default)
184
+ if (windowPng) {
185
+ return { primary: windowPng, window: windowPng, captureMode: "window" };
186
+ }
187
+ return {
188
+ primary: contentPng,
189
+ content: contentPng,
190
+ captureMode: "content-fallback",
191
+ captureNote: win.note,
192
+ };
193
+ }
194
+ function verdictStructured(result) {
195
+ return {
196
+ verdict: result.verdict,
197
+ hardFails: result.hardFails,
198
+ warns: result.warns,
199
+ signals: result.signals,
200
+ ...(result.expect !== undefined ? { expect: result.expect } : {}),
201
+ windowStrategy: result.windowStrategy,
202
+ windowWarning: result.windowWarning,
203
+ holes: result.holes,
204
+ captureMode: result.captureMode,
205
+ ...(result.captureNote ? { captureNote: result.captureNote } : {}),
206
+ };
207
+ }
208
+ // ─── MCP server ──────────────────────────────────────────────────────────────
209
+ const server = new McpServer({
210
+ name: "electron-verify-mcp",
211
+ version: SERVER_VERSION,
212
+ });
213
+ const captureModeSchema = z
214
+ .enum(["content", "window", "os-window", "both"])
215
+ .optional()
216
+ .describe('Screenshot mode: "window" (default, composited Electron window — menu bar + content, no OS caption), "os-window" (full OS frame including min/max/close caption bar), "content" (viewport only), "both" (window + content images)');
217
+ server.registerTool("app_launch", {
218
+ title: "Launch Electron app",
219
+ description: "Launch an Electron app under a single managed session. Closes any prior session first. " +
220
+ "Returns window list (URLs truncated) and process pid. Call app_close when done.",
221
+ inputSchema: {
222
+ appPath: z.string().describe("Path to the app main entry (e.g. dist/main/index.js)"),
223
+ cwd: z.string().optional().describe("App working directory (defaults to dirname of appPath)"),
224
+ args: z.array(z.string()).optional().describe("Extra args after the main entry"),
225
+ env: z.record(z.string()).optional().describe("Extra environment variables"),
226
+ buildCommand: z
227
+ .string()
228
+ .optional()
229
+ .describe("Shell command run in cwd before launch (avoid stale dist/)"),
230
+ windowUrlMatch: z
231
+ .string()
232
+ .optional()
233
+ .describe("Force target window URL substring"),
234
+ ignoreConsolePatterns: z
235
+ .array(z.string())
236
+ .optional()
237
+ .describe("Extra console ignore regexes"),
238
+ criticalSelectors: z
239
+ .array(z.string())
240
+ .optional()
241
+ .describe("Selectors that must be present and non-zero size"),
242
+ srcGlobs: z
243
+ .array(z.string())
244
+ .optional()
245
+ .describe("Source globs for stale-build detection"),
246
+ strictConsole: z
247
+ .boolean()
248
+ .optional()
249
+ .describe("When true, bare console.error is hard-FAIL (default: WARN)"),
250
+ },
251
+ }, async (args) => withLock(async () => {
252
+ try {
253
+ await closeSession();
254
+ session = await AppSession.launch({
255
+ appPath: args.appPath,
256
+ cwd: args.cwd,
257
+ args: args.args,
258
+ env: args.env,
259
+ buildCommand: args.buildCommand,
260
+ windowUrlMatch: args.windowUrlMatch,
261
+ ignoreConsolePatterns: args.ignoreConsolePatterns,
262
+ criticalSelectors: args.criticalSelectors,
263
+ srcGlobs: args.srcGlobs,
264
+ strictConsole: args.strictConsole,
265
+ });
266
+ try {
267
+ const { stubAllDialogs } = await import("electron-playwright-helpers");
268
+ await stubAllDialogs(session.app);
269
+ }
270
+ catch (err) {
271
+ console.error(`[electron-verify-mcp] dialog stub skipped: ${err instanceof Error ? err.message : String(err)}`);
272
+ }
273
+ const windows = await listWindows(session);
274
+ const pid = pidOf(session);
275
+ const structuredContent = { windows, pid };
276
+ const text = `launched pid=${pid ?? "?"} windows=${windows.length} ${windows
277
+ .map((w) => `"${w.title}" ${w.url}`)
278
+ .join(" | ")}`;
279
+ return {
280
+ content: [{ type: "text", text }],
281
+ structuredContent,
282
+ };
283
+ }
284
+ catch (err) {
285
+ session = null;
286
+ return toolError(err);
287
+ }
288
+ }));
289
+ server.registerTool("app_screenshot", {
290
+ title: "Screenshot app window",
291
+ description: "Screenshot of the active session window. Default captureMode=window (composited Electron chrome — " +
292
+ "menu bar + content, no OS caption). Use os-window for the real min/max/close caption bar. " +
293
+ "Falls back window→content (or os-window→window→content). BrowserView hole annotations only in " +
294
+ "content/fallback mode. Uses a short readiness budget (~1.5s). PNG returned as image content block(s).",
295
+ inputSchema: {
296
+ maxWidth: z
297
+ .number()
298
+ .optional()
299
+ .describe("Max PNG width (default 1280); larger captures are downscaled"),
300
+ captureMode: captureModeSchema,
301
+ readinessTimeoutMs: z
302
+ .number()
303
+ .optional()
304
+ .describe("Readiness budget in ms (default 1500 for screenshots)"),
305
+ },
306
+ }, async (args) => withLock(async () => {
307
+ const s = await requireSession();
308
+ if (!s)
309
+ return noSessionResult();
310
+ try {
311
+ const cap = await captureForScreenshot(s, {
312
+ maxWidth: args.maxWidth,
313
+ captureMode: args.captureMode,
314
+ readinessTimeoutMs: args.readinessTimeoutMs,
315
+ });
316
+ const content = [
317
+ {
318
+ type: "text",
319
+ text: `screenshot ${cap.primary.length} bytes captureMode=${cap.captureMode}${cap.captureNote ? ` (${cap.captureNote})` : ""}`,
320
+ },
321
+ {
322
+ type: "image",
323
+ data: cap.primary.toString("base64"),
324
+ mimeType: "image/png",
325
+ },
326
+ ];
327
+ if (args.captureMode === "both" && cap.content && cap.window) {
328
+ content.push({
329
+ type: "image",
330
+ data: cap.content.toString("base64"),
331
+ mimeType: "image/png",
332
+ });
333
+ }
334
+ return {
335
+ content,
336
+ structuredContent: {
337
+ bytes: cap.primary.length,
338
+ captureMode: cap.captureMode,
339
+ ...(cap.captureNote ? { captureNote: cap.captureNote } : {}),
340
+ },
341
+ };
342
+ }
343
+ catch (err) {
344
+ return toolError(err);
345
+ }
346
+ }));
347
+ server.registerTool("app_click", {
348
+ title: "Click element",
349
+ description: "Click a CSS selector in the active session's page.",
350
+ inputSchema: {
351
+ selector: z.string().describe("CSS selector to click"),
352
+ },
353
+ }, async (args) => withLock(async () => {
354
+ const s = await requireSession();
355
+ if (!s)
356
+ return noSessionResult();
357
+ try {
358
+ await clickEl(s.page(), args.selector);
359
+ s.noteInteraction();
360
+ return {
361
+ content: [{ type: "text", text: `clicked ${args.selector}` }],
362
+ structuredContent: { ok: true, selector: args.selector },
363
+ };
364
+ }
365
+ catch (err) {
366
+ return toolError(err);
367
+ }
368
+ }));
369
+ server.registerTool("app_type", {
370
+ title: "Type into element",
371
+ description: "Fill text into an input matching the CSS selector.",
372
+ inputSchema: {
373
+ selector: z.string().describe("CSS selector of the input"),
374
+ text: z.string().describe("Text to type"),
375
+ },
376
+ }, async (args) => withLock(async () => {
377
+ const s = await requireSession();
378
+ if (!s)
379
+ return noSessionResult();
380
+ try {
381
+ await typeEl(s.page(), args.selector, args.text);
382
+ s.noteInteraction();
383
+ return {
384
+ content: [
385
+ {
386
+ type: "text",
387
+ text: `typed into ${args.selector} (${args.text.length} chars)`,
388
+ },
389
+ ],
390
+ structuredContent: { ok: true, selector: args.selector },
391
+ };
392
+ }
393
+ catch (err) {
394
+ return toolError(err);
395
+ }
396
+ }));
397
+ server.registerTool("app_wait_for", {
398
+ title: "Wait for selector or text",
399
+ description: "Wait until a CSS selector or visible text appears. " +
400
+ "Heuristic: target is a CSS selector only if it starts with . # [ or contains " +
401
+ "selector chars (>, :, [, etc.); otherwise plain text is matched via getByText. " +
402
+ 'Pass by="selector" or by="text" to force the mode. Single-word labels like "Submit" are text.',
403
+ inputSchema: {
404
+ target: z.string().describe("CSS selector or visible text to wait for"),
405
+ timeoutMs: z.number().optional().describe("Timeout in ms (default 10000)"),
406
+ by: z
407
+ .enum(["selector", "text"])
408
+ .optional()
409
+ .describe('Force interpretation: "selector" (CSS) or "text" (getByText)'),
410
+ },
411
+ }, async (args) => withLock(async () => {
412
+ const s = await requireSession();
413
+ if (!s)
414
+ return noSessionResult();
415
+ try {
416
+ await waitForSel(s.page(), args.target, {
417
+ timeoutMs: args.timeoutMs,
418
+ by: args.by,
419
+ });
420
+ return {
421
+ content: [{ type: "text", text: `waited for ${args.target}` }],
422
+ structuredContent: { ok: true, target: args.target },
423
+ };
424
+ }
425
+ catch (err) {
426
+ return toolError(err);
427
+ }
428
+ }));
429
+ server.registerTool("app_evaluate", {
430
+ title: "Evaluate JS in page",
431
+ description: "Evaluate a JavaScript expression in the renderer via CDP Runtime.evaluate (CSP-safe). " +
432
+ "Pass an expression (e.g. document.title). Set asStatement=true for a statement body with return.",
433
+ inputSchema: {
434
+ expression: z
435
+ .string()
436
+ .describe("JS expression, e.g. document.title or document.querySelector('#ok')?.textContent"),
437
+ asStatement: z
438
+ .boolean()
439
+ .optional()
440
+ .describe("If true, treat expression as a statement body (may include return)"),
441
+ },
442
+ }, async (args) => withLock(async () => {
443
+ const s = await requireSession();
444
+ if (!s)
445
+ return noSessionResult();
446
+ try {
447
+ const result = await evalJs(s.page(), args.expression, {
448
+ asStatement: args.asStatement,
449
+ });
450
+ // Read-only evaluate does not count as interaction; mutating ones should use click/type.
451
+ // Still note interaction when asStatement (likely mutates) — conservative.
452
+ if (args.asStatement)
453
+ s.noteInteraction();
454
+ const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
455
+ return {
456
+ content: [{ type: "text", text: text ?? "undefined" }],
457
+ structuredContent: { result },
458
+ };
459
+ }
460
+ catch (err) {
461
+ return toolError(err);
462
+ }
463
+ }));
464
+ server.registerTool("app_verify", {
465
+ title: "Verify app UI",
466
+ description: "Run a high-precision signal pass + capture a screenshot. " +
467
+ "Returns image (PNG) + structuredContent verdict (PASS|WARN|FAIL). " +
468
+ "freshLoad: when omitted, defaults TRUE if no click/type since last verify " +
469
+ "(so an unchanged broken app is re-checked); FALSE after interactions. " +
470
+ "Signal analysis always uses content capture; human image defaults to window (composited). " +
471
+ "Use captureMode=os-window for the full OS caption bar (min/max/close). " +
472
+ "console.error → WARN by default; pageerror/crash → FAIL. Set strictConsole to promote console.error.",
473
+ inputSchema: {
474
+ expect: z.string().optional().describe("Intent string echoed into the verdict"),
475
+ freshLoad: z
476
+ .boolean()
477
+ .optional()
478
+ .describe("Reload before verify. Omitted: true if no interaction since last verify, else false"),
479
+ criticalSelectors: z
480
+ .array(z.string())
481
+ .optional()
482
+ .describe("Override session criticalSelectors for this pass"),
483
+ captureMode: captureModeSchema,
484
+ strictConsole: z
485
+ .boolean()
486
+ .optional()
487
+ .describe("When true, bare console.error is hard-FAIL for this pass"),
488
+ },
489
+ }, async (args) => withLock(async () => {
490
+ const s = await requireSession();
491
+ if (!s)
492
+ return noSessionResult();
493
+ try {
494
+ const result = await verify(s, {
495
+ expect: args.expect,
496
+ // Undefined → core defaults based on interactionSinceVerify.
497
+ freshLoad: args.freshLoad,
498
+ criticalSelectors: args.criticalSelectors,
499
+ captureMode: args.captureMode ?? "window",
500
+ strictConsole: args.strictConsole,
501
+ });
502
+ const structuredContent = verdictStructured(result);
503
+ const summary = [
504
+ result.verdict,
505
+ result.hardFails.length ? `hardFails=${result.hardFails.join(",")}` : "",
506
+ result.warns.length ? `warns=${result.warns.join(",")}` : "",
507
+ `capture=${result.captureMode}`,
508
+ ]
509
+ .filter(Boolean)
510
+ .join(" ");
511
+ const content = [
512
+ { type: "text", text: summary },
513
+ {
514
+ type: "image",
515
+ data: result.screenshot.toString("base64"),
516
+ mimeType: "image/png",
517
+ },
518
+ ];
519
+ if (args.captureMode === "both" &&
520
+ result.contentScreenshot &&
521
+ result.windowScreenshot) {
522
+ content.push({
523
+ type: "image",
524
+ data: result.contentScreenshot.toString("base64"),
525
+ mimeType: "image/png",
526
+ });
527
+ }
528
+ return { content, structuredContent };
529
+ }
530
+ catch (err) {
531
+ return toolError(err);
532
+ }
533
+ }));
534
+ server.registerTool("app_close", {
535
+ title: "Close app session",
536
+ description: "Dispose the active Electron session (graceful close with 5s timeout + force-kill fallback). No arguments.",
537
+ }, async () => withLock(async () => {
538
+ if (!session) {
539
+ return {
540
+ content: [{ type: "text", text: "no session to close" }],
541
+ structuredContent: { closed: false, reason: "no_session" },
542
+ };
543
+ }
544
+ await closeSession();
545
+ return {
546
+ content: [{ type: "text", text: "session closed" }],
547
+ structuredContent: { closed: true },
548
+ };
549
+ }));
550
+ // ─── Lifecycle hygiene ───────────────────────────────────────────────────────
551
+ async function shutdown(reason) {
552
+ console.error(`[electron-verify-mcp] shutting down (${reason})`);
553
+ await closeSession();
554
+ }
555
+ process.on("SIGINT", () => {
556
+ void shutdown("SIGINT").finally(() => process.exit(0));
557
+ });
558
+ process.on("SIGTERM", () => {
559
+ void shutdown("SIGTERM").finally(() => process.exit(0));
560
+ });
561
+ process.stdin.on("end", () => {
562
+ void shutdown("stdin end").finally(() => process.exit(0));
563
+ });
564
+ process.stdin.on("close", () => {
565
+ void shutdown("stdin close").finally(() => process.exit(0));
566
+ });
567
+ async function main() {
568
+ const transport = new StdioServerTransport();
569
+ transport.onclose = () => {
570
+ void shutdown("transport close");
571
+ };
572
+ await server.connect(transport);
573
+ console.error(`[electron-verify-mcp] stdio server ready v${SERVER_VERSION}`);
574
+ }
575
+ main().catch((err) => {
576
+ console.error(err instanceof Error ? (err.stack ?? err.message) : String(err));
577
+ process.exit(1);
578
+ });
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "electron-verify-mcp",
3
+ "version": "0.0.1",
4
+ "description": "Give your AI coding agent eyes for Electron apps: launch, drive, screenshot, and get a high-precision pass/fail verdict so it can verify its own UI in a loop.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "dpraj007",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/dpraj007/electron-verify-mcp.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/dpraj007/electron-verify-mcp/issues"
14
+ },
15
+ "homepage": "https://github.com/dpraj007/electron-verify-mcp#readme",
16
+ "keywords": [
17
+ "electron",
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "screenshot",
21
+ "visual-testing",
22
+ "verification",
23
+ "playwright",
24
+ "ai-agent",
25
+ "claude"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "main": "./dist/core/index.js",
34
+ "types": "./dist/core/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/core/index.d.ts",
38
+ "import": "./dist/core/index.js"
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "bin": {
43
+ "electron-verify": "./dist/cli.js",
44
+ "electron-verify-mcp": "./dist/mcp.js"
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md",
49
+ "LICENSE"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsc",
53
+ "verify": "tsx src/cli.ts",
54
+ "mcp": "tsx src/mcp.ts",
55
+ "test": "node --import tsx --test test/**/*.test.ts",
56
+ "test:mcp": "node test/mcp-smoke.mjs",
57
+ "typecheck": "tsc --noEmit",
58
+ "prepublishOnly": "npm run build"
59
+ },
60
+ "dependencies": {
61
+ "@modelcontextprotocol/sdk": "^1.29.0",
62
+ "electron-playwright-helpers": "^1.7.1",
63
+ "playwright-core": "^1.51.0",
64
+ "pngjs": "^7.0.0",
65
+ "zod": "^3.24.0"
66
+ },
67
+ "devDependencies": {
68
+ "@types/node": "^22.0.0",
69
+ "@types/pngjs": "^6.0.5",
70
+ "electron": "^35.0.0",
71
+ "tsx": "^4.19.0",
72
+ "typescript": "^5.6.0"
73
+ }
74
+ }