@midscene/web 1.7.7 → 1.7.9

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 (41) hide show
  1. package/dist/es/bridge-mode/io-client.mjs +1 -1
  2. package/dist/es/bridge-mode/io-server.mjs +2 -2
  3. package/dist/es/bridge-mode/page-browser-side.mjs +1 -1
  4. package/dist/es/cli-options.mjs +99 -0
  5. package/dist/es/cli-options.mjs.map +1 -0
  6. package/dist/es/cli.mjs +11 -32
  7. package/dist/es/cli.mjs.map +1 -1
  8. package/dist/es/common/viewport.mjs +38 -0
  9. package/dist/es/common/viewport.mjs.map +1 -0
  10. package/dist/es/mcp-server.mjs +1 -1
  11. package/dist/es/mcp-tools-cdp.mjs +2 -4
  12. package/dist/es/mcp-tools-cdp.mjs.map +1 -1
  13. package/dist/es/mcp-tools-puppeteer.mjs +48 -24
  14. package/dist/es/mcp-tools-puppeteer.mjs.map +1 -1
  15. package/dist/es/mcp-tools.mjs +2 -4
  16. package/dist/es/mcp-tools.mjs.map +1 -1
  17. package/dist/es/puppeteer/agent-launcher.mjs +2 -14
  18. package/dist/es/puppeteer/agent-launcher.mjs.map +1 -1
  19. package/dist/lib/bridge-mode/io-client.js +1 -1
  20. package/dist/lib/bridge-mode/io-server.js +2 -2
  21. package/dist/lib/bridge-mode/page-browser-side.js +1 -1
  22. package/dist/lib/cli-options.js +133 -0
  23. package/dist/lib/cli-options.js.map +1 -0
  24. package/dist/lib/cli.js +11 -32
  25. package/dist/lib/cli.js.map +1 -1
  26. package/dist/lib/common/viewport.js +90 -0
  27. package/dist/lib/common/viewport.js.map +1 -0
  28. package/dist/lib/mcp-server.js +1 -1
  29. package/dist/lib/mcp-tools-cdp.js +2 -4
  30. package/dist/lib/mcp-tools-cdp.js.map +1 -1
  31. package/dist/lib/mcp-tools-puppeteer.js +55 -25
  32. package/dist/lib/mcp-tools-puppeteer.js.map +1 -1
  33. package/dist/lib/mcp-tools.js +2 -4
  34. package/dist/lib/mcp-tools.js.map +1 -1
  35. package/dist/lib/puppeteer/agent-launcher.js +4 -16
  36. package/dist/lib/puppeteer/agent-launcher.js.map +1 -1
  37. package/dist/types/cli-options.d.ts +8 -0
  38. package/dist/types/common/viewport.d.ts +17 -0
  39. package/dist/types/mcp-tools-puppeteer.d.ts +8 -0
  40. package/dist/types/puppeteer/agent-launcher.d.ts +1 -3
  41. package/package.json +4 -4
@@ -23,7 +23,7 @@ class BridgeClient {
23
23
  ]
24
24
  } : {},
25
25
  query: {
26
- version: "1.7.7"
26
+ version: "1.7.9"
27
27
  }
28
28
  });
