context.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. "use strict";
  2. var __create = Object.create;
  3. var __defProp = Object.defineProperty;
  4. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  5. var __getOwnPropNames = Object.getOwnPropertyNames;
  6. var __getProtoOf = Object.getPrototypeOf;
  7. var __hasOwnProp = Object.prototype.hasOwnProperty;
  8. var __export = (target, all) => {
  9. for (var name in all)
  10. __defProp(target, name, { get: all[name], enumerable: true });
  11. };
  12. var __copyProps = (to, from, except, desc) => {
  13. if (from && typeof from === "object" || typeof from === "function") {
  14. for (let key of __getOwnPropNames(from))
  15. if (!__hasOwnProp.call(to, key) && key !== except)
  16. __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  17. }
  18. return to;
  19. };
  20. var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  21. // If the importer is in node compatibility mode or this is not an ESM
  22. // file that has been converted to a CommonJS file using a Babel-
  23. // compatible transform (i.e. "__esModule" has not been set), then set
  24. // "default" to the CommonJS "module.exports" for node compatibility.
  25. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  26. mod
  27. ));
  28. var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
  29. var context_exports = {};
  30. __export(context_exports, {
  31. Context: () => Context,
  32. InputRecorder: () => InputRecorder
  33. });
  34. module.exports = __toCommonJS(context_exports);
  35. var import_fs = __toESM(require("fs"));
  36. var import_path = __toESM(require("path"));
  37. var import_utilsBundle = require("playwright-core/lib/utilsBundle");
  38. var import_playwright_core = require("playwright-core");
  39. var import_log = require("../log");
  40. var import_tab = require("./tab");
  41. var import_config = require("./config");
  42. var codegen = __toESM(require("./codegen"));
  43. var import_utils = require("./tools/utils");
  44. const testDebug = (0, import_utilsBundle.debug)("pw:mcp:test");
  45. class Context {
  46. constructor(options) {
  47. this._tabs = [];
  48. this._abortController = new AbortController();
  49. this.config = options.config;
  50. this.sessionLog = options.sessionLog;
  51. this.options = options;
  52. this._browserContextFactory = options.browserContextFactory;
  53. this._clientInfo = options.clientInfo;
  54. testDebug("create context");
  55. Context._allContexts.add(this);
  56. }
  57. static {
  58. this._allContexts = /* @__PURE__ */ new Set();
  59. }
  60. static async disposeAll() {
  61. await Promise.all([...Context._allContexts].map((context) => context.dispose()));
  62. }
  63. tabs() {
  64. return this._tabs;
  65. }
  66. currentTab() {
  67. return this._currentTab;
  68. }
  69. currentTabOrDie() {
  70. if (!this._currentTab)
  71. throw new Error('No open pages available. Use the "browser_navigate" tool to navigate to a page first.');
  72. return this._currentTab;
  73. }
  74. async newTab() {
  75. const { browserContext } = await this._ensureBrowserContext();
  76. const page = await browserContext.newPage();
  77. this._currentTab = this._tabs.find((t) => t.page === page);
  78. await this._currentTab.initializedPromise;
  79. return this._currentTab;
  80. }
  81. async selectTab(index) {
  82. const tab = this._tabs[index];
  83. if (!tab)
  84. throw new Error(`Tab ${index} not found`);
  85. await tab.page.bringToFront();
  86. this._currentTab = tab;
  87. return tab;
  88. }
  89. async ensureTab() {
  90. const { browserContext } = await this._ensureBrowserContext();
  91. if (!this._currentTab)
  92. await browserContext.newPage();
  93. return this._currentTab;
  94. }
  95. async closeTab(index) {
  96. const tab = index === void 0 ? this._currentTab : this._tabs[index];
  97. if (!tab)
  98. throw new Error(`Tab ${index} not found`);
  99. const url = tab.page.url();
  100. await tab.page.close();
  101. return url;
  102. }
  103. async outputFile(fileName, options) {
  104. return (0, import_config.outputFile)(this.config, this._clientInfo, fileName, options);
  105. }
  106. _onPageCreated(page) {
  107. const tab = new import_tab.Tab(this, page, (tab2) => this._onPageClosed(tab2));
  108. this._tabs.push(tab);
  109. if (!this._currentTab)
  110. this._currentTab = tab;
  111. }
  112. _onPageClosed(tab) {
  113. const index = this._tabs.indexOf(tab);
  114. if (index === -1)
  115. return;
  116. this._tabs.splice(index, 1);
  117. if (this._currentTab === tab)
  118. this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
  119. if (!this._tabs.length)
  120. void this.closeBrowserContext();
  121. }
  122. async closeBrowserContext() {
  123. if (!this._closeBrowserContextPromise)
  124. this._closeBrowserContextPromise = this._closeBrowserContextImpl().catch(import_log.logUnhandledError);
  125. await this._closeBrowserContextPromise;
  126. this._closeBrowserContextPromise = void 0;
  127. }
  128. isRunningTool() {
  129. return this._runningToolName !== void 0;
  130. }
  131. setRunningTool(name) {
  132. this._runningToolName = name;
  133. }
  134. async _closeBrowserContextImpl() {
  135. if (!this._browserContextPromise)
  136. return;
  137. testDebug("close context");
  138. const promise = this._browserContextPromise;
  139. this._browserContextPromise = void 0;
  140. await promise.then(async ({ browserContext, close }) => {
  141. if (this.config.saveTrace)
  142. await browserContext.tracing.stop();
  143. const videos = this.config.saveVideo ? browserContext.pages().map((page) => page.video()).filter((video) => !!video) : [];
  144. await close(async () => {
  145. for (const video of videos) {
  146. const name = await this.outputFile((0, import_utils.dateAsFileName)("webm"), { origin: "code", reason: "Saving video" });
  147. await import_fs.default.promises.mkdir(import_path.default.dirname(name), { recursive: true });
  148. const p = await video.path();
  149. if (import_fs.default.existsSync(p)) {
  150. try {
  151. await import_fs.default.promises.rename(p, name);
  152. } catch (e) {
  153. if (e.code !== "EXDEV")
  154. (0, import_log.logUnhandledError)(e);
  155. try {
  156. await import_fs.default.promises.copyFile(p, name);
  157. await import_fs.default.promises.unlink(p);
  158. } catch (e2) {
  159. (0, import_log.logUnhandledError)(e2);
  160. }
  161. }
  162. }
  163. }
  164. });
  165. });
  166. }
  167. async dispose() {
  168. this._abortController.abort("MCP context disposed");
  169. await this.closeBrowserContext();
  170. Context._allContexts.delete(this);
  171. }
  172. async ensureBrowserContext() {
  173. const { browserContext } = await this._ensureBrowserContext();
  174. return browserContext;
  175. }
  176. _ensureBrowserContext() {
  177. if (!this._browserContextPromise) {
  178. this._browserContextPromise = this._setupBrowserContext();
  179. this._browserContextPromise.catch(() => {
  180. this._browserContextPromise = void 0;
  181. });
  182. }
  183. return this._browserContextPromise;
  184. }
  185. async _setupBrowserContext() {
  186. if (this._closeBrowserContextPromise)
  187. throw new Error("Another browser context is being closed.");
  188. if (this.config.testIdAttribute)
  189. import_playwright_core.selectors.setTestIdAttribute(this.config.testIdAttribute);
  190. const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, this._runningToolName);
  191. const { browserContext } = result;
  192. if (this.sessionLog)
  193. await InputRecorder.create(this, browserContext);
  194. for (const page of browserContext.pages())
  195. this._onPageCreated(page);
  196. browserContext.on("page", (page) => this._onPageCreated(page));
  197. if (this.config.saveTrace) {
  198. await browserContext.tracing.start({
  199. name: "trace-" + Date.now(),
  200. screenshots: true,
  201. snapshots: true,
  202. _live: true
  203. });
  204. }
  205. return result;
  206. }
  207. lookupSecret(secretName) {
  208. if (!this.config.secrets?.[secretName])
  209. return { value: secretName, code: codegen.quote(secretName) };
  210. return {
  211. value: this.config.secrets[secretName],
  212. code: `process.env['${secretName}']`
  213. };
  214. }
  215. }
  216. class InputRecorder {
  217. constructor(context, browserContext) {
  218. this._context = context;
  219. this._browserContext = browserContext;
  220. }
  221. static async create(context, browserContext) {
  222. const recorder = new InputRecorder(context, browserContext);
  223. await recorder._initialize();
  224. return recorder;
  225. }
  226. async _initialize() {
  227. const sessionLog = this._context.sessionLog;
  228. await this._browserContext._enableRecorder({
  229. mode: "recording",
  230. recorderMode: "api"
  231. }, {
  232. actionAdded: (page, data, code) => {
  233. if (this._context.isRunningTool())
  234. return;
  235. const tab = import_tab.Tab.forPage(page);
  236. if (tab)
  237. sessionLog.logUserAction(data.action, tab, code, false);
  238. },
  239. actionUpdated: (page, data, code) => {
  240. if (this._context.isRunningTool())
  241. return;
  242. const tab = import_tab.Tab.forPage(page);
  243. if (tab)
  244. sessionLog.logUserAction(data.action, tab, code, true);
  245. },
  246. signalAdded: (page, data) => {
  247. if (this._context.isRunningTool())
  248. return;
  249. if (data.signal.name !== "navigation")
  250. return;
  251. const tab = import_tab.Tab.forPage(page);
  252. const navigateAction = {
  253. name: "navigate",
  254. url: data.signal.url,
  255. signals: []
  256. };
  257. if (tab)
  258. sessionLog.logUserAction(navigateAction, tab, `await page.goto('${data.signal.url}');`, false);
  259. }
  260. });
  261. }
  262. }
  263. // Annotate the CommonJS export names for ESM import in node:
  264. 0 && (module.exports = {
  265. Context,
  266. InputRecorder
  267. });