UNPKG

playwright-mcp

Version:
1,166 lines (1,146 loc) 34.5 kB
#!/usr/bin/env node import { injectToolbox } from "./chunk-7XNUMIZP.js"; // src/server.ts import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; // src/mcp/index.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { chromium } from "playwright"; // src/mcp/eval.ts import vm from "vm"; var secureEvalAsync = async (page2, code, context2 = {}) => { const timeout = 2e4; const filename = "eval.js"; let logs = []; let errors = []; const wrappedCode = ` ${code} run(page); `; const sandbox = { // Core async essentials Promise, setTimeout, clearTimeout, setImmediate, clearImmediate, // Pass page object to sandbox page: page2, // Capture all console methods console: { log: (...args) => { const msg = args.map((arg) => String(arg)).join(" "); logs.push(`[log] ${msg}`); }, error: (...args) => { const msg = args.map((arg) => String(arg)).join(" "); errors.push(`[error] ${msg}`); }, warn: (...args) => { const msg = args.map((arg) => String(arg)).join(" "); logs.push(`[warn] ${msg}`); }, info: (...args) => { const msg = args.map((arg) => String(arg)).join(" "); logs.push(`[info] ${msg}`); }, debug: (...args) => { const msg = args.map((arg) => String(arg)).join(" "); logs.push(`[debug] ${msg}`); }, trace: (...args) => { const msg = args.map((arg) => String(arg)).join(" "); logs.push(`[trace] ${msg}`); } }, // User-provided context ...context2, // Explicitly block access to sensitive globals process: void 0, global: void 0, require: void 0, __dirname: void 0, __filename: void 0, Buffer: void 0 }; try { const vmContext = vm.createContext(sandbox); const script = new vm.Script(wrappedCode, { filename }); const result = script.runInContext(vmContext); const awaitedResult = await result; return { result: awaitedResult, logs, errors }; } catch (error) { return { error: true, message: error.message, stack: error.stack, logs, errors }; } }; // src/mcp/state.ts var globalState = { messages: [], pickingType: null, recordingInteractions: false, code: `async function run(page) { let title = await page.title(); return title }` }; async function initState(page2) { await page2.exposeFunction("updateGlobalState", (state) => { updateState(page2, state); }); await page2.exposeFunction("triggerSyncToReact", () => { updateState(page2, getState()); }); await page2.addInitScript((state) => { if (window.globalState) { return; } window.globalState = state; window.stateSubscribers = []; window.notifyStateSubscribers = () => { window.stateSubscribers.forEach((cb) => cb(window.globalState)); }; }, globalState); } async function syncToReact(page2, state) { const allFrames = await page2.frames(); const toolboxFrame = allFrames.find((f) => f.name() === "toolbox-frame"); if (!toolboxFrame) { console.error("Toolbox frame not found"); return; } try { await toolboxFrame.evaluate((state2) => { window.globalState = state2; window.notifyStateSubscribers(); }, state); } catch (error) { console.debug("Error syncing to React:", error); } } var getState = () => { return structuredClone(globalState); }; var updateState = (page2, state) => { globalState = structuredClone(state); syncToReact(page2, state); }; // src/mcp/recording/snowflake.ts import { Snowflake } from "@skorotkiewicz/snowflake-id"; var snowflake = new Snowflake(42 * 10); var getSnowflakeId = async () => { return await snowflake.generate(); }; // src/mcp/recording/init-recording.ts var initRecording = async (page2, onBrowserEvent) => { page2.addInitScript(() => { if (window.self !== window.top) { return; } function getDom() { const snapshot = document.documentElement.cloneNode(true); const originalElements = document.querySelectorAll("*"); const clonedElements = snapshot.querySelectorAll("*"); originalElements.forEach((originalElement, index) => { const clonedElement = clonedElements[index]; if (!clonedElement) return; if (originalElement.scrollLeft || originalElement.scrollTop) { if (originalElement.scrollLeft) { clonedElement.setAttribute( "qaby-data-scroll-left", originalElement.scrollLeft.toString() ); } if (originalElement.scrollTop) { clonedElement.setAttribute( "qaby-data-scroll-top", originalElement.scrollTop.toString() ); } } if (originalElement instanceof HTMLInputElement || originalElement instanceof HTMLTextAreaElement || originalElement instanceof HTMLSelectElement || originalElement.hasAttribute("contenteditable")) { preserveElementState(originalElement, clonedElement); } }); return snapshot.outerHTML; } function preserveElementState(original, cloned) { if (original.hasAttribute("contenteditable")) { const escapedHTML = original.innerHTML.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); cloned.setAttribute("qaby-data-contenteditable", escapedHTML); } if (original instanceof HTMLInputElement) { preserveInputState(original, cloned); } else if (original instanceof HTMLTextAreaElement) { preserveTextAreaState(original, cloned); } else if (original instanceof HTMLSelectElement) { preserveSelectState(original, cloned); } } function preserveInputState(original, cloned) { switch (original.type) { case "checkbox": case "radio": if (original.checked) { cloned.setAttribute("checked", ""); } else { cloned.removeAttribute("checked"); } if (original.indeterminate) { cloned.setAttribute("qaby-data-indeterminate", "true"); } break; case "range": cloned.setAttribute("value", original.value); break; case "date": case "datetime-local": case "month": case "time": case "week": if (original.valueAsDate) { cloned.setAttribute( "qaby-data-value-as-date", original.valueAsDate.toISOString() ); cloned.setAttribute("value", original.value); } break; default: cloned.setAttribute("value", original.value); } } function preserveTextAreaState(original, cloned) { cloned.innerHTML = original.value; } function preserveSelectState(original, cloned) { cloned.querySelectorAll("option").forEach((option) => { option.removeAttribute("selected"); }); if (original.multiple) { Array.from(original.selectedOptions).forEach((option) => { const optionIndex = Array.from(original.options).indexOf(option); const clonedOption = cloned.querySelector( `option:nth-child(${optionIndex + 1})` ); if (clonedOption) { clonedOption.setAttribute("selected", ""); } }); } else if (original.selectedIndex >= 0) { const clonedOption = cloned.querySelector( `option:nth-child(${original.selectedIndex + 1})` ); if (clonedOption) { clonedOption.setAttribute("selected", ""); } } } function generateUUID() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === "x" ? r : r & 3 | 8; return v.toString(16); }); } function addAttributesToNode(node) { if (node.nodeType === window.Node.ELEMENT_NODE) { const element = node; if (!element.getAttribute("uuid")) { element.setAttribute("uuid", generateUUID()); } for (const child of node.childNodes) { addAttributesToNode(child); } } } function removeAttributesFromNode(node) { if (node.nodeType === window.Node.ELEMENT_NODE) { const element = node; element.removeAttribute("uuid"); } } const recordedEvents = /* @__PURE__ */ new WeakMap(); function handleClick(e) { if (recordedEvents.get(e)) { return; } const target = e.target; if (!target) return; if (target.getAttribute("data-skip-recording")) return; e.stopPropagation(); recordedEvents.set(e, true); addAttributesToNode(document.documentElement); const elementUUID = target.getAttribute("uuid"); const dom = getDom(); window.recordDOM(dom, elementUUID).then(() => { target.dispatchEvent(e); }); removeAttributesFromNode(document.documentElement); } function handleKeyDown(event) { const dom = getDom(); if (["Enter", "Escape"].includes(event.key)) { window.recordKeyPress(dom, [event.key]); return; } if (document.activeElement?.tagName.toLowerCase() === "input") { if (event.key === "Tab") { window.recordKeyPress(dom, [event.key]); } return; } if (document.activeElement?.tagName.toLowerCase() === "textarea") { return; } const keys = []; if (event.ctrlKey) keys.push("Control"); if (event.shiftKey) keys.push("Shift"); if (event.altKey) keys.push("Alt"); if (event.metaKey) keys.push("Meta"); if (!["Control", "Shift", "Alt", "Meta"].includes(event.key)) { keys.push(event.key); } if (keys.includes("Meta") && keys.includes("Tab")) { return; } if (keys.length === 1 && keys[0] === "Meta") { return; } if (keys.length > 0) { window.recordKeyPress(dom, keys); } } function handleKeyUp(event) { if (["Enter", "Escape"].includes(event.key)) { return; } if (document.activeElement?.tagName.toLowerCase() !== "input" && document.activeElement?.tagName.toLowerCase() !== "textarea") { return; } const dom = getDom(); window.recordInput( dom, document.activeElement.getAttribute("uuid"), document.activeElement.value ); } window.addEventListener("click", handleClick, { capture: true }); window.addEventListener("keydown", handleKeyDown, { capture: true }); window.addEventListener("keyup", handleKeyUp, { capture: true }); console.log("Recording initialized for window:", window.location.href); }); let buttonClicked = false; await page2.exposeFunction( "recordDOM", async (dom, elementUUID) => { buttonClicked = true; const event = { eventId: await getSnowflakeId(), type: "click" /* Click */, dom, elementUUID, selectors: [`[uuid="${elementUUID}"]`], windowUrl: page2.url() }; onBrowserEvent(event); } ); await page2.exposeFunction( "recordInput", async (dom, elementUUID, value) => { const event = { eventId: await getSnowflakeId(), type: "input" /* Input */, dom, elementUUID, typedText: value, selectors: [`[uuid="${elementUUID}"]`], windowUrl: page2.url() }; onBrowserEvent(event); } ); await page2.exposeFunction( "recordKeyPress", async (dom, keys) => { const event = { eventId: await getSnowflakeId(), type: "key-press" /* KeyPress */, keys, dom, windowUrl: page2.url() }; onBrowserEvent(event); } ); page2.on("load", async () => { if (!buttonClicked) { const event = { eventId: await getSnowflakeId(), type: "open-page" /* OpenPage */, windowUrl: page2.url(), // TODO: Fix navigation handling // title: await page.title(), title: "", // TODO: Fix dom content here dom: "" }; onBrowserEvent(event); } buttonClicked = false; }); }; // src/mcp/logger.ts var Logger = class { level; constructor(level = "info") { this.level = level; } shouldLog(messageLevel) { const levels = ["debug", "info", "warn", "error"]; return levels.indexOf(messageLevel) >= levels.indexOf(this.level); } log(level, ...args) { if (this.shouldLog(level)) { console[level](...args); } } debug(...args) { this.log("debug", ...args); } info(...args) { this.log("info", ...args); } warn(...args) { this.log("warn", ...args); } error(...args) { this.log("error", ...args); } }; var logger = new Logger(); // src/mcp/recording/selector-engine.ts var ATTR_PRIORITIES = { id: 1, "data-testid": 2, "data-test-id": 2, "data-pw": 2, "data-cy": 2, "data-id": 2, "data-name": 3, name: 3, "aria-label": 3, title: 3, placeholder: 4, href: 4, alt: 4, "data-index": 5, "data-role": 5, role: 5 }; var IMPORTANT_ATTRS = Object.keys(ATTR_PRIORITIES); var _escapeSpecialCharacters = (str) => { return str.replace(/"/g, '\\"'); }; var getNodeSimpleSelectors = (element) => { const selectors = []; const tag = element.tagName.toLowerCase(); const attrSelectors = IMPORTANT_ATTRS.map((attr) => { const value = element.getAttribute(attr); if (!value) return null; return { priority: ATTR_PRIORITIES[attr] || 999, selector: attr === "id" ? `#${_escapeSpecialCharacters(value)}` : `${tag}[${attr}="${_escapeSpecialCharacters(value)}"]` }; }).filter((item) => item !== null); const otherSelectors = []; const classList = element.classList; if (classList.length > 0) { otherSelectors.push({ priority: 100, selector: `${tag}.${Array.from(classList).join(".")}` }); } const availableSelectors = [...attrSelectors, ...otherSelectors]; availableSelectors.sort((a, b) => a.priority - b.priority); const topSelectors = availableSelectors.slice(0, 5); topSelectors.push({ priority: 999, selector: tag }); for (const item of topSelectors) { selectors.push(item.selector); } return selectors; }; var _getSiblingRelationshipSelectors = (dom, element) => { const selectors = []; const parent = element.parentElement; if (!parent || parent.tagName === "BODY") { return selectors; } const siblings = Array.from(parent.children); const elementIndex = siblings.indexOf(element); const tagName = element.tagName.toLowerCase(); const selectorPrefixes = []; for (let i = 0; i < siblings.length; i++) { if (i === elementIndex) continue; const sibling = siblings[i]; const siblingSimpleSelectors = getNodeSimpleSelectors(sibling); siblingSimpleSelectors.forEach((siblingSelector) => { selectorPrefixes.push(`${siblingSelector} ~ `); }); } const selectorSuffixes = [tagName, ...getNodeSimpleSelectors(element)]; selectorSuffixes.forEach((selectorSuffix) => { selectorPrefixes.forEach((selectorPrefix) => { selectors.push(`${selectorPrefix}${selectorSuffix}`); }); }); return selectors; }; var _getChildRelationshipSelectors = (dom, element) => { const children = []; const currentQueue = Array.from(element.children).map((child) => ({ child, depth: 0 })); while (currentQueue.length > 0) { const item = currentQueue.shift(); if (!item) continue; const { child, depth } = item; if (depth > 3) { continue; } children.push({ child, depth }); currentQueue.push( ...Array.from(child.children).map((child2) => ({ child: child2, depth: depth + 1 })) ); } const selectorSuffixes = []; children.forEach(({ child, depth }) => { const childSelectors = getNodeSimpleSelectors(child); const childIndex = Array.from(element.children).indexOf(child) + 1; childSelectors.forEach((childSelector) => { if (depth === 0) { selectorSuffixes.push(`:has(${childSelector})`); selectorSuffixes.push(`:has(${childSelector}:nth-child(${childIndex}))`); } else { selectorSuffixes.push(`:has(${childSelector})`); } }); }); const selectorPrefixes = [ element.tagName.toLowerCase(), ...getNodeSimpleSelectors(element) ]; const selectors = []; selectorPrefixes.forEach((selectorPrefix) => { selectorSuffixes.forEach((selectorSuffix) => { selectors.push(`${selectorPrefix}${selectorSuffix}`); }); }); return selectors; }; var getMatchCount = (dom, selector) => { try { return dom.querySelectorAll(selector).length; } catch { return Number.POSITIVE_INFINITY; } }; var _getParentPathSelectors = (dom, element) => { const path2 = []; let current = element; while (current && current.tagName !== "HTML") { path2.push(current); current = current.parentElement; } logger.debug( "Path", path2.map((node) => node.tagName) ); const nodeSelectors = path2.map((node) => ({ node, selectors: getNodeSimpleSelectors(node) })); if (!nodeSelectors.length) { return []; } const result = []; const targetNode = nodeSelectors[0].node; const targetSelectors = nodeSelectors[0].selectors; const targetSelectorsWithNthChild = targetSelectors.map((selector) => { const index = targetNode.parentElement ? Array.from(targetNode.parentElement.children).indexOf(targetNode) + 1 : 1; return `${selector}:nth-child(${index})`; }); const allTargetSelectors = [ ...targetSelectors, ...targetSelectorsWithNthChild ]; logger.debug("Target Selectors", allTargetSelectors); for (const targetSelector of allTargetSelectors) { const matches = getMatchCount(dom, targetSelector); if (matches === 0) continue; if (matches === 1) { result.push(targetSelector); } let currentSelector = targetSelector; let currentMatches = matches; let lastAddedNode = targetNode; for (let i = 1; i < nodeSelectors.length; i++) { const ancestor = nodeSelectors[i].node; const ancestorSelectors = nodeSelectors[i].selectors; let bestSelector = null; let bestMatches = currentMatches; for (const ancestorSelector of ancestorSelectors) { const descendantOperator = Array.from(ancestor.children).indexOf(lastAddedNode) !== -1 ? " > " : " "; const possibleCombinedSelectors = [ `${ancestorSelector} ${descendantOperator} ${currentSelector}` ]; if (ancestor.tagName != "BODY" && ancestor.parentElement) { const elementIndex = Array.from(ancestor.parentElement.children).indexOf(ancestor) + 1; possibleCombinedSelectors.push( `${ancestorSelector}:nth-child(${elementIndex}) ${descendantOperator} ${currentSelector}` ); } logger.debug("Possible Combined Selectors", possibleCombinedSelectors); for (const combinedSelector of possibleCombinedSelectors) { const newMatches = getMatchCount(dom, combinedSelector); if (newMatches === 0) continue; else if (newMatches === 1) { result.push(combinedSelector); bestSelector = null; } else if (newMatches < bestMatches) { bestSelector = combinedSelector; bestMatches = newMatches; } } } if (bestSelector && bestMatches < currentMatches) { currentSelector = bestSelector; currentMatches = bestMatches; lastAddedNode = ancestor; } } } return result; }; var validateSelector = (document2, element, selector) => { try { const selectedElements = document2.querySelectorAll(selector); return selectedElements.length === 1 && selectedElements[0] === element; } catch (e) { return false; } }; var getSelectors = (document2, elementUUID) => { const element = document2.querySelector(`[uuid="${elementUUID}"]`); if (!element) { throw new Error(`Element with UUID ${elementUUID} not found`); } const validSelectors = []; const selectorGenerators = [ () => _getParentPathSelectors(document2, element), () => _getChildRelationshipSelectors(document2, element), () => _getSiblingRelationshipSelectors(document2, element) ]; for (const generator of selectorGenerators) { const selectors = generator(); for (const selector of selectors) { if (validateSelector(document2, element, selector)) { validSelectors.push(selector); if (validSelectors.length >= 10) { return validSelectors; } } } } return validSelectors; }; // src/mcp/recording/utils.ts import { Window } from "happy-dom"; var parseDom = (html) => { const window2 = new Window({ settings: { disableJavaScriptEvaluation: true } }); window2.document.write(html); return window2.document; }; var preprocessBrowserEvent = (event) => { if (event.type === "click" /* Click */ || event.type === "input" /* Input */) { const dom = parseDom(event.dom); event.selectors = getSelectors(dom, event.elementUUID); const element = dom.querySelector(`[uuid="${event.elementUUID}"]`); event.elementName = element ? getElementName(element) : "unknown"; event.elementType = element ? getElementType(element) : "unknown"; event.dom = ""; } }; var extractText = (element) => { if (element.childNodes.length === 0) { return element.textContent?.trim() || ""; } const texts = Array.from(element.childNodes).map( (node) => extractText(node) ); return texts.filter((text) => text.trim().length > 0).map((text) => text.trim()).join("\n"); }; var extractTextsFromSiblings = (element) => { const siblings = Array.from(element.parentElement?.childNodes || []); return siblings.map((sibling) => extractText(sibling)).map((text) => text.trim()).filter((text) => text.length > 0); }; var getElementName = (element) => { let text = ""; const priorityAttrs = ["aria-label", "title", "placeholder", "name", "alt"]; for (const attr of priorityAttrs) { if (!text) { text = element?.getAttribute(attr) || ""; } } if (!text) { text = extractText(element); } if (!text) { text = extractTextsFromSiblings(element).join("\n"); } if (!text) { text = "unknown"; } return text; }; var getElementType = (element) => { const tagName = element?.tagName.toLowerCase(); let elementType = "element"; if (tagName === "a") { elementType = "link"; } else if (tagName === "button") { elementType = "button"; } else if (tagName === "textarea") { elementType = "textarea"; } else if (tagName === "input") { elementType = "input"; } return elementType; }; // src/mcp/handle-browser-event.ts import _ from "lodash"; var handleBrowserEvent = (page2) => { const eventQueue = []; const processEvents = _.debounce(() => { if (eventQueue.length === 0) { return; } while (eventQueue.length > 1) { const currentEvent = eventQueue[0]; const nextEvent = eventQueue[1]; if (currentEvent.type === nextEvent.type && currentEvent.elementUUID === nextEvent.elementUUID) { eventQueue.shift(); } else { break; } } const event = eventQueue.shift(); const state = getState(); preprocessBrowserEvent(event); if (state.messages.length > 0) { const lastMessage = state.messages[state.messages.length - 1]; if (lastMessage.type === "Interaction") { const lastInteraction = JSON.parse(lastMessage.content); if (lastInteraction.type === "input" && lastInteraction.elementUUID === event.elementUUID) { lastInteraction.typedText = event.typedText; state.messages[state.messages.length - 1] = { type: "Interaction", content: JSON.stringify(lastInteraction), windowUrl: event.windowUrl }; updateState(page2, state); return; } } } state.messages.push({ type: "Interaction", content: JSON.stringify(event), windowUrl: event.windowUrl }); updateState(page2, state); }, 100, { maxWait: 500 }); return (event) => { const state = getState(); if (!state.recordingInteractions || state.pickingType) { return; } eventQueue.push(event); processEvents(); }; }; // src/mcp/index.ts var browser; var context; var page; var server = new McpServer({ name: "playwright", version: "1.0.0" }); server.prompt( "server-flow", "Get prompt on how to use this MCP server", () => { return { messages: [ { role: "user", content: { type: "text", text: `# DON'T ASSUME ANYTHING. Whatever you write in code, it must be found in the context. Otherwise leave comments. ## Goal Help me write playwright code with following functionalities: - [[add semi-high level functionality you want here]] - [[more]] - [[more]] - [[more]] ## Reference - Use @x, @y files if you want to take reference on how I write POM code ## Steps - First fetch the context from 'get-context' tool, until it returns no elements remaining - Based on context and user functionality, write code in POM format, encapsulating high level functionality into reusable functions - Try executing code using 'execute-code' tool. You could be on any page, so make sure to navigate to the correct page - Write spec file using those reusable functions, covering multiple scenarios ` } } ] }; } ); server.tool( "init-browser", "Initialize a browser with a URL", { url: z.string().url().describe("The URL to navigate to") }, async ({ url: url2 }) => { if (context) { await context.close(); } if (browser) { await browser.close(); } browser = await chromium.launch({ headless: false }); context = await browser.newContext({ viewport: null, userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", bypassCSP: true }); page = await context.newPage(); await page.exposeFunction("triggerMcpStartPicking", (pickingType) => { page.evaluate((pickingType2) => { window.mcpStartPicking(pickingType2); }, pickingType); }); await page.exposeFunction("triggerMcpStopPicking", () => { page.evaluate(() => { window.mcpStopPicking(); }); }); await page.exposeFunction("onElementPicked", (message) => { const state = getState(); state.messages.push(message); state.pickingType = null; updateState(page, state); }); await page.exposeFunction("takeScreenshot", async (selector) => { try { const screenshot = await page.locator(selector).screenshot({ timeout: 5e3 }); return screenshot.toString("base64"); } catch (error) { console.error("Error taking screenshot", error); return null; } }); await page.exposeFunction("executeCode", async (code) => { const result = await secureEvalAsync(page, code); return result; }); await initState(page); await initRecording(page, handleBrowserEvent(page)); await page.addInitScript(injectToolbox); await page.goto(url2); return { content: [ { type: "text", text: `Browser has been initialized and navigated to ${url2}` } ] }; } ); server.tool( "get-full-dom", "Get the full DOM of the current page. (Deprecated, use get-context instead)", {}, async () => { const html = await page.content(); return { content: [ { type: "text", text: html } ] }; } ); server.tool( "get-screenshot", "Get a screenshot of the current page", {}, async () => { const screenshot = await page.screenshot({ type: "png" }); return { content: [ { type: "image", data: screenshot.toString("base64"), mimeType: "image/png" } ] }; } ); server.tool( "execute-code", "Execute custom Playwright JS code against the current page", { code: z.string().describe(`The Playwright code to execute. Must be an async function declaration that takes a page parameter. Example: async function run(page) { console.log(await page.title()); return await page.title(); } Returns an object with: - result: The return value from your function - logs: Array of console logs from execution - errors: Array of any errors encountered Example response: {"result": "Google", "logs": ["[log] Google"], "errors": []}`) }, async ({ code }) => { const result = await secureEvalAsync(page, code); return { content: [ { type: "text", text: JSON.stringify(result, null, 2) // Pretty print the JSON } ] }; } ); server.tool( "get-context", "Get the website context which would be used to write the testcase", {}, async () => { const state = getState(); if (state.messages.length === 0) { return { content: [ { type: "text", text: `No messages available` } ] }; } const content = []; let totalLength = 0; let messagesProcessed = 0; while (messagesProcessed < state.messages.length && totalLength < 2e4) { const message = state.messages[messagesProcessed]; let currentContent = message.content; if (message.type === "DOM") { currentContent = `DOM: ${message.content}`; } else if (message.type === "Text") { currentContent = `Text: ${message.content}`; } else if (message.type === "Interaction") { const interaction = JSON.parse(message.content); delete interaction.eventId; delete interaction.dom; delete interaction.elementUUID; if (interaction.selectors) { interaction.selectors = interaction.selectors.slice(0, 10); } currentContent = JSON.stringify(interaction); } else if (message.type === "Image") { currentContent = message.content; } totalLength += currentContent.length; const item = {}; const isImage = message.type === "Image"; if (isImage) { item.type = "image"; item.data = message.content; item.mimeType = "image/png"; } else { item.type = "text"; item.text = currentContent; } content.push(item); messagesProcessed++; } state.messages.splice(0, messagesProcessed); updateState(page, state); const remainingCount = state.messages.length; if (remainingCount > 0) { content.push({ type: "text", text: `Remaining ${remainingCount} messages, please fetch those in next requests. ` }); } return { content }; } ); // src/web-server.ts import http from "http"; import fs from "fs"; import path from "path"; import url, { fileURLToPath } from "url"; import { dirname } from "path"; import net from "net"; var __filename = fileURLToPath(import.meta.url); var __dirname = dirname(__filename); var SERVE_DIR = path.join(__dirname, "ui"); async function isPortInUse(port) { return new Promise((resolve) => { const tester = net.createServer().once("error", () => resolve(true)).once("listening", () => { tester.once("close", () => resolve(false)); tester.close(); }).listen(port); }); } var server2 = http.createServer((req, res) => { const parsedUrl = url.parse(req.url || ""); const pathname = parsedUrl.pathname || "/"; let filePath = path.join(SERVE_DIR, pathname); if (!filePath.startsWith(SERVE_DIR)) { res.writeHead(403, { "Content-Type": "text/plain" }); res.end("403 Forbidden: Access denied"); return; } if (pathname.endsWith("/")) { filePath = path.join(filePath, "index.html"); } fs.stat(filePath, (err, stats) => { if (err) { res.writeHead(404, { "Content-Type": "text/plain" }); res.end("404 Not Found"); return; } if (stats.isDirectory()) { filePath = path.join(filePath, "index.html"); fs.stat(filePath, (err2, stats2) => { if (err2) { res.writeHead(404, { "Content-Type": "text/plain" }); res.end("404 Not Found"); return; } serveFile(filePath, res); }); } else { serveFile(filePath, res); } }); }); function serveFile(filePath, res) { const ext = path.extname(filePath); let contentType = "text/plain"; switch (ext) { case ".html": contentType = "text/html"; break; case ".css": contentType = "text/css"; break; case ".js": contentType = "application/javascript"; break; case ".json": contentType = "application/json"; break; case ".png": contentType = "image/png"; break; case ".jpg": case ".jpeg": contentType = "image/jpeg"; break; case ".gif": contentType = "image/gif"; break; case ".svg": contentType = "image/svg+xml"; break; case ".pdf": contentType = "application/pdf"; break; } fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(500, { "Content-Type": "text/plain" }); res.end("Internal Server Error"); return; } res.writeHead(200, { "Content-Type": contentType }); res.end(data); }); } // src/server.ts async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Server started"); if (true) { const portInUse = await isPortInUse(5174); if (!portInUse) { server2.listen(5174, () => { console.error("Web server started"); }); } else { console.error("Port 5174 is in use, skipping web server"); } } } main().catch((error) => { console.error("Fatal error in main", error); process.exit(1); });