29
29
  const timeout = setTimeout(()=>{
@@ -86,7 +86,7 @@ class BridgeServer {
86
86
  logMsg('one client connected');
87
87
  this.socket = socket;
88
88
  const clientVersion = socket.handshake.query.version;
89
- logMsg(`Bridge connected, cli-side version v1.7.7, browser-side version v${clientVersion}`);
89
+ logMsg(`Bridge connected, cli-side version v1.7.9, browser-side version v${clientVersion}`);
90
90
  socket.on(BridgeEvent.CallResponse, (params)=>{
91
91
  const id = params.id;
92
92
  const response = params.response;
@@ -110,7 +110,7 @@ class BridgeServer {
110
110
  setTimeout(()=>{
111
111
  this.onConnect?.();
112
112
  const payload = {
113
- version: "1.7.7"
113
+ version: "1.7.9"
114
114
  };
115
115
  socket.emit(BridgeEvent.Connected, payload);
116
116
  Promise.resolve().then(()=>{
@@ -65,7 +65,7 @@ class ExtensionBridgePageBrowserSide extends page {
65
65
  throw new Error('Connection denied by user');
66
66
  }
67
67
  }
68
- this.onLogMessage(`Bridge connected, cli-side version v${this.bridgeClient.serverVersion}, browser-side version v1.7.7`, 'log');
68
+ this.onLogMessage(`Bridge connected, cli-side version v${this.bridgeClient.serverVersion}, browser-side version v1.7.9`, 'log');
69
69
  }
70
70
  async connect() {
71
71
  return await this.setupBridgeClient();
@@ -0,0 +1,99 @@
1
+ import { CLIError } from "@midscene/shared/cli";
2
+ import { defaultPuppeteerWindowViewportSize, resolveViewportSize } from "./common/viewport.mjs";
3
+ const viewportWidthFlag = '--viewport-width';
4
+ const viewportHeightFlag = '--viewport-height';
5
+ function isLikelyCdpEndpoint(value) {
6
+ return !!value && /^(wss?):\/\//.test(value);
7
+ }
8
+ function parsePositiveIntegerOption(flag, rawValue) {
9
+ const value = Number(rawValue);
10
+ if (!Number.isInteger(value) || value <= 0) throw new CLIError(`Invalid value for "${flag}": expected a positive integer, got "${rawValue}".`);
11
+ return value;
12
+ }
13
+ function readRequiredOptionValue(args, index, flag) {
14
+ const currentArg = args[index];
15
+ const inlinePrefix = `${flag}=`;
16
+ if (currentArg.startsWith(inlinePrefix)) return {
17
+ value: currentArg.slice(inlinePrefix.length),
18
+ nextIndex: index
19
+ };
20
+ const nextArg = args[index + 1];
21
+ if (!nextArg || nextArg.startsWith('--')) throw new CLIError(`Option "${flag}" requires a value.`);
22
+ return {
23
+ value: nextArg,
24
+ nextIndex: index + 1
25
+ };
26
+ }
27
+ function readOptionalCdpEndpoint(args, index) {
28
+ const currentArg = args[index];
29
+ const inlinePrefix = '--cdp=';
30
+ if (currentArg.startsWith(inlinePrefix)) return {
31
+ value: currentArg.slice(inlinePrefix.length),
32
+ nextIndex: index
33
+ };
34
+ const nextArg = args[index + 1];
35
+ if (!isLikelyCdpEndpoint(nextArg)) return {
36
+ nextIndex: index
37
+ };
38
+ return {
39
+ value: nextArg,
40
+ nextIndex: index + 1
41
+ };
42
+ }
43
+ function parseWebCliOptions(rawArgs, env = process.env) {
44
+ const argv = [];
45
+ let isBridge = false;
46
+ let isCdp = false;
47
+ let viewportWidth;
48
+ let viewportHeight;
49
+ let cdpEndpoint;
50
+ for(let index = 0; index < rawArgs.length; index += 1){
51
+ const arg = rawArgs[index];
52
+ if ('--bridge' === arg) {
53
+ isBridge = true;
54
+ continue;
55
+ }
56
+ if ('--cdp' === arg || arg.startsWith('--cdp=')) {
57
+ isCdp = true;
58
+ const parsed = readOptionalCdpEndpoint(rawArgs, index);
59
+ cdpEndpoint = parsed.value ?? cdpEndpoint;
60
+ index = parsed.nextIndex;
61
+ continue;
62
+ }
63
+ if (arg === viewportWidthFlag || arg.startsWith(`${viewportWidthFlag}=`)) {
64
+ const parsed = readRequiredOptionValue(rawArgs, index, viewportWidthFlag);
65
+ viewportWidth = parsePositiveIntegerOption(viewportWidthFlag, parsed.value);
66
+ index = parsed.nextIndex;
67
+ continue;
68
+ }
69
+ if (arg === viewportHeightFlag || arg.startsWith(`${viewportHeightFlag}=`)) {
70
+ const parsed = readRequiredOptionValue(rawArgs, index, viewportHeightFlag);
71
+ viewportHeight = parsePositiveIntegerOption(viewportHeightFlag, parsed.value);
72
+ index = parsed.nextIndex;
73
+ continue;
74
+ }
75
+ argv.push(arg);
76
+ }
77
+ if (isBridge && isCdp) throw new CLIError('--bridge and --cdp are mutually exclusive. Please specify only one.');
78
+ const mode = isBridge ? 'bridge' : isCdp ? 'cdp' : 'puppeteer';
79
+ if ('puppeteer' !== mode) {
80
+ if (void 0 !== viewportWidth || void 0 !== viewportHeight) throw new CLIError('Viewport options are only supported in the default Puppeteer mode.');
81
+ }
82
+ if ('cdp' === mode) {
83
+ cdpEndpoint = cdpEndpoint ?? env.MIDSCENE_CDP_ENDPOINT;
84
+ if (!cdpEndpoint) throw new CLIError('CDP endpoint is required. Provide it as: --cdp <ws-endpoint> or set MIDSCENE_CDP_ENDPOINT environment variable.');
85
+ }
86
+ const hasViewportOverride = void 0 !== viewportWidth || void 0 !== viewportHeight;
87
+ return {
88
+ argv,
89
+ mode,
90
+ cdpEndpoint,
91
+ viewport: hasViewportOverride ? resolveViewportSize({
92
+ width: viewportWidth,
93
+ height: viewportHeight
94
+ }, defaultPuppeteerWindowViewportSize) : void 0
95
+ };
96
+ }
97
+ export { parseWebCliOptions };
98
+
99
+ //# sourceMappingURL=cli-options.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-options.mjs","sources":["../../src/cli-options.ts"],"sourcesContent":["import { CLIError } from '@midscene/shared/cli';\nimport {\n type ViewportSize,\n defaultPuppeteerWindowViewportSize,\n resolveViewportSize,\n} from './common/viewport';\n\nconst viewportWidthFlag = '--viewport-width';\nconst viewportHeightFlag = '--viewport-height';\n\nexport interface ParsedWebCliOptions {\n argv: string[];\n mode: 'bridge' | 'cdp' | 'puppeteer';\n cdpEndpoint?: string;\n viewport?: ViewportSize;\n}\n\nfunction isLikelyCdpEndpoint(value: string | undefined): boolean {\n return !!value && /^(wss?):\\/\\//.test(value);\n}\n\nfunction parsePositiveIntegerOption(flag: string, rawValue: string): number {\n const value = Number(rawValue);\n if (!Number.isInteger(value) || value <= 0) {\n throw new CLIError(\n `Invalid value for \"${flag}\": expected a positive integer, got \"${rawValue}\".`,\n );\n }\n\n return value;\n}\n\nfunction readRequiredOptionValue(\n args: string[],\n index: number,\n flag: string,\n): { value: string; nextIndex: number } {\n const currentArg = args[index];\n const inlinePrefix = `${flag}=`;\n if (currentArg.startsWith(inlinePrefix)) {\n return {\n value: currentArg.slice(inlinePrefix.length),\n nextIndex: index,\n };\n }\n\n const nextArg = args[index + 1];\n if (!nextArg || nextArg.startsWith('--')) {\n throw new CLIError(`Option \"${flag}\" requires a value.`);\n }\n\n return {\n value: nextArg,\n nextIndex: index + 1,\n };\n}\n\nfunction readOptionalCdpEndpoint(\n args: string[],\n index: number,\n): { value?: string; nextIndex: number } {\n const currentArg = args[index];\n const inlinePrefix = '--cdp=';\n if (currentArg.startsWith(inlinePrefix)) {\n return {\n value: currentArg.slice(inlinePrefix.length),\n nextIndex: index,\n };\n }\n\n const nextArg = args[index + 1];\n if (!isLikelyCdpEndpoint(nextArg)) {\n return { nextIndex: index };\n }\n\n return {\n value: nextArg,\n nextIndex: index + 1,\n };\n}\n\nexport function parseWebCliOptions(\n rawArgs: string[],\n env: NodeJS.ProcessEnv = process.env,\n): ParsedWebCliOptions {\n const argv: string[] = [];\n let isBridge = false;\n let isCdp = false;\n let viewportWidth: number | undefined;\n let viewportHeight: number | undefined;\n let cdpEndpoint: string | undefined;\n\n for (let index = 0; index < rawArgs.length; index += 1) {\n const arg = rawArgs[index];\n\n if (arg === '--bridge') {\n isBridge = true;\n continue;\n }\n\n if (arg === '--cdp' || arg.startsWith('--cdp=')) {\n isCdp = true;\n const parsed = readOptionalCdpEndpoint(rawArgs, index);\n cdpEndpoint = parsed.value ?? cdpEndpoint;\n index = parsed.nextIndex;\n continue;\n }\n\n if (arg === viewportWidthFlag || arg.startsWith(`${viewportWidthFlag}=`)) {\n const parsed = readRequiredOptionValue(rawArgs, index, viewportWidthFlag);\n viewportWidth = parsePositiveIntegerOption(\n viewportWidthFlag,\n parsed.value,\n );\n index = parsed.nextIndex;\n continue;\n }\n\n if (\n arg === viewportHeightFlag ||\n arg.startsWith(`${viewportHeightFlag}=`)\n ) {\n const parsed = readRequiredOptionValue(\n rawArgs,\n index,\n viewportHeightFlag,\n );\n viewportHeight = parsePositiveIntegerOption(\n viewportHeightFlag,\n parsed.value,\n );\n index = parsed.nextIndex;\n continue;\n }\n\n argv.push(arg);\n }\n\n if (isBridge && isCdp) {\n throw new CLIError(\n '--bridge and --cdp are mutually exclusive. Please specify only one.',\n );\n }\n\n const mode = isBridge ? 'bridge' : isCdp ? 'cdp' : 'puppeteer';\n\n if (mode !== 'puppeteer') {\n if (viewportWidth !== undefined || viewportHeight !== undefined) {\n throw new CLIError(\n 'Viewport options are only supported in the default Puppeteer mode.',\n );\n }\n }\n\n if (mode === 'cdp') {\n cdpEndpoint = cdpEndpoint ?? env.MIDSCENE_CDP_ENDPOINT;\n if (!cdpEndpoint) {\n throw new CLIError(\n 'CDP endpoint is required. Provide it as: --cdp <ws-endpoint> or set MIDSCENE_CDP_ENDPOINT environment variable.',\n );\n }\n }\n\n const hasViewportOverride =\n viewportWidth !== undefined || viewportHeight !== undefined;\n\n return {\n argv,\n mode,\n cdpEndpoint,\n viewport: hasViewportOverride\n ? resolveViewportSize(\n {\n width: viewportWidth,\n height: viewportHeight,\n },\n defaultPuppeteerWindowViewportSize,\n )\n : undefined,\n };\n}\n"],"names":["viewportWidthFlag","viewportHeightFlag","isLikelyCdpEndpoint","value","parsePositiveIntegerOption","flag","rawValue","Number","CLIError","readRequiredOptionValue","args","index","currentArg","inlinePrefix","nextArg","readOptionalCdpEndpoint","parseWebCliOptions","rawArgs","env","process","argv","isBridge","isCdp","viewportWidth","viewportHeight","cdpEndpoint","arg","parsed","mode","undefined","hasViewportOverride","resolveViewportSize","defaultPuppeteerWindowViewportSize"],"mappings":"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"}
package/dist/es/cli.mjs CHANGED
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { createReportCliCommands } from "@midscene/core";
4
4
  import { reportCLIError, runToolsCLI } from "@midscene/shared/cli";
5
5
  import dotenv from "dotenv";
6
+ import { parseWebCliOptions } from "./cli-options.mjs";
6
7
  import { WebMidsceneTools } from "./mcp-tools.mjs";
7
8
  import { WebCdpMidsceneTools } from "./mcp-tools-cdp.mjs";
8
9
  import { WebPuppeteerMidsceneTools } from "./mcp-tools-puppeteer.mjs";
@@ -10,38 +11,16 @@ const envFile = join(process.cwd(), '.env');
10
11
  if (existsSync(envFile)) dotenv.config({
11
12
  path: envFile
12
13
  });
13
- const isBridge = process.argv.includes('--bridge');
14
- const cdpIdx = process.argv.indexOf('--cdp');
15
- const isCdp = -1 !== cdpIdx;
16
- if (isBridge && isCdp) {
17
- console.error('--bridge and --cdp are mutually exclusive. Please specify only one.');
18
- process.exit(1);
19
- }
20
- let cdpEndpoint;
21
- if (isCdp) {
22
- const next = process.argv[cdpIdx + 1];
23
- if (next && !next.startsWith('-')) cdpEndpoint = next;
24
- if (!cdpEndpoint) cdpEndpoint = process.env.MIDSCENE_CDP_ENDPOINT;
25
- if (!cdpEndpoint) {
26
- console.error('CDP endpoint is required. Provide it as: --cdp <ws-endpoint> or set MIDSCENE_CDP_ENDPOINT environment variable.');
27
- process.exit(1);
28
- }
29
- }
30
- const bridgeIdx = process.argv.indexOf('--bridge');
31
- const cdpValueIdx = -1 !== cdpIdx && cdpIdx + 1 < process.argv.length && !process.argv[cdpIdx + 1].startsWith('-') ? cdpIdx + 1 : -1;
32
- const skipIndices = new Set([
33
- bridgeIdx,
34
- cdpIdx,
35
- cdpValueIdx
36
- ].filter((i)=>-1 !== i));
37
- const argv = process.argv.slice(2).filter((_, idx)=>!skipIndices.has(idx + 2));
38
- let tools;
39
- tools = isBridge ? new WebMidsceneTools() : isCdp ? new WebCdpMidsceneTools(cdpEndpoint) : new WebPuppeteerMidsceneTools();
40
- runToolsCLI(tools, 'midscene-web', {
41
- stripPrefix: 'web_',
42
- argv,
43
- version: "1.7.7",
44
- extraCommands: createReportCliCommands()
14
+ Promise.resolve().then(()=>{
15
+ const parsedOptions = parseWebCliOptions(process.argv.slice(2));
16
+ let tools;
17
+ tools = 'bridge' === parsedOptions.mode ? new WebMidsceneTools() : 'cdp' === parsedOptions.mode ? new WebCdpMidsceneTools(parsedOptions.cdpEndpoint) : new WebPuppeteerMidsceneTools(parsedOptions.viewport);
18
+ return runToolsCLI(tools, 'midscene-web', {
19
+ stripPrefix: 'web_',
20
+ argv: parsedOptions.argv,
21
+ version: "1.7.9",
22
+ extraCommands: createReportCliCommands()
23
+ });
45
24
  }).catch((e)=>{
46
25
  process.exit(reportCLIError(e));
47
26
  });
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","sources":["../../src/cli.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createReportCliCommands } from '@midscene/core';\nimport { reportCLIError, runToolsCLI } from '@midscene/shared/cli';\nimport dotenv from 'dotenv';\nimport { WebMidsceneTools } from './mcp-tools';\nimport { WebCdpMidsceneTools } from './mcp-tools-cdp';\nimport { WebPuppeteerMidsceneTools } from './mcp-tools-puppeteer';\n\n// Load .env early so MIDSCENE_CDP_ENDPOINT is available during arg parsing\nconst envFile = join(process.cwd(), '.env');\nif (existsSync(envFile)) {\n dotenv.config({ path: envFile });\n}\n\ndeclare const __VERSION__: string;\nconst isBridge = process.argv.includes('--bridge');\nconst cdpIdx = process.argv.indexOf('--cdp');\nconst isCdp = cdpIdx !== -1;\n\n// Fail-fast: mutually exclusive flags\nif (isBridge && isCdp) {\n console.error(\n '--bridge and --cdp are mutually exclusive. Please specify only one.',\n );\n process.exit(1);\n}\n\n// Parse --cdp endpoint value\nlet cdpEndpoint: string | undefined;\nif (isCdp) {\n const next = process.argv[cdpIdx + 1];\n if (next && !next.startsWith('-')) {\n cdpEndpoint = next;\n }\n if (!cdpEndpoint) {\n cdpEndpoint = process.env.MIDSCENE_CDP_ENDPOINT;\n }\n if (!cdpEndpoint) {\n console.error(\n 'CDP endpoint is required. Provide it as: --cdp <ws-endpoint> or set MIDSCENE_CDP_ENDPOINT environment variable.',\n );\n process.exit(1);\n }\n}\n\n// Filter out --bridge, --cdp, and cdp endpoint from argv using absolute indices\nconst bridgeIdx = process.argv.indexOf('--bridge');\nconst cdpValueIdx =\n cdpIdx !== -1 &&\n cdpIdx + 1 < process.argv.length &&\n !process.argv[cdpIdx + 1].startsWith('-')\n ? cdpIdx + 1\n : -1;\nconst skipIndices = new Set(\n [bridgeIdx, cdpIdx, cdpValueIdx].filter((i) => i !== -1),\n);\nconst argv = process.argv\n .slice(2)\n .filter((_, idx) => !skipIndices.has(idx + 2));\n\nlet tools: WebMidsceneTools | WebPuppeteerMidsceneTools | WebCdpMidsceneTools;\nif (isBridge) {\n tools = new WebMidsceneTools();\n} else if (isCdp) {\n tools = new WebCdpMidsceneTools(cdpEndpoint!);\n} else {\n tools = new WebPuppeteerMidsceneTools();\n}\n\nrunToolsCLI(tools, 'midscene-web', {\n stripPrefix: 'web_',\n argv,\n version: __VERSION__,\n extraCommands: createReportCliCommands(),\n}).catch((e) => {\n process.exit(reportCLIError(e));\n});\n"],"names":["envFile","join","process","existsSync","dotenv","isBridge","cdpIdx","isCdp","console","cdpEndpoint","next","bridgeIdx","cdpValueIdx","skipIndices","Set","i","argv","_","idx","tools","WebMidsceneTools","WebCdpMidsceneTools","WebPuppeteerMidsceneTools","runToolsCLI","__VERSION__","createReportCliCommands","e","reportCLIError"],"mappings":";;;;;;;;AAUA,MAAMA,UAAUC,KAAKC,QAAQ,GAAG,IAAI;AACpC,IAAIC,WAAWH,UACbI,OAAO,MAAM,CAAC;IAAE,MAAMJ;AAAQ;AAIhC,MAAMK,WAAWH,QAAQ,IAAI,CAAC,QAAQ,CAAC;AACvC,MAAMI,SAASJ,QAAQ,IAAI,CAAC,OAAO,CAAC;AACpC,MAAMK,QAAQD,AAAW,OAAXA;AAGd,IAAID,YAAYE,OAAO;IACrBC,QAAQ,KAAK,CACX;IAEFN,QAAQ,IAAI,CAAC;AACf;AAGA,IAAIO;AACJ,IAAIF,OAAO;IACT,MAAMG,OAAOR,QAAQ,IAAI,CAACI,SAAS,EAAE;IACrC,IAAII,QAAQ,CAACA,KAAK,UAAU,CAAC,MAC3BD,cAAcC;IAEhB,IAAI,CAACD,aACHA,cAAcP,QAAQ,GAAG,CAAC,qBAAqB;IAEjD,IAAI,CAACO,aAAa;QAChBD,QAAQ,KAAK,CACX;QAEFN,QAAQ,IAAI,CAAC;IACf;AACF;AAGA,MAAMS,YAAYT,QAAQ,IAAI,CAAC,OAAO,CAAC;AACvC,MAAMU,cACJN,AAAW,OAAXA,UACAA,SAAS,IAAIJ,QAAQ,IAAI,CAAC,MAAM,IAChC,CAACA,QAAQ,IAAI,CAACI,SAAS,EAAE,CAAC,UAAU,CAAC,OACjCA,SAAS,IACT;AACN,MAAMO,cAAc,IAAIC,IACtB;IAACH;IAAWL;IAAQM;CAAY,CAAC,MAAM,CAAC,CAACG,IAAMA,AAAM,OAANA;AAEjD,MAAMC,OAAOd,QAAQ,IAAI,CACtB,KAAK,CAAC,GACN,MAAM,CAAC,CAACe,GAAGC,MAAQ,CAACL,YAAY,GAAG,CAACK,MAAM;AAE7C,IAAIC;AAEFA,QADEd,WACM,IAAIe,qBACHb,QACD,IAAIc,oBAAoBZ,eAExB,IAAIa;AAGdC,YAAYJ,OAAO,gBAAgB;IACjC,aAAa;IACbH;IACA,SAASQ;IACT,eAAeC;AACjB,GAAG,KAAK,CAAC,CAACC;IACRxB,QAAQ,IAAI,CAACyB,eAAeD;AAC9B"}
1
+ {"version":3,"file":"cli.mjs","sources":["../../src/cli.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createReportCliCommands } from '@midscene/core';\nimport { reportCLIError, runToolsCLI } from '@midscene/shared/cli';\nimport dotenv from 'dotenv';\nimport { parseWebCliOptions } from './cli-options';\nimport { WebMidsceneTools } from './mcp-tools';\nimport { WebCdpMidsceneTools } from './mcp-tools-cdp';\nimport { WebPuppeteerMidsceneTools } from './mcp-tools-puppeteer';\n\n// Load .env early so MIDSCENE_CDP_ENDPOINT is available during arg parsing\nconst envFile = join(process.cwd(), '.env');\nif (existsSync(envFile)) {\n dotenv.config({ path: envFile });\n}\n\ndeclare const __VERSION__: string;\n\nPromise.resolve()\n .then(() => {\n const parsedOptions = parseWebCliOptions(process.argv.slice(2));\n\n let tools:\n | WebMidsceneTools\n | WebPuppeteerMidsceneTools\n | WebCdpMidsceneTools;\n if (parsedOptions.mode === 'bridge') {\n tools = new WebMidsceneTools();\n } else if (parsedOptions.mode === 'cdp') {\n tools = new WebCdpMidsceneTools(parsedOptions.cdpEndpoint!);\n } else {\n tools = new WebPuppeteerMidsceneTools(parsedOptions.viewport);\n }\n\n return runToolsCLI(tools, 'midscene-web', {\n stripPrefix: 'web_',\n argv: parsedOptions.argv,\n version: __VERSION__,\n extraCommands: createReportCliCommands(),\n });\n })\n .catch((e) => {\n process.exit(reportCLIError(e));\n });\n"],"names":["envFile","join","process","existsSync","dotenv","Promise","parsedOptions","parseWebCliOptions","tools","WebMidsceneTools","WebCdpMidsceneTools","WebPuppeteerMidsceneTools","runToolsCLI","__VERSION__","createReportCliCommands","e","reportCLIError"],"mappings":";;;;;;;;;AAWA,MAAMA,UAAUC,KAAKC,QAAQ,GAAG,IAAI;AACpC,IAAIC,WAAWH,UACbI,OAAO,MAAM,CAAC;IAAE,MAAMJ;AAAQ;AAKhCK,QAAQ,OAAO,GACZ,IAAI,CAAC;IACJ,MAAMC,gBAAgBC,mBAAmBL,QAAQ,IAAI,CAAC,KAAK,CAAC;IAE5D,IAAIM;IAKFA,QADEF,AAAuB,aAAvBA,cAAc,IAAI,GACZ,IAAIG,qBACHH,AAAuB,UAAvBA,cAAc,IAAI,GACnB,IAAII,oBAAoBJ,cAAc,WAAW,IAEjD,IAAIK,0BAA0BL,cAAc,QAAQ;IAG9D,OAAOM,YAAYJ,OAAO,gBAAgB;QACxC,aAAa;QACb,MAAMF,cAAc,IAAI;QACxB,SAASO;QACT,eAAeC;IACjB;AACF,GACC,KAAK,CAAC,CAACC;IACNb,QAAQ,IAAI,CAACc,eAAeD;AAC9B"}
@@ -0,0 +1,38 @@
1
+ import { assert } from "@midscene/shared/utils";
2
+ const defaultViewportWidth = 1440;
3
+ const defaultViewportHeight = 800;
4
+ const defaultViewportSize = {
5
+ width: defaultViewportWidth,
6
+ height: defaultViewportHeight
7
+ };
8
+ const defaultPuppeteerWindowViewportSize = {
9
+ width: defaultViewportWidth,
10
+ height: defaultViewportHeight
11
+ };
12
+ const defaultStaticPageViewportSize = {
13
+ width: defaultViewportWidth,
14
+ height: defaultViewportHeight
15
+ };
16
+ function parseViewportDimension(rawValue, name) {
17
+ const parsedValue = 'number' == typeof rawValue ? rawValue : Number(rawValue);
18
+ assert(Number.isInteger(parsedValue), `${name} must be a positive integer, but got ${rawValue}`);
19
+ assert(parsedValue > 0, `${name} must be greater than 0, but got ${rawValue}`);
20
+ return parsedValue;
21
+ }
22
+ function resolveViewportSize(viewport, fallback = defaultViewportSize) {
23
+ const width = viewport?.width === void 0 || null === viewport.width ? fallback.width : parseViewportDimension(viewport.width, 'viewportWidth');
24
+ const height = viewport?.height === void 0 || null === viewport.height ? fallback.height : parseViewportDimension(viewport.height, 'viewportHeight');
25
+ return {
26
+ width,
27
+ height
28
+ };
29
+ }
30
+ function resolveWebViewportSize(viewport, fallback = defaultViewportSize) {
31
+ return resolveViewportSize({
32
+ width: viewport?.viewportWidth,
33
+ height: viewport?.viewportHeight
34
+ }, fallback);
35
+ }
36
+ export { defaultPuppeteerWindowViewportSize, defaultStaticPageViewportSize, defaultViewportHeight, defaultViewportSize, defaultViewportWidth, resolveViewportSize, resolveWebViewportSize };
37
+
38
+ //# sourceMappingURL=viewport.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common/viewport.mjs","sources":["../../../src/common/viewport.ts"],"sourcesContent":["import { assert } from '@midscene/shared/utils';\n\nexport interface ViewportSize {\n width: number;\n height: number;\n}\n\nexport const defaultViewportWidth = 1440;\nexport const defaultViewportHeight = 800;\nexport const defaultViewportSize: ViewportSize = {\n width: defaultViewportWidth,\n height: defaultViewportHeight,\n};\nexport const defaultPuppeteerWindowViewportSize: ViewportSize = {\n width: defaultViewportWidth,\n height: defaultViewportHeight,\n};\nexport const defaultStaticPageViewportSize: ViewportSize = {\n width: defaultViewportWidth,\n height: defaultViewportHeight,\n};\n\nfunction parseViewportDimension(\n rawValue: number | string,\n name: 'viewportWidth' | 'viewportHeight',\n): number {\n const parsedValue =\n typeof rawValue === 'number' ? rawValue : Number(rawValue);\n\n assert(\n Number.isInteger(parsedValue),\n `${name} must be a positive integer, but got ${rawValue}`,\n );\n assert(\n parsedValue > 0,\n `${name} must be greater than 0, but got ${rawValue}`,\n );\n\n return parsedValue;\n}\n\nexport function resolveViewportSize(\n viewport?: {\n width?: number | string | null;\n height?: number | string | null;\n },\n fallback: ViewportSize = defaultViewportSize,\n): ViewportSize {\n const width =\n viewport?.width === undefined || viewport.width === null\n ? fallback.width\n : parseViewportDimension(viewport.width, 'viewportWidth');\n const height =\n viewport?.height === undefined || viewport.height === null\n ? fallback.height\n : parseViewportDimension(viewport.height, 'viewportHeight');\n\n return { width, height };\n}\n\nexport function resolveWebViewportSize(\n viewport?: {\n viewportWidth?: number | string | null;\n viewportHeight?: number | string | null;\n },\n fallback: ViewportSize = defaultViewportSize,\n): ViewportSize {\n return resolveViewportSize(\n {\n width: viewport?.viewportWidth,\n height: viewport?.viewportHeight,\n },\n fallback,\n );\n}\n"],"names":["defaultViewportWidth","defaultViewportHeight","defaultViewportSize","defaultPuppeteerWindowViewportSize","defaultStaticPageViewportSize","parseViewportDimension","rawValue","name","parsedValue","Number","assert","resolveViewportSize","viewport","fallback","width","undefined","height","resolveWebViewportSize"],"mappings":";AAOO,MAAMA,uBAAuB;AAC7B,MAAMC,wBAAwB;AAC9B,MAAMC,sBAAoC;IAC/C,OAAOF;IACP,QAAQC;AACV;AACO,MAAME,qCAAmD;IAC9D,OAAOH;IACP,QAAQC;AACV;AACO,MAAMG,gCAA8C;IACzD,OAAOJ;IACP,QAAQC;AACV;AAEA,SAASI,uBACPC,QAAyB,EACzBC,IAAwC;IAExC,MAAMC,cACJ,AAAoB,YAApB,OAAOF,WAAwBA,WAAWG,OAAOH;IAEnDI,OACED,OAAO,SAAS,CAACD,cACjB,GAAGD,KAAK,qCAAqC,EAAED,UAAU;IAE3DI,OACEF,cAAc,GACd,GAAGD,KAAK,iCAAiC,EAAED,UAAU;IAGvD,OAAOE;AACT;AAEO,SAASG,oBACdC,QAGC,EACDC,WAAyBX,mBAAmB;IAE5C,MAAMY,QACJF,UAAU,UAAUG,UAAaH,AAAmB,SAAnBA,SAAS,KAAK,GAC3CC,SAAS,KAAK,GACdR,uBAAuBO,SAAS,KAAK,EAAE;IAC7C,MAAMI,SACJJ,UAAU,WAAWG,UAAaH,AAAoB,SAApBA,SAAS,MAAM,GAC7CC,SAAS,MAAM,GACfR,uBAAuBO,SAAS,MAAM,EAAE;IAE9C,OAAO;QAAEE;QAAOE;IAAO;AACzB;AAEO,SAASC,uBACdL,QAGC,EACDC,WAAyBX,mBAAmB;IAE5C,OAAOS,oBACL;QACE,OAAOC,UAAU;QACjB,QAAQA,UAAU;IACpB,GACAC;AAEJ"}
@@ -7,7 +7,7 @@ class WebMCPServer extends BaseMCPServer {
7
7
  constructor(toolsManager){
8
8
  super({
9
9
  name: '@midscene/web-bridge-mcp',
10
- version: "1.7.7",
10
+ version: "1.7.9",
11
11
  description: 'Control the browser using natural language commands'
12
12
  }, toolsManager);
13
13
  }
@@ -4,6 +4,7 @@ import { BaseMidsceneTools } from "@midscene/shared/mcp/base-tools";
4
4
  import puppeteer_core from "puppeteer-core";
5
5
  import { getProxyEndpoint } from "./cdp-proxy-manager.mjs";
6
6
  import { cleanupTargetIdFile, readSavedTargetId, saveTargetId } from "./cdp-target-store.mjs";
7
+ import { defaultStaticPageViewportSize } from "./common/viewport.mjs";
7
8
  import { PuppeteerAgent } from "./puppeteer/index.mjs";
8
9
  import { StaticPage } from "./static/index.mjs";
9
10
  function _define_property(obj, key, value) {
@@ -28,10 +29,7 @@ class WebCdpMidsceneTools extends BaseMidsceneTools {
28
29
  createTemporaryDevice() {
29
30
  return new StaticPage({
30
31
  screenshot: ScreenshotItem.create('', Date.now()),
31
- shotSize: {
32
- width: 1920,
33
- height: 1080
34
- },
32
+ shotSize: defaultStaticPageViewportSize,
35
33
  shrunkShotToLogicalRatio: 1
36
34
  });
37
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-tools-cdp.mjs","sources":["../../src/mcp-tools-cdp.ts"],"sourcesContent":["import { ScreenshotItem, z } from '@midscene/core';\nimport { getDebug } from '@midscene/shared/logger';\nimport { BaseMidsceneTools } from '@midscene/shared/mcp/base-tools';\nimport type { ToolDefinition } from '@midscene/shared/mcp/types';\nimport type { Page as PuppeteerPage } from 'puppeteer';\nimport puppeteer from 'puppeteer-core';\nimport type { Browser, Page } from 'puppeteer-core';\nimport { getProxyEndpoint } from './cdp-proxy-manager';\nimport {\n cleanupTargetIdFile,\n readSavedTargetId,\n saveTargetId,\n} from './cdp-target-store';\nimport { PuppeteerAgent } from './puppeteer';\nimport { StaticPage } from './static';\n\nconst debug = getDebug('mcp:cdp');\n\n/** CDP target discovery may need a brief moment after WebSocket open. */\nconst CDP_TARGET_DISCOVERY_DELAY_MS = 500;\n\n/**\n * puppeteer-core does not expose a public method for the underlying CDP\n * target id, so we reach into `_targetId`. Centralised here so a future\n * puppeteer release exposing this properly only requires one change.\n * Callers must treat the result as optional.\n */\nfunction getTargetId(page: Page): string | undefined {\n return (page.target() as unknown as { _targetId?: string })._targetId;\n}\n\n/**\n * Tools manager for Web CDP-mode MCP.\n * Connects to an existing Chrome browser via CDP (Chrome DevTools Protocol) endpoint.\n * Unlike WebPuppeteerMidsceneTools which launches its own Chrome, this connects\n * to a browser that is already running with remote debugging enabled.\n *\n * Uses a persistent WebSocket proxy to avoid repeated Chrome permission popups\n * when Chrome's settings-based remote debugging is used.\n */\nexport class WebCdpMidsceneTools extends BaseMidsceneTools<PuppeteerAgent> {\n protected getCliReportSessionName() {\n return 'midscene-web';\n }\n private cdpEndpoint: string;\n private activeBrowser: Browser | null = null;\n\n constructor(cdpEndpoint: string) {\n super();\n this.cdpEndpoint = cdpEndpoint;\n }\n\n protected createTemporaryDevice() {\n return new StaticPage({\n screenshot: ScreenshotItem.create('', Date.now()),\n shotSize: { width: 1920, height: 1080 },\n shrunkShotToLogicalRatio: 1,\n });\n }\n\n protected async ensureAgent(navigateToUrl?: string): Promise<PuppeteerAgent> {\n // Re-init if URL provided\n if (this.agent && navigateToUrl) {\n try {\n await this.agent?.destroy?.();\n } catch (error) {\n console.debug('Failed to destroy agent during re-init:', error);\n }\n this.agent = undefined;\n }\n\n if (this.agent) return this.agent;\n\n // Connect via proxy to avoid repeated Chrome permission popups\n if (!this.activeBrowser) {\n const endpoint = await getProxyEndpoint(this.cdpEndpoint);\n this.activeBrowser = await puppeteer.connect({\n browserWSEndpoint: endpoint,\n defaultViewport: null,\n });\n }\n\n const browser = this.activeBrowser;\n let pages = await browser.pages();\n\n // If no pages discovered, wait briefly and retry — some CDP targets\n // need a moment to appear after the WebSocket connection is established.\n if (pages.length === 0) {\n await new Promise((r) => setTimeout(r, CDP_TARGET_DISCOVERY_DELAY_MS));\n pages = await browser.pages();\n }\n\n const webPages = pages.filter((p) => /^https?:\\/\\//.test(p.url()));\n debug(\n 'Found %d page(s), %d web page(s): %o',\n pages.length,\n webPages.length,\n pages.map((p) => p.url()),\n );\n let page: Page;\n\n if (navigateToUrl) {\n if (webPages.length > 0) {\n // Reuse an existing page and navigate it — avoids creating invisible\n // tabs when Chrome uses settings-based remote debugging (no HTTP\n // discovery endpoints, /devtools/page/* returns 403).\n page = webPages[webPages.length - 1];\n await page.bringToFront();\n await page.goto(navigateToUrl, {\n timeout: 30000,\n waitUntil: 'domcontentloaded',\n });\n } else {\n // No existing web pages — fall back to creating a new tab\n page = await browser.newPage();\n await page.goto(navigateToUrl, {\n timeout: 30000,\n waitUntil: 'domcontentloaded',\n });\n }\n } else {\n // Try to find the exact tab from a previous `connect` command via saved targetId.\n const savedTargetId = readSavedTargetId();\n let matchedPage: Page | undefined;\n\n if (savedTargetId && pages.length > 0) {\n matchedPage = pages.find((p) => getTargetId(p) === savedTargetId);\n if (matchedPage) {\n debug('Matched saved targetId %s', savedTargetId);\n } else {\n debug(\n 'Saved targetId %s not found among %d pages, falling back',\n savedTargetId,\n pages.length,\n );\n }\n }\n\n if (matchedPage) {\n page = matchedPage;\n } else if (webPages.length > 0) {\n page = webPages[webPages.length - 1];\n } else if (pages.length > 0) {\n page = pages[pages.length - 1];\n } else {\n page = await browser.newPage();\n }\n\n await page.bringToFront();\n }\n\n // Persist the targetId so subsequent CLI commands can find this exact tab\n const targetId = getTargetId(page);\n if (targetId) {\n saveTargetId(targetId);\n } else {\n // If puppeteer ever drops the private _targetId field, this branch\n // makes the regression visible instead of silently disabling the\n // cross-command tab reuse path.\n debug(\n 'No targetId on page.target(); cross-command tab reuse disabled until puppeteer integration is updated.',\n );\n }\n\n const reportOptions = this.readCliReportAgentOptions();\n this.agent = new PuppeteerAgent(page as unknown as PuppeteerPage, {\n ...(reportOptions ?? {}),\n });\n return this.agent;\n }\n\n public async destroy(): Promise<void> {\n await super.destroy();\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n }\n\n protected preparePlatformTools(): ToolDefinition[] {\n return [\n {\n name: 'web_connect',\n description:\n 'Connect to a web page via CDP. Opens a new tab with the given URL, or reuses the current page.',\n schema: {\n url: z\n .string()\n .url()\n .optional()\n .describe('URL to open in new tab (omit to use current page)'),\n },\n handler: async (args) => {\n const { url } = args as { url?: string };\n\n // Destroy existing agent\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch (e) {\n console.debug('Failed to destroy agent during connect:', e);\n }\n this.agent = undefined;\n }\n\n const reportSession = this.createNewCliReportSession(\n url ?? 'current-page',\n );\n this.commitCliReportSession(reportSession);\n this.agent = await this.ensureAgent(url);\n\n const screenshot = await this.agent.page?.screenshotBase64();\n const label = url ?? 'current page';\n\n return {\n content: [\n { type: 'text', text: `Connected via CDP to: ${label}` },\n ...(screenshot ? this.buildScreenshotContent(screenshot) : []),\n ],\n };\n },\n },\n {\n name: 'web_disconnect',\n description:\n 'Disconnect from current web page. The browser stays running (managed externally).',\n schema: {},\n handler: async () => {\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch (e) {\n console.debug('Failed to destroy agent during disconnect:', e);\n }\n this.agent = undefined;\n }\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n cleanupTargetIdFile();\n return this.buildTextResult(\n 'Disconnected from web page (browser still running externally)',\n );\n },\n },\n ];\n }\n}\n"],"names":["debug","getDebug","CDP_TARGET_DISCOVERY_DELAY_MS","getTargetId","page","WebCdpMidsceneTools","BaseMidsceneTools","StaticPage","ScreenshotItem","Date","navigateToUrl","error","console","undefined","endpoint","getProxyEndpoint","puppeteer","browser","pages","Promise","r","setTimeout","webPages","p","savedTargetId","readSavedTargetId","matchedPage","targetId","saveTargetId","reportOptions","PuppeteerAgent","z","args","url","e","reportSession","screenshot","label","cleanupTargetIdFile","cdpEndpoint"],"mappings":"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"}
1
+ {"version":3,"file":"mcp-tools-cdp.mjs","sources":["../../src/mcp-tools-cdp.ts"],"sourcesContent":["import { ScreenshotItem, z } from '@midscene/core';\nimport { getDebug } from '@midscene/shared/logger';\nimport { BaseMidsceneTools } from '@midscene/shared/mcp/base-tools';\nimport type { ToolDefinition } from '@midscene/shared/mcp/types';\nimport type { Page as PuppeteerPage } from 'puppeteer';\nimport puppeteer from 'puppeteer-core';\nimport type { Browser, Page } from 'puppeteer-core';\nimport { getProxyEndpoint } from './cdp-proxy-manager';\nimport {\n cleanupTargetIdFile,\n readSavedTargetId,\n saveTargetId,\n} from './cdp-target-store';\nimport { defaultStaticPageViewportSize } from './common/viewport';\nimport { PuppeteerAgent } from './puppeteer';\nimport { StaticPage } from './static';\n\nconst debug = getDebug('mcp:cdp');\n\n/** CDP target discovery may need a brief moment after WebSocket open. */\nconst CDP_TARGET_DISCOVERY_DELAY_MS = 500;\n\n/**\n * puppeteer-core does not expose a public method for the underlying CDP\n * target id, so we reach into `_targetId`. Centralised here so a future\n * puppeteer release exposing this properly only requires one change.\n * Callers must treat the result as optional.\n */\nfunction getTargetId(page: Page): string | undefined {\n return (page.target() as unknown as { _targetId?: string })._targetId;\n}\n\n/**\n * Tools manager for Web CDP-mode MCP.\n * Connects to an existing Chrome browser via CDP (Chrome DevTools Protocol) endpoint.\n * Unlike WebPuppeteerMidsceneTools which launches its own Chrome, this connects\n * to a browser that is already running with remote debugging enabled.\n *\n * Uses a persistent WebSocket proxy to avoid repeated Chrome permission popups\n * when Chrome's settings-based remote debugging is used.\n */\nexport class WebCdpMidsceneTools extends BaseMidsceneTools<PuppeteerAgent> {\n protected getCliReportSessionName() {\n return 'midscene-web';\n }\n private cdpEndpoint: string;\n private activeBrowser: Browser | null = null;\n\n constructor(cdpEndpoint: string) {\n super();\n this.cdpEndpoint = cdpEndpoint;\n }\n\n protected createTemporaryDevice() {\n return new StaticPage({\n screenshot: ScreenshotItem.create('', Date.now()),\n shotSize: defaultStaticPageViewportSize,\n shrunkShotToLogicalRatio: 1,\n });\n }\n\n protected async ensureAgent(navigateToUrl?: string): Promise<PuppeteerAgent> {\n // Re-init if URL provided\n if (this.agent && navigateToUrl) {\n try {\n await this.agent?.destroy?.();\n } catch (error) {\n console.debug('Failed to destroy agent during re-init:', error);\n }\n this.agent = undefined;\n }\n\n if (this.agent) return this.agent;\n\n // Connect via proxy to avoid repeated Chrome permission popups\n if (!this.activeBrowser) {\n const endpoint = await getProxyEndpoint(this.cdpEndpoint);\n this.activeBrowser = await puppeteer.connect({\n browserWSEndpoint: endpoint,\n defaultViewport: null,\n });\n }\n\n const browser = this.activeBrowser;\n let pages = await browser.pages();\n\n // If no pages discovered, wait briefly and retry — some CDP targets\n // need a moment to appear after the WebSocket connection is established.\n if (pages.length === 0) {\n await new Promise((r) => setTimeout(r, CDP_TARGET_DISCOVERY_DELAY_MS));\n pages = await browser.pages();\n }\n\n const webPages = pages.filter((p) => /^https?:\\/\\//.test(p.url()));\n debug(\n 'Found %d page(s), %d web page(s): %o',\n pages.length,\n webPages.length,\n pages.map((p) => p.url()),\n );\n let page: Page;\n\n if (navigateToUrl) {\n if (webPages.length > 0) {\n // Reuse an existing page and navigate it — avoids creating invisible\n // tabs when Chrome uses settings-based remote debugging (no HTTP\n // discovery endpoints, /devtools/page/* returns 403).\n page = webPages[webPages.length - 1];\n await page.bringToFront();\n await page.goto(navigateToUrl, {\n timeout: 30000,\n waitUntil: 'domcontentloaded',\n });\n } else {\n // No existing web pages — fall back to creating a new tab\n page = await browser.newPage();\n await page.goto(navigateToUrl, {\n timeout: 30000,\n waitUntil: 'domcontentloaded',\n });\n }\n } else {\n // Try to find the exact tab from a previous `connect` command via saved targetId.\n const savedTargetId = readSavedTargetId();\n let matchedPage: Page | undefined;\n\n if (savedTargetId && pages.length > 0) {\n matchedPage = pages.find((p) => getTargetId(p) === savedTargetId);\n if (matchedPage) {\n debug('Matched saved targetId %s', savedTargetId);\n } else {\n debug(\n 'Saved targetId %s not found among %d pages, falling back',\n savedTargetId,\n pages.length,\n );\n }\n }\n\n if (matchedPage) {\n page = matchedPage;\n } else if (webPages.length > 0) {\n page = webPages[webPages.length - 1];\n } else if (pages.length > 0) {\n page = pages[pages.length - 1];\n } else {\n page = await browser.newPage();\n }\n\n await page.bringToFront();\n }\n\n // Persist the targetId so subsequent CLI commands can find this exact tab\n const targetId = getTargetId(page);\n if (targetId) {\n saveTargetId(targetId);\n } else {\n // If puppeteer ever drops the private _targetId field, this branch\n // makes the regression visible instead of silently disabling the\n // cross-command tab reuse path.\n debug(\n 'No targetId on page.target(); cross-command tab reuse disabled until puppeteer integration is updated.',\n );\n }\n\n const reportOptions = this.readCliReportAgentOptions();\n this.agent = new PuppeteerAgent(page as unknown as PuppeteerPage, {\n ...(reportOptions ?? {}),\n });\n return this.agent;\n }\n\n public async destroy(): Promise<void> {\n await super.destroy();\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n }\n\n protected preparePlatformTools(): ToolDefinition[] {\n return [\n {\n name: 'web_connect',\n description:\n 'Connect to a web page via CDP. Opens a new tab with the given URL, or reuses the current page.',\n schema: {\n url: z\n .string()\n .url()\n .optional()\n .describe('URL to open in new tab (omit to use current page)'),\n },\n handler: async (args) => {\n const { url } = args as { url?: string };\n\n // Destroy existing agent\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch (e) {\n console.debug('Failed to destroy agent during connect:', e);\n }\n this.agent = undefined;\n }\n\n const reportSession = this.createNewCliReportSession(\n url ?? 'current-page',\n );\n this.commitCliReportSession(reportSession);\n this.agent = await this.ensureAgent(url);\n\n const screenshot = await this.agent.page?.screenshotBase64();\n const label = url ?? 'current page';\n\n return {\n content: [\n { type: 'text', text: `Connected via CDP to: ${label}` },\n ...(screenshot ? this.buildScreenshotContent(screenshot) : []),\n ],\n };\n },\n },\n {\n name: 'web_disconnect',\n description:\n 'Disconnect from current web page. The browser stays running (managed externally).',\n schema: {},\n handler: async () => {\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch (e) {\n console.debug('Failed to destroy agent during disconnect:', e);\n }\n this.agent = undefined;\n }\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n cleanupTargetIdFile();\n return this.buildTextResult(\n 'Disconnected from web page (browser still running externally)',\n );\n },\n },\n ];\n }\n}\n"],"names":["debug","getDebug","CDP_TARGET_DISCOVERY_DELAY_MS","getTargetId","page","WebCdpMidsceneTools","BaseMidsceneTools","StaticPage","ScreenshotItem","Date","defaultStaticPageViewportSize","navigateToUrl","error","console","undefined","endpoint","getProxyEndpoint","puppeteer","browser","pages","Promise","r","setTimeout","webPages","p","savedTargetId","readSavedTargetId","matchedPage","targetId","saveTargetId","reportOptions","PuppeteerAgent","z","args","url","e","reportSession","screenshot","label","cleanupTargetIdFile","cdpEndpoint"],"mappings":"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"}
@@ -7,13 +7,43 @@ import { ScreenshotItem, z } from "@midscene/core";
7
7
  import { BaseMidsceneTools } from "@midscene/shared/mcp/base-tools";
8
8
  import { resolveChromePath } from "@midscene/shared/mcp/chrome-path";
9
9
  import puppeteer_core from "puppeteer-core";
10
+ import { defaultPuppeteerWindowViewportSize, defaultStaticPageViewportSize } from "./common/viewport.mjs";
10
11
  import { PuppeteerAgent } from "./puppeteer/index.mjs";
11
12
  import { StaticPage } from "./static/index.mjs";
13
+ function _define_property(obj, key, value) {
14
+ if (key in obj) Object.defineProperty(obj, key, {
15
+ value: value,
16
+ enumerable: true,
17
+ configurable: true,
18
+ writable: true
19
+ });
20
+ else obj[key] = value;
21
+ return obj;
22
+ }
12
23
  const ENDPOINT_FILE = join(tmpdir(), 'midscene-puppeteer-endpoint');
13
24
  const USER_DATA_DIR = join(tmpdir(), 'midscene-puppeteer-profile');
25
+ const PUPPETEER_ENDPOINT_FILE = ENDPOINT_FILE;
26
+ function buildDetachedChromeArgs(options) {
27
+ const viewport = options.viewport ?? defaultPuppeteerWindowViewportSize;
28
+ return [
29
+ '--headless=new',
30
+ `--user-data-dir=${options.userDataDir}`,
31
+ '--remote-debugging-port=0',
32
+ '--no-first-run',
33
+ '--no-default-browser-check',
34
+ '--disable-extensions',
35
+ '--disable-default-apps',
36
+ '--disable-sync',
37
+ '--disable-background-networking',
38
+ '--password-store=basic',
39
+ '--use-mock-keychain',
40
+ `--window-size=${viewport.width},${viewport.height}`,
41
+ '--force-color-profile=srgb'
42
+ ];
43
+ }
14
44
  const browserManager = {
15
45
  activeBrowser: null,
16
- async getOrLaunch () {
46
+ async getOrLaunch (viewport) {
17
47
  if (existsSync(ENDPOINT_FILE)) try {
18
48
  const endpoint = (await readFile(ENDPOINT_FILE, 'utf-8')).trim();
19
49
  const browser = await puppeteer_core.connect({
@@ -29,7 +59,7 @@ const browserManager = {
29
59
  await unlink(ENDPOINT_FILE);
30
60
  } catch {}
31
61
  }
32
- const wsEndpoint = await this.launchDetachedChrome();
62
+ const wsEndpoint = await this.launchDetachedChrome(viewport);
33
63
  await writeFile(ENDPOINT_FILE, wsEndpoint);
34
64
  const browser = await puppeteer_core.connect({
35
65
  browserWSEndpoint: wsEndpoint,
@@ -59,26 +89,15 @@ const browserManager = {
59
89
  this.activeBrowser = null;
60
90
  }
61
91
  },
62
- async launchDetachedChrome () {
92
+ async launchDetachedChrome (viewport) {
63
93
  const chromePath = resolveChromePath();
64
94
  await mkdir(USER_DATA_DIR, {
65
95
  recursive: true
66
96
  });
67
- const args = [
68
- '--headless=new',
69
- `--user-data-dir=${USER_DATA_DIR}`,
70
- '--remote-debugging-port=0',
71
- '--no-first-run',
72
- '--no-default-browser-check',
73
- '--disable-extensions',
74
- '--disable-default-apps',
75
- '--disable-sync',
76
- '--disable-background-networking',
77
- '--password-store=basic',
78
- '--use-mock-keychain',
79
- '--window-size=1280,800',
80
- '--force-color-profile=srgb'
81
- ];
97
+ const args = buildDetachedChromeArgs({
98
+ userDataDir: USER_DATA_DIR,
99
+ viewport
100
+ });
82
101
  const proc = spawn(chromePath, args, {
83
102
  detached: true,
84
103
  stdio: [
@@ -114,10 +133,7 @@ class WebPuppeteerMidsceneTools extends BaseMidsceneTools {
114
133
  createTemporaryDevice() {
115
134
  return new StaticPage({
116
135
  screenshot: ScreenshotItem.create('', Date.now()),
117
- shotSize: {
118
- width: 1920,
119
- height: 1080
120
- },
136
+ shotSize: this.viewport ?? defaultStaticPageViewportSize,
121
137
  shrunkShotToLogicalRatio: 1
122
138
  });
123
139
  }
@@ -129,12 +145,13 @@ class WebPuppeteerMidsceneTools extends BaseMidsceneTools {
129
145
  this.agent = void 0;
130
146
  }
131
147
  if (this.agent) return this.agent;
132
- const { browser, reused } = await browserManager.getOrLaunch();
148
+ const { browser, reused } = await browserManager.getOrLaunch(this.viewport);
133
149
  browserManager.activeBrowser = browser;
134
150
  const pages = await browser.pages();
135
151
  let page;
136
152
  if (navigateToUrl) {
137
153
  page = await browser.newPage();
154
+ if (this.viewport) await page.setViewport(this.viewport);
138
155
  await page.goto(navigateToUrl, {
139
156
  timeout: 30000,
140
157
  waitUntil: 'domcontentloaded'
@@ -143,6 +160,7 @@ class WebPuppeteerMidsceneTools extends BaseMidsceneTools {
143
160
  const webPages = pages.filter((p)=>/^https?:\/\//.test(p.url()));
144
161
  page = webPages.length > 0 ? webPages[webPages.length - 1] : pages[pages.length - 1] || await browser.newPage();
145
162
  if (reused) await page.bringToFront();
163
+ if (this.viewport) await page.setViewport(this.viewport);
146
164
  }
147
165
  const reportOptions = this.readCliReportAgentOptions();
148
166
  this.agent = new PuppeteerAgent(page, {
@@ -218,7 +236,13 @@ class WebPuppeteerMidsceneTools extends BaseMidsceneTools {
218
236
  }
219
237
  ];
220
238
  }
239
+ constructor(viewport){
240
+ super(), _define_property(this, "viewport", void 0);
241
+ this.viewport = viewport ? {
242
+ ...viewport
243
+ } : void 0;
244
+ }
221
245
  }
222
- export { WebPuppeteerMidsceneTools };
246
+ export { PUPPETEER_ENDPOINT_FILE, WebPuppeteerMidsceneTools, buildDetachedChromeArgs };
223
247
 
224
248
  //# sourceMappingURL=mcp-tools-puppeteer.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-tools-puppeteer.mjs","sources":["../../src/mcp-tools-puppeteer.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { ScreenshotItem, z } from '@midscene/core';\nimport { BaseMidsceneTools } from '@midscene/shared/mcp/base-tools';\nimport { resolveChromePath } from '@midscene/shared/mcp/chrome-path';\nimport type { ToolDefinition } from '@midscene/shared/mcp/types';\nimport type { Page as PuppeteerPage } from 'puppeteer';\nimport puppeteer from 'puppeteer-core';\nimport type { Browser, Page } from 'puppeteer-core';\nimport { PuppeteerAgent } from './puppeteer';\nimport { StaticPage } from './static';\n\nconst ENDPOINT_FILE = join(tmpdir(), 'midscene-puppeteer-endpoint');\nconst USER_DATA_DIR = join(tmpdir(), 'midscene-puppeteer-profile');\n\n/**\n * Persistent Puppeteer browser manager.\n * Launches a detached Chrome and persists the WS endpoint across CLI calls.\n */\nconst browserManager = {\n activeBrowser: null as Browser | null,\n\n async getOrLaunch(): Promise<{ browser: Browser; reused: boolean }> {\n if (existsSync(ENDPOINT_FILE)) {\n try {\n const endpoint = (await readFile(ENDPOINT_FILE, 'utf-8')).trim();\n const browser = await puppeteer.connect({\n browserWSEndpoint: endpoint,\n defaultViewport: null,\n });\n return { browser, reused: true };\n } catch {\n try {\n await unlink(ENDPOINT_FILE);\n } catch {}\n }\n }\n\n const wsEndpoint = await this.launchDetachedChrome();\n await writeFile(ENDPOINT_FILE, wsEndpoint);\n\n const browser = await puppeteer.connect({\n browserWSEndpoint: wsEndpoint,\n defaultViewport: null,\n });\n return { browser, reused: false };\n },\n\n async closeBrowser(): Promise<void> {\n if (!existsSync(ENDPOINT_FILE)) return;\n try {\n const endpoint = (await readFile(ENDPOINT_FILE, 'utf-8')).trim();\n const browser = await puppeteer.connect({\n browserWSEndpoint: endpoint,\n });\n await browser.close();\n } catch {}\n try {\n await unlink(ENDPOINT_FILE);\n } catch {}\n },\n\n disconnect(): void {\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n },\n\n async launchDetachedChrome(): Promise<string> {\n const chromePath = resolveChromePath();\n\n await mkdir(USER_DATA_DIR, { recursive: true });\n\n const args = [\n '--headless=new',\n `--user-data-dir=${USER_DATA_DIR}`,\n '--remote-debugging-port=0',\n '--no-first-run',\n '--no-default-browser-check',\n '--disable-extensions',\n '--disable-default-apps',\n '--disable-sync',\n '--disable-background-networking',\n '--password-store=basic',\n '--use-mock-keychain',\n '--window-size=1280,800',\n '--force-color-profile=srgb',\n ];\n\n const proc = spawn(chromePath, args, {\n detached: true,\n stdio: ['ignore', 'ignore', 'pipe'],\n });\n proc.unref();\n\n return new Promise<string>((resolve, reject) => {\n let output = '';\n const onData = (data: Buffer) => {\n output += data.toString();\n const match = output.match(/DevTools listening on (ws:\\/\\/[^\\s]+)/);\n if (match) {\n proc.stderr!.removeListener('data', onData);\n resolve(match[1]);\n }\n };\n proc.stderr!.on('data', onData);\n\n proc.on('exit', (code) => {\n proc.stderr!.removeListener('data', onData);\n reject(\n new Error(\n `Chrome exited with code ${code} before DevTools was ready.\\nChrome stderr: ${output}\\nTip: try setting MIDSCENE_MCP_NO_SANDBOX=1 if running in a container.`,\n ),\n );\n });\n\n setTimeout(\n () =>\n reject(\n new Error(\n `Chrome launch timeout.\\nChrome stderr: ${output}\\nTip: try setting MIDSCENE_MCP_NO_SANDBOX=1 if running in a container.`,\n ),\n ),\n 15000,\n );\n });\n },\n};\n\n/**\n * Tools manager for Web Puppeteer-mode MCP.\n * Uses a persistent headless Chrome browser that survives across CLI calls.\n */\nexport class WebPuppeteerMidsceneTools extends BaseMidsceneTools<PuppeteerAgent> {\n protected getCliReportSessionName() {\n return 'midscene-web';\n }\n\n protected createTemporaryDevice() {\n return new StaticPage({\n screenshot: ScreenshotItem.create('', Date.now()),\n shotSize: { width: 1920, height: 1080 },\n shrunkShotToLogicalRatio: 1,\n });\n }\n\n protected async ensureAgent(navigateToUrl?: string): Promise<PuppeteerAgent> {\n // Re-init if URL provided\n if (this.agent && navigateToUrl) {\n try {\n await this.agent?.destroy?.();\n } catch {}\n this.agent = undefined;\n }\n\n if (this.agent) return this.agent;\n\n const { browser, reused } = await browserManager.getOrLaunch();\n browserManager.activeBrowser = browser;\n\n const pages = await browser.pages();\n let page: Page;\n\n if (navigateToUrl) {\n page = await browser.newPage();\n await page.goto(navigateToUrl, {\n timeout: 30000,\n waitUntil: 'domcontentloaded',\n });\n } else {\n // Reuse the last web page\n const webPages = pages.filter((p) => /^https?:\\/\\//.test(p.url()));\n page =\n webPages.length > 0\n ? webPages[webPages.length - 1]\n : pages[pages.length - 1] || (await browser.newPage());\n\n if (reused) {\n await page.bringToFront();\n }\n }\n\n const reportOptions = this.readCliReportAgentOptions();\n this.agent = new PuppeteerAgent(page as unknown as PuppeteerPage, {\n ...(reportOptions ?? {}),\n });\n return this.agent;\n }\n\n public async destroy(): Promise<void> {\n await super.destroy();\n browserManager.disconnect();\n }\n\n protected preparePlatformTools(): ToolDefinition[] {\n return [\n {\n name: 'web_connect',\n description:\n 'Connect to a web page. Opens a new tab with the given URL, or reuses the current page.',\n schema: {\n url: z\n .string()\n .url()\n .optional()\n .describe('URL to open in new tab (omit to use current page)'),\n },\n handler: async (args) => {\n const { url } = args as { url?: string };\n\n // Destroy existing agent\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch {}\n this.agent = undefined;\n }\n\n const reportSession = this.createNewCliReportSession(\n url ?? 'current-page',\n );\n this.commitCliReportSession(reportSession);\n this.agent = await this.ensureAgent(url);\n\n const screenshot = await this.agent.page?.screenshotBase64();\n const label = url ?? 'current page';\n\n return {\n content: [\n { type: 'text', text: `Connected to: ${label}` },\n ...(screenshot ? this.buildScreenshotContent(screenshot) : []),\n ],\n };\n },\n },\n {\n name: 'web_disconnect',\n description:\n 'Disconnect from current web page. The browser stays running for future calls.',\n schema: {},\n handler: async () => {\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch {}\n this.agent = undefined;\n }\n browserManager.disconnect();\n return this.buildTextResult(\n 'Disconnected from web page (browser still running)',\n );\n },\n },\n {\n name: 'web_close',\n description: 'Close the browser completely and release all resources.',\n schema: {},\n handler: async () => {\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch {}\n this.agent = undefined;\n }\n await browserManager.closeBrowser();\n return this.buildTextResult('Browser closed');\n },\n },\n ];\n }\n}\n"],"names":["ENDPOINT_FILE","join","tmpdir","USER_DATA_DIR","browserManager","existsSync","endpoint","readFile","browser","puppeteer","unlink","wsEndpoint","writeFile","chromePath","resolveChromePath","mkdir","args","proc","spawn","Promise","resolve","reject","output","onData","data","match","code","Error","setTimeout","WebPuppeteerMidsceneTools","BaseMidsceneTools","StaticPage","ScreenshotItem","Date","navigateToUrl","undefined","reused","pages","page","webPages","p","reportOptions","PuppeteerAgent","z","url","reportSession","screenshot","label"],"mappings":"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"}
1
+ {"version":3,"file":"mcp-tools-puppeteer.mjs","sources":["../../src/mcp-tools-puppeteer.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { ScreenshotItem, z } from '@midscene/core';\nimport { BaseMidsceneTools } from '@midscene/shared/mcp/base-tools';\nimport { resolveChromePath } from '@midscene/shared/mcp/chrome-path';\nimport type { ToolDefinition } from '@midscene/shared/mcp/types';\nimport type { Page as PuppeteerPage } from 'puppeteer';\nimport puppeteer from 'puppeteer-core';\nimport type { Browser, Page } from 'puppeteer-core';\nimport {\n type ViewportSize,\n defaultPuppeteerWindowViewportSize,\n defaultStaticPageViewportSize,\n} from './common/viewport';\nimport { PuppeteerAgent } from './puppeteer';\nimport { StaticPage } from './static';\n\nconst ENDPOINT_FILE = join(tmpdir(), 'midscene-puppeteer-endpoint');\nconst USER_DATA_DIR = join(tmpdir(), 'midscene-puppeteer-profile');\n\nexport const PUPPETEER_ENDPOINT_FILE = ENDPOINT_FILE;\n\nexport function buildDetachedChromeArgs(options: {\n userDataDir: string;\n viewport?: ViewportSize;\n}): string[] {\n const viewport = options.viewport ?? defaultPuppeteerWindowViewportSize;\n\n return [\n '--headless=new',\n `--user-data-dir=${options.userDataDir}`,\n '--remote-debugging-port=0',\n '--no-first-run',\n '--no-default-browser-check',\n '--disable-extensions',\n '--disable-default-apps',\n '--disable-sync',\n '--disable-background-networking',\n '--password-store=basic',\n '--use-mock-keychain',\n `--window-size=${viewport.width},${viewport.height}`,\n '--force-color-profile=srgb',\n ];\n}\n\n/**\n * Persistent Puppeteer browser manager.\n * Launches a detached Chrome and persists the WS endpoint across CLI calls.\n */\nconst browserManager = {\n activeBrowser: null as Browser | null,\n\n async getOrLaunch(\n viewport?: ViewportSize,\n ): Promise<{ browser: Browser; reused: boolean }> {\n if (existsSync(ENDPOINT_FILE)) {\n try {\n const endpoint = (await readFile(ENDPOINT_FILE, 'utf-8')).trim();\n const browser = await puppeteer.connect({\n browserWSEndpoint: endpoint,\n defaultViewport: null,\n });\n return { browser, reused: true };\n } catch {\n try {\n await unlink(ENDPOINT_FILE);\n } catch {}\n }\n }\n\n const wsEndpoint = await this.launchDetachedChrome(viewport);\n await writeFile(ENDPOINT_FILE, wsEndpoint);\n\n const browser = await puppeteer.connect({\n browserWSEndpoint: wsEndpoint,\n defaultViewport: null,\n });\n return { browser, reused: false };\n },\n\n async closeBrowser(): Promise<void> {\n if (!existsSync(ENDPOINT_FILE)) return;\n try {\n const endpoint = (await readFile(ENDPOINT_FILE, 'utf-8')).trim();\n const browser = await puppeteer.connect({\n browserWSEndpoint: endpoint,\n });\n await browser.close();\n } catch {}\n try {\n await unlink(ENDPOINT_FILE);\n } catch {}\n },\n\n disconnect(): void {\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n },\n\n async launchDetachedChrome(viewport?: ViewportSize): Promise<string> {\n const chromePath = resolveChromePath();\n\n await mkdir(USER_DATA_DIR, { recursive: true });\n\n const args = buildDetachedChromeArgs({\n userDataDir: USER_DATA_DIR,\n viewport,\n });\n\n const proc = spawn(chromePath, args, {\n detached: true,\n stdio: ['ignore', 'ignore', 'pipe'],\n });\n proc.unref();\n\n return new Promise<string>((resolve, reject) => {\n let output = '';\n const onData = (data: Buffer) => {\n output += data.toString();\n const match = output.match(/DevTools listening on (ws:\\/\\/[^\\s]+)/);\n if (match) {\n proc.stderr!.removeListener('data', onData);\n resolve(match[1]);\n }\n };\n proc.stderr!.on('data', onData);\n\n proc.on('exit', (code) => {\n proc.stderr!.removeListener('data', onData);\n reject(\n new Error(\n `Chrome exited with code ${code} before DevTools was ready.\\nChrome stderr: ${output}\\nTip: try setting MIDSCENE_MCP_NO_SANDBOX=1 if running in a container.`,\n ),\n );\n });\n\n setTimeout(\n () =>\n reject(\n new Error(\n `Chrome launch timeout.\\nChrome stderr: ${output}\\nTip: try setting MIDSCENE_MCP_NO_SANDBOX=1 if running in a container.`,\n ),\n ),\n 15000,\n );\n });\n },\n};\n\n/**\n * Tools manager for Web Puppeteer-mode MCP.\n * Uses a persistent headless Chrome browser that survives across CLI calls.\n */\nexport class WebPuppeteerMidsceneTools extends BaseMidsceneTools<PuppeteerAgent> {\n private readonly viewport?: ViewportSize;\n\n constructor(viewport?: ViewportSize) {\n super();\n this.viewport = viewport ? { ...viewport } : undefined;\n }\n\n protected getCliReportSessionName() {\n return 'midscene-web';\n }\n\n protected createTemporaryDevice() {\n return new StaticPage({\n screenshot: ScreenshotItem.create('', Date.now()),\n shotSize: this.viewport ?? defaultStaticPageViewportSize,\n shrunkShotToLogicalRatio: 1,\n });\n }\n\n protected async ensureAgent(navigateToUrl?: string): Promise<PuppeteerAgent> {\n // Re-init if URL provided\n if (this.agent && navigateToUrl) {\n try {\n await this.agent?.destroy?.();\n } catch {}\n this.agent = undefined;\n }\n\n if (this.agent) return this.agent;\n\n const { browser, reused } = await browserManager.getOrLaunch(this.viewport);\n browserManager.activeBrowser = browser;\n\n const pages = await browser.pages();\n let page: Page;\n\n if (navigateToUrl) {\n page = await browser.newPage();\n if (this.viewport) {\n await page.setViewport(this.viewport);\n }\n await page.goto(navigateToUrl, {\n timeout: 30000,\n waitUntil: 'domcontentloaded',\n });\n } else {\n // Reuse the last web page\n const webPages = pages.filter((p) => /^https?:\\/\\//.test(p.url()));\n page =\n webPages.length > 0\n ? webPages[webPages.length - 1]\n : pages[pages.length - 1] || (await browser.newPage());\n\n if (reused) {\n await page.bringToFront();\n }\n if (this.viewport) {\n await page.setViewport(this.viewport);\n }\n }\n\n const reportOptions = this.readCliReportAgentOptions();\n this.agent = new PuppeteerAgent(page as unknown as PuppeteerPage, {\n ...(reportOptions ?? {}),\n });\n return this.agent;\n }\n\n public async destroy(): Promise<void> {\n await super.destroy();\n browserManager.disconnect();\n }\n\n protected preparePlatformTools(): ToolDefinition[] {\n return [\n {\n name: 'web_connect',\n description:\n 'Connect to a web page. Opens a new tab with the given URL, or reuses the current page.',\n schema: {\n url: z\n .string()\n .url()\n .optional()\n .describe('URL to open in new tab (omit to use current page)'),\n },\n handler: async (args) => {\n const { url } = args as { url?: string };\n\n // Destroy existing agent\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch {}\n this.agent = undefined;\n }\n\n const reportSession = this.createNewCliReportSession(\n url ?? 'current-page',\n );\n this.commitCliReportSession(reportSession);\n this.agent = await this.ensureAgent(url);\n\n const screenshot = await this.agent.page?.screenshotBase64();\n const label = url ?? 'current page';\n\n return {\n content: [\n { type: 'text', text: `Connected to: ${label}` },\n ...(screenshot ? this.buildScreenshotContent(screenshot) : []),\n ],\n };\n },\n },\n {\n name: 'web_disconnect',\n description:\n 'Disconnect from current web page. The browser stays running for future calls.',\n schema: {},\n handler: async () => {\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch {}\n this.agent = undefined;\n }\n browserManager.disconnect();\n return this.buildTextResult(\n 'Disconnected from web page (browser still running)',\n );\n },\n },\n {\n name: 'web_close',\n description: 'Close the browser completely and release all resources.',\n schema: {},\n handler: async () => {\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch {}\n this.agent = undefined;\n }\n await browserManager.closeBrowser();\n return this.buildTextResult('Browser closed');\n },\n },\n ];\n }\n}\n"],"names":["ENDPOINT_FILE","join","tmpdir","USER_DATA_DIR","PUPPETEER_ENDPOINT_FILE","buildDetachedChromeArgs","options","viewport","defaultPuppeteerWindowViewportSize","browserManager","existsSync","endpoint","readFile","browser","puppeteer","unlink","wsEndpoint","writeFile","chromePath","resolveChromePath","mkdir","args","proc","spawn","Promise","resolve","reject","output","onData","data","match","code","Error","setTimeout","WebPuppeteerMidsceneTools","BaseMidsceneTools","StaticPage","ScreenshotItem","Date","defaultStaticPageViewportSize","navigateToUrl","undefined","reused","pages","page","webPages","p","reportOptions","PuppeteerAgent","z","url","reportSession","screenshot","label"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { ScreenshotItem, z } from "@midscene/core";
2
2
  import { BaseMidsceneTools } from "@midscene/shared/mcp/base-tools";
3
3
  import { AgentOverChromeBridge } from "./bridge-mode/index.mjs";
4
+ import { defaultStaticPageViewportSize } from "./common/viewport.mjs";
4
5
  import { StaticPage } from "./static/index.mjs";
5
6
  class WebMidsceneTools extends BaseMidsceneTools {
6
7
  getCliReportSessionName() {
@@ -9,10 +10,7 @@ class WebMidsceneTools extends BaseMidsceneTools {
9
10
  createTemporaryDevice() {
10
11
  return new StaticPage({
11
12
  screenshot: ScreenshotItem.create('', Date.now()),
12
- shotSize: {
13
- width: 1920,
14
- height: 1080
15
- },
13
+ shotSize: defaultStaticPageViewportSize,
16
14
  shrunkShotToLogicalRatio: 1
17
15
  });
18
16
  }