@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.
- package/dist/es/bridge-mode/io-client.mjs +1 -1
- package/dist/es/bridge-mode/io-server.mjs +2 -2
- package/dist/es/bridge-mode/page-browser-side.mjs +1 -1
- package/dist/es/cli-options.mjs +99 -0
- package/dist/es/cli-options.mjs.map +1 -0
- package/dist/es/cli.mjs +11 -32
- package/dist/es/cli.mjs.map +1 -1
- package/dist/es/common/viewport.mjs +38 -0
- package/dist/es/common/viewport.mjs.map +1 -0
- package/dist/es/mcp-server.mjs +1 -1
- package/dist/es/mcp-tools-cdp.mjs +2 -4
- package/dist/es/mcp-tools-cdp.mjs.map +1 -1
- package/dist/es/mcp-tools-puppeteer.mjs +48 -24
- package/dist/es/mcp-tools-puppeteer.mjs.map +1 -1
- package/dist/es/mcp-tools.mjs +2 -4
- package/dist/es/mcp-tools.mjs.map +1 -1
- package/dist/es/puppeteer/agent-launcher.mjs +2 -14
- package/dist/es/puppeteer/agent-launcher.mjs.map +1 -1
- package/dist/lib/bridge-mode/io-client.js +1 -1
- package/dist/lib/bridge-mode/io-server.js +2 -2
- package/dist/lib/bridge-mode/page-browser-side.js +1 -1
- package/dist/lib/cli-options.js +133 -0
- package/dist/lib/cli-options.js.map +1 -0
- package/dist/lib/cli.js +11 -32
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/common/viewport.js +90 -0
- package/dist/lib/common/viewport.js.map +1 -0
- package/dist/lib/mcp-server.js +1 -1
- package/dist/lib/mcp-tools-cdp.js +2 -4
- package/dist/lib/mcp-tools-cdp.js.map +1 -1
- package/dist/lib/mcp-tools-puppeteer.js +55 -25
- package/dist/lib/mcp-tools-puppeteer.js.map +1 -1
- package/dist/lib/mcp-tools.js +2 -4
- package/dist/lib/mcp-tools.js.map +1 -1
- package/dist/lib/puppeteer/agent-launcher.js +4 -16
- package/dist/lib/puppeteer/agent-launcher.js.map +1 -1
- package/dist/types/cli-options.d.ts +8 -0
- package/dist/types/common/viewport.d.ts +17 -0
- package/dist/types/mcp-tools-puppeteer.d.ts +8 -0
- package/dist/types/puppeteer/agent-launcher.d.ts +1 -3
- package/package.json +4 -4
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
});
|
package/dist/es/cli.mjs.map
CHANGED
|
@@ -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;\
|
|
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"}
|
package/dist/es/mcp-server.mjs
CHANGED
|
@@ -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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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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"}
|
package/dist/es/mcp-tools.mjs
CHANGED
|
@@ -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
|
}
|