@opentiny/webmcp-cli 0.0.4 → 0.0.8

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/bin.js CHANGED
@@ -199,11 +199,37 @@ import { spawn } from "child_process";
199
199
  import { fileURLToPath as fileURLToPath2 } from "url";
200
200
  var __filename = fileURLToPath2(import.meta.url);
201
201
  var __dirname = path.dirname(__filename);
202
- var CDP_PORT = 9222;
203
- var CDP_URL = `http://localhost:${CDP_PORT}`;
202
+ function getCdpPort() {
203
+ const raw = process.env.WEBMCP_CDP_PORT;
204
+ if (raw) {
205
+ const port = Number.parseInt(raw, 10);
206
+ if (Number.isFinite(port) && port > 0) return port;
207
+ }
208
+ return 9222;
209
+ }
204
210
  function getWorkspaceDir() {
205
211
  return process.env.WEBMCP_WORKSPACE || path.join(os.homedir(), ".webmcp_chrome_profile");
206
212
  }
213
+ function shouldLaunchHeadless() {
214
+ if (process.env.WEBMCP_HEADLESS === "0") return false;
215
+ return process.env.WEBMCP_HEADLESS === "1" || process.env.CI === "true" || process.env.CI === "1";
216
+ }
217
+ function readActivePortFromProfile(userDataDir) {
218
+ try {
219
+ const content = fs.readFileSync(path.join(userDataDir, "DevToolsActivePort"), "utf-8");
220
+ const port = parseInt(content.split("\n")[0].trim(), 10);
221
+ return Number.isFinite(port) && port > 0 ? port : null;
222
+ } catch {
223
+ return null;
224
+ }
225
+ }
226
+ function candidateCdpBaseUrls(port) {
227
+ return [
228
+ `http://127.0.0.1:${port}`,
229
+ `http://[::1]:${port}`,
230
+ `http://localhost:${port}`
231
+ ];
232
+ }
207
233
  function getLastTabIdFilePath() {
208
234
  return path.join(getWorkspaceDir(), ".last-tab-id");
209
235
  }
@@ -297,6 +323,13 @@ async function connectPuppeteer(browserBaseUrl, attempt = 1, maxAttempts = 3) {
297
323
  }
298
324
  }
299
325
  function getDefaultBrowserPath() {
326
+ const envPath = process.env.CHROME_PATH || process.env.WEBMCP_CHROME_PATH || process.env.PUPPETEER_EXECUTABLE_PATH;
327
+ if (envPath && fs.existsSync(envPath)) {
328
+ return { path: envPath, name: "Chrome" };
329
+ }
330
+ if (envPath) {
331
+ console.log(pc.yellow(`\u6307\u5B9A\u7684\u6D4F\u89C8\u5668\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF0C\u5C06\u56DE\u9000\u5230\u81EA\u52A8\u68C0\u6D4B: ${envPath}`));
332
+ }
300
333
  const platform = os.platform();
301
334
  if (platform === "darwin") {
302
335
  const chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
@@ -340,16 +373,30 @@ async function startBrowserInBackground() {
340
373
  if (!browserInfo || !fs.existsSync(browserInfo.path)) {
341
374
  throw new Error("\u65E0\u6CD5\u5728\u7CFB\u7EDF\u4E2D\u627E\u5230 Chrome \u6216 Edge \u6D4F\u89C8\u5668\u7684\u9ED8\u8BA4\u5B89\u88C5\u8DEF\u5F84\u3002");
342
375
  }
343
- console.log(pc.yellow(`\u6B63\u5728\u542F\u52A8\u540E\u53F0 ${browserInfo.name} \u5B9E\u4F8B (\u7AEF\u53E3: ${CDP_PORT})...`));
344
- const userDataDir = process.env.WEBMCP_WORKSPACE || path.join(os.homedir(), ".webmcp_chrome_profile");
376
+ const cdpPort = getCdpPort();
377
+ console.log(pc.yellow(`\u6B63\u5728\u542F\u52A8\u540E\u53F0 ${browserInfo.name} \u5B9E\u4F8B (\u7AEF\u53E3: ${cdpPort})...`));
378
+ const userDataDir = getWorkspaceDir();
379
+ if (!fs.existsSync(userDataDir)) {
380
+ fs.mkdirSync(userDataDir, { recursive: true });
381
+ }
382
+ try {
383
+ fs.unlinkSync(path.join(userDataDir, "DevToolsActivePort"));
384
+ } catch {
385
+ }
345
386
  const launchArgs = [
346
- `--remote-debugging-port=${CDP_PORT}`,
387
+ `--remote-debugging-port=${cdpPort}`,
347
388
  `--user-data-dir=${userDataDir}`,
348
389
  "--no-first-run",
349
390
  "--no-default-browser-check"
350
391
  ];
351
- if (os.platform() === "win32") {
352
- launchArgs.push("--remote-debugging-address=127.0.0.1");
392
+ if (shouldLaunchHeadless()) {
393
+ launchArgs.push(
394
+ "--headless=new",
395
+ "--no-sandbox",
396
+ "--disable-setuid-sandbox",
397
+ "--disable-gpu",
398
+ "--disable-dev-shm-usage"
399
+ );
353
400
  }
354
401
  const child = spawn(
355
402
  browserInfo.path,
@@ -360,15 +407,22 @@ async function startBrowserInBackground() {
360
407
  }
361
408
  );
362
409
  child.unref();
363
- const pollUrls = [`http://127.0.0.1:${CDP_PORT}/json/version`, `http://localhost:${CDP_PORT}/json/version`];
364
- for (let i = 0; i < 30; i++) {
365
- for (const url of pollUrls) {
366
- try {
367
- await httpGet(url, 1e3);
368
- console.log(pc.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\u3002`));
369
- await new Promise((resolve3) => setTimeout(resolve3, 500));
370
- return;
371
- } catch {
410
+ for (let i = 0; i < 40; i++) {
411
+ const ports = /* @__PURE__ */ new Set([cdpPort]);
412
+ const activePort = readActivePortFromProfile(userDataDir);
413
+ if (activePort) ports.add(activePort);
414
+ for (const port of ports) {
415
+ for (const baseUrl of candidateCdpBaseUrls(port)) {
416
+ try {
417
+ await httpGet(`${baseUrl}/json/version`, 1e3);
418
+ if (port !== cdpPort) {
419
+ console.log(pc.yellow(`\u68C0\u6D4B\u5230\u7AEF\u53E3 ${cdpPort} \u5DF2\u88AB\u5176\u4ED6\u7A0B\u5E8F\u5360\u7528\uFF0C${browserInfo.name} \u5DF2\u81EA\u52A8\u5207\u6362\u5230\u7AEF\u53E3 ${port}\u3002`));
420
+ }
421
+ console.log(pc.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\uFF08${baseUrl}\uFF09\u3002`));
422
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
423
+ return baseUrl;
424
+ } catch {
425
+ }
372
426
  }
373
427
  }
374
428
  await new Promise((resolve3) => setTimeout(resolve3, 500));
@@ -376,13 +430,16 @@ async function startBrowserInBackground() {
376
430
  throw new Error(`${browserInfo.name} \u542F\u52A8\u8D85\u65F6\uFF0C\u65E0\u6CD5\u8FDE\u63A5\u5230 CDP \u7AEF\u53E3\u3002`);
377
431
  }
378
432
  async function connectBrowser() {
433
+ const userDataDir = getWorkspaceDir();
434
+ const cdpPort = getCdpPort();
435
+ const recordedPort = readActivePortFromProfile(userDataDir);
379
436
  const versionAddresses = [
380
- `http://127.0.0.1:${CDP_PORT}/json/version`,
381
- `http://localhost:${CDP_PORT}/json/version`
437
+ ...recordedPort && recordedPort !== cdpPort ? candidateCdpBaseUrls(recordedPort).map((u) => `${u}/json/version`) : [],
438
+ ...candidateCdpBaseUrls(cdpPort).map((u) => `${u}/json/version`)
382
439
  ];
383
440
  const existingUrl = await findAvailableCdpUrl(versionAddresses, 2);
384
441
  if (existingUrl) {
385
- console.log(pc.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${CDP_PORT} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${existingUrl}...`));
442
+ console.log(pc.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${cdpPort} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${existingUrl}...`));
386
443
  try {
387
444
  const browser = await connectPuppeteer(existingUrl);
388
445
  console.log(pc.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${existingUrl}`));
@@ -394,25 +451,22 @@ async function connectBrowser() {
394
451
  );
395
452
  }
396
453
  }
397
- console.log(pc.yellow(`connectBrowser: \u7AEF\u53E3 ${CDP_PORT} \u65E0\u54CD\u5E94\uFF0C\u6B63\u5728\u542F\u52A8\u65B0\u7684\u6D4F\u89C8\u5668\u5B9E\u4F8B...`));
454
+ console.log(pc.yellow(`connectBrowser: \u7AEF\u53E3 ${cdpPort} \u65E0\u54CD\u5E94\uFF0C\u6B63\u5728\u542F\u52A8\u65B0\u7684\u6D4F\u89C8\u5668\u5B9E\u4F8B...`));
455
+ let launchedUrl;
398
456
  try {
399
- await startBrowserInBackground();
457
+ launchedUrl = await startBrowserInBackground();
400
458
  } catch (launchError) {
401
459
  const msg = launchError instanceof Error ? launchError.message : String(launchError);
402
460
  console.error(pc.red(`\u65E0\u6CD5\u542F\u52A8\u6D4F\u89C8\u5668: ${msg}`));
403
461
  throw new Error("Browser launch failed.");
404
462
  }
405
- const launchedUrl = await findAvailableCdpUrl(versionAddresses, 5);
406
- if (!launchedUrl) {
407
- throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\uFF08\u7AEF\u53E3 ${CDP_PORT}\uFF09\uFF0C\u8BF7\u68C0\u67E5 Chrome/Edge \u662F\u5426\u5DF2\u5B89\u88C5\u3002`);
408
- }
409
463
  try {
410
464
  console.log(pc.yellow(`connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u8FDE\u63A5 ${launchedUrl}...`));
411
465
  const browser = await connectPuppeteer(launchedUrl);
412
466
  console.log(pc.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${launchedUrl}`));
413
467
  return browser;
414
468
  } catch {
415
- throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\uFF08\u7AEF\u53E3 ${CDP_PORT}\uFF09\uFF0C\u8BF7\u68C0\u67E5 Chrome/Edge \u662F\u5426\u5DF2\u5B89\u88C5\u3002`);
469
+ throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\uFF08${launchedUrl}\uFF09\uFF0C\u8BF7\u68C0\u67E5 Chrome/Edge \u662F\u5426\u5DF2\u5B89\u88C5\u3002`);
416
470
  }
417
471
  }
418
472
  async function getPageTargetId(page) {
@@ -492,10 +546,11 @@ async function getTargetPage(browser, tabid) {
492
546
  }
493
547
  if (!targetPage) {
494
548
  try {
495
- const urls = [
496
- `http://localhost:${CDP_PORT}/json/list`,
497
- `http://127.0.0.1:${CDP_PORT}/json/list`
498
- ];
549
+ const workspaceDir = getWorkspaceDir();
550
+ const cdpPort = getCdpPort();
551
+ const recordedPort = readActivePortFromProfile(workspaceDir);
552
+ const ports = recordedPort && recordedPort !== cdpPort ? [recordedPort, cdpPort] : [cdpPort];
553
+ const urls = ports.flatMap((port) => candidateCdpBaseUrls(port).map((u) => `${u}/json/list`));
499
554
  let activeTargetId = null;
500
555
  for (const url of urls) {
501
556
  try {
@@ -532,10 +587,10 @@ async function getTargetPage(browser, tabid) {
532
587
  return targetPage;
533
588
  }
534
589
  async function injectIntoPage(page) {
535
- await injectWebMCPPolyfillAndTools(page, true);
590
+ await injectWebMCPPolyfillAndTools(page);
536
591
  }
537
- async function injectWebMCPPolyfillAndTools(page, force = false) {
538
- const polyfillReady = !force && await page.evaluate(() => {
592
+ async function injectWebMCPPolyfillAndTools(page) {
593
+ const polyfillReady = await page.evaluate(() => {
539
594
  return !!window.__webmcpcli_init;
540
595
  }).catch(() => false);
541
596
  if (!polyfillReady) {
@@ -625,11 +680,19 @@ async function stateCommand({ tabid }) {
625
680
  const state = await page.evaluate(async () => {
626
681
  const url = document.URL;
627
682
  const title = document.title;
628
- const mcp = navigator.modelContextTesting || navigator.modelContext;
683
+ const mcp = document.modelContext || navigator.modelContext;
629
684
  let webmcpTools = [];
630
- if (mcp && typeof mcp.listTools === "function") {
631
- const toolsResult = await mcp.listTools();
632
- webmcpTools = toolsResult?.tools || toolsResult || [];
685
+ if (mcp && typeof mcp.getTools === "function") {
686
+ try {
687
+ const toolsResult = await mcp.getTools();
688
+ const list = toolsResult?.tools || toolsResult || [];
689
+ webmcpTools = list.filter((t) => t && typeof t.name === "string").map((t) => ({
690
+ name: t.name,
691
+ description: typeof t.description === "string" ? t.description.slice(0, 200) : void 0
692
+ }));
693
+ } catch {
694
+ webmcpTools = [];
695
+ }
633
696
  }
634
697
  return { url, title, webmcpTools };
635
698
  });
@@ -671,15 +734,19 @@ async function runCommand({
671
734
  try {
672
735
  const page = await getTargetPage(browser, tabid);
673
736
  const urlBefore = page.url();
737
+ await injectIntoPage(page);
674
738
  const cleanedArgs = argsJson.trim();
675
739
  let result;
676
740
  try {
677
741
  result = await page.evaluate(async (name, inputString) => {
678
- const mcp = navigator.modelContextTesting || navigator.modelContext;
742
+ const mcp = document.modelContext || document.modelContext || document.modelContext;
679
743
  if (!mcp || typeof mcp.executeTool !== "function") {
680
- throw new Error("\u5F53\u524D\u9875\u9762\u6CA1\u6709\u6CE8\u5165 WebMCP \u73AF\u5883 (navigator.modelContextTesting.executeTool \u672A\u627E\u5230)");
744
+ throw new Error("\u5F53\u524D\u9875\u9762\u6CA1\u6709\u6CE8\u5165 WebMCP \u73AF\u5883 (document.modelContext \u672A\u627E\u5230)");
681
745
  }
682
- let res = await mcp.executeTool(name, inputString);
746
+ const tools = await mcp.getTools();
747
+ const toolObj = tools.find((t) => t.name === name);
748
+ if (!toolObj) throw new Error(`Tool ${name} not found`);
749
+ let res = await mcp.executeTool(toolObj, inputString);
683
750
  if (typeof res === "string") {
684
751
  try {
685
752
  res = JSON.parse(res);
@@ -856,7 +923,7 @@ async function tabsForwardCommand(tabid) {
856
923
  // package.json
857
924
  var package_default = {
858
925
  name: "@opentiny/webmcp-cli",
859
- version: "0.0.4",
926
+ version: "0.0.8",
860
927
  type: "module",
861
928
  description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
862
929
  main: "dist/index.js",
@@ -874,7 +941,10 @@ var package_default = {
874
941
  build: "tsup && pnpm run build:inject",
875
942
  prepublishOnly: "pnpm run build",
876
943
  "link:global": "npm install -g .",
877
- start: "node dist/bin.js"
944
+ start: "node dist/bin.js",
945
+ test: "vitest run",
946
+ "test:watch": "vitest",
947
+ "test:browser": "vitest run --config vitest.e2e.config.ts"
878
948
  },
879
949
  keywords: [
880
950
  "chrome",
@@ -896,6 +966,7 @@ var package_default = {
896
966
  clipboardy: "^5.3.1",
897
967
  commander: "^11.1.0",
898
968
  json5: "^2.2.3",
969
+ marked: "^15.0.7",
899
970
  picocolors: "^1.0.0",
900
971
  "puppeteer-core": "^22.0.0"
901
972
  },
@@ -904,7 +975,8 @@ var package_default = {
904
975
  esbuild: "^0.25.0",
905
976
  tsup: "^8.0.2",
906
977
  tsx: "^4.0.0",
907
- typescript: "^5.3.3"
978
+ typescript: "^5.3.3",
979
+ vitest: "^4.1.10"
908
980
  }
909
981
  };
910
982
 
@@ -914,6 +986,30 @@ function parseTabId(id) {
914
986
  if (!id) return void 0;
915
987
  return id;
916
988
  }
989
+ function tryParseBrowserStateText(text) {
990
+ const tryParse = (jsonStr) => {
991
+ try {
992
+ const parsed = JSON.parse(jsonStr);
993
+ if (parsed && typeof parsed.content === "string") {
994
+ return parsed;
995
+ }
996
+ } catch {
997
+ }
998
+ return null;
999
+ };
1000
+ const direct = tryParse(text);
1001
+ if (direct) return direct;
1002
+ if (text.startsWith("\u6D4F\u89C8\u5668\u72B6\u6001: ")) {
1003
+ let jsonStr = text.substring("\u6D4F\u89C8\u5668\u72B6\u6001: ".length);
1004
+ const firstNewline = jsonStr.indexOf("\n");
1005
+ if (firstNewline !== -1) {
1006
+ const maybeJson = jsonStr.substring(0, firstNewline);
1007
+ if (tryParse(maybeJson)) jsonStr = maybeJson;
1008
+ }
1009
+ return tryParse(jsonStr);
1010
+ }
1011
+ return null;
1012
+ }
917
1013
  function cleanOldLogs(baseDir, logDir) {
918
1014
  try {
919
1015
  const oldLogFile = path2.join(baseDir, "webmcp-cli.log");
@@ -956,6 +1052,51 @@ function cleanOldLogs(baseDir, logDir) {
956
1052
  } catch (e) {
957
1053
  }
958
1054
  }
1055
+ function formatLogResult(result) {
1056
+ if (result === void 0 || result === null) {
1057
+ return "null";
1058
+ }
1059
+ try {
1060
+ if (typeof result !== "object") {
1061
+ return String(result);
1062
+ }
1063
+ const clone = JSON.parse(JSON.stringify(result));
1064
+ let a11yTreeStr = "";
1065
+ let searchResultStr = "";
1066
+ if (clone && Array.isArray(clone.content) && clone.content.length > 0 && clone.content[0] && typeof clone.content[0].text === "string") {
1067
+ const text = clone.content[0].text;
1068
+ const browserState = tryParseBrowserStateText(text);
1069
+ if (browserState) {
1070
+ a11yTreeStr = browserState.content;
1071
+ browserState.content = "[Formatted A11y Tree - See details below]";
1072
+ clone.content[0].text = JSON.stringify(browserState, null, 2);
1073
+ } else if (text.includes("A11y Tree \u641C\u7D22\u7ED3\u679C") || text.includes("\u65E0\u969C\u788D\u6811\u641C\u7D22\u7ED3\u679C") || text.includes("\u65E0\u969C\u788D\u6811\u641C\u7D22") || text.includes("\u547D\u4E2D\u884C") || text.includes("context:")) {
1074
+ searchResultStr = text;
1075
+ clone.content[0].text = "[Search A11y Tree Result - See details below]";
1076
+ }
1077
+ }
1078
+ let out = JSON.stringify(clone, null, 2);
1079
+ if (a11yTreeStr) {
1080
+ out += `
1081
+
1082
+ [FORMATTED A11Y TREE]:
1083
+ ${a11yTreeStr}`;
1084
+ }
1085
+ if (searchResultStr) {
1086
+ out += `
1087
+
1088
+ [FORMATTED SEARCH RESULT]:
1089
+ ${searchResultStr}`;
1090
+ }
1091
+ return out;
1092
+ } catch (e) {
1093
+ try {
1094
+ return JSON.stringify(result, null, 2);
1095
+ } catch {
1096
+ return String(result);
1097
+ }
1098
+ }
1099
+ }
959
1100
  function writeLog(commandName, args, result, error) {
960
1101
  try {
961
1102
  const baseDir = process.env.WEBMCP_WORKSPACE || path2.join(os2.homedir(), ".webmcp_chrome_profile");
@@ -981,15 +1122,8 @@ ${error instanceof Error ? error.stack : String(error)}
981
1122
  } else if (result) {
982
1123
  logText += `RESULT:
983
1124
  `;
984
- try {
985
- if (result && typeof result === "object") {
986
- logText += JSON.stringify(result, null, 2).split("\n").map((line) => ` ${line}`).join("\n") + "\n";
987
- } else {
988
- logText += String(result).split("\n").map((line) => ` ${line}`).join("\n") + "\n";
989
- }
990
- } catch {
991
- logText += JSON.stringify(result, null, 2).split("\n").map((line) => ` ${line}`).join("\n") + "\n";
992
- }
1125
+ const formattedResult = formatLogResult(result);
1126
+ logText += formattedResult.split("\n").map((line) => ` ${line}`).join("\n") + "\n";
993
1127
  } else {
994
1128
  logText += `RESULT: null
995
1129
  `;
@@ -1019,7 +1153,7 @@ program.command("state").description("\u83B7\u53D6\u6D4F\u89C8\u5668\u5F53\u524D
1019
1153
  try {
1020
1154
  const result = await stateCommand(args);
1021
1155
  writeLog("state", args, result);
1022
- console.log(JSON.stringify(result, null, 2));
1156
+ console.log(formatLogResult(result));
1023
1157
  } catch (error) {
1024
1158
  handleCommandError(error, "state", args);
1025
1159
  }
@@ -1053,7 +1187,7 @@ program.command("run <toolName> [args...]").description("\u5411\u6307\u5B9A\u987
1053
1187
  tabid: parseTabId(options.tabid)
1054
1188
  });
1055
1189
  writeLog("run", { ...rawArgs, finalArgsJson }, result);
1056
- console.log(JSON.stringify(result, null, 2));
1190
+ console.log(formatLogResult(result));
1057
1191
  } catch (error) {
1058
1192
  handleCommandError(error, "run", { ...rawArgs, finalArgsJson });
1059
1193
  }