diff --git a/.gitignore b/.gitignore index 550f500..a31fe64 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ dist/ downloads/ eggs/ .eggs/ -lib/ +/lib/ lib64/ parts/ sdist/ diff --git a/AGENTS.md b/AGENTS.md index c747e21..7fd3594 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,16 +5,17 @@ MCP server for Selenium WebDriver browser automation. JavaScript (ES Modules), N ## File Map ```text -src/lib/server.js ← ALL server logic: tool definitions, state, helpers, cleanup -src/index.js ← Thin CLI wrapper, spawns server.js as child process -test/mcp-client.mjs ← Reusable MCP test client (JSON-RPC over stdio) -test/*.test.mjs ← Tests grouped by feature -test/fixtures/*.html ← HTML files loaded via file:// URLs in tests +src/lib/server.js ← ALL server logic: tool definitions, state, helpers, cleanup +src/lib/accessibility-snapshot.js ← Browser-side JS injected via executeScript to build accessibility tree +bin/mcp-selenium.js ← CLI entry point, spawns server.js as child process +test/mcp-client.mjs ← Reusable MCP test client (JSON-RPC over stdio) +test/*.test.mjs ← Tests grouped by feature +test/fixtures/*.html ← HTML files loaded via file:// URLs in tests ``` ## Architecture -**Single-file server** — everything is in `server.js`. 18 tools, 1 resource. +Server logic lives in `server.js`, with browser-injected scripts in separate files. 18 tools, 2 resources. State is a module-level object: ```js @@ -80,3 +81,4 @@ Tests talk to the real MCP server over stdio. No mocking. Each test file uses ** | `tools.test.mjs` | get_element_attribute, execute_script, window, frame, alert | | `cookies.test.mjs` | add_cookie, get_cookies, delete_cookie | | `bidi.test.mjs` | diagnostics (console/errors/network), session isolation | +| `resources.test.mjs` | accessibility-snapshot resource (tree structure, filtering, no-session error) | diff --git a/README.md b/README.md index 018d93a..8ff7695 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,29 @@ Gets browser diagnostics captured via WebDriver BiDi (auto-enabled when supporte +
+Resources + +MCP resources provide read-only data that clients can access without calling a tool. + +### browser-status://current +Returns the current browser session status (active session ID or "no active session"). + +| Property | Value | +|----------|-------| +| MIME type | `text/plain` | +| Requires browser | No | + +### accessibility://current +Returns an accessibility tree snapshot of the current page — a compact, structured JSON representation of interactive elements and text content. Much smaller than full HTML. Useful for understanding page layout and finding elements to interact with. + +| Property | Value | +|----------|-------| +| MIME type | `application/json` | +| Requires browser | Yes | + +
+ ---
diff --git a/package-lock.json b/package-lock.json index ee3dc75..b66a126 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.0", "license": "ISC", "dependencies": { - "@modelcontextprotocol/sdk": "^1.7.0", + "@modelcontextprotocol/sdk": "^1.26.0", "selenium-webdriver": "^4.18.1" }, "bin": { @@ -22,24 +22,56 @@ "integrity": "sha512-1uLNT5NZsUVIGS4syuHwTzZ8HycMPyr6POA3FCE4GbMtc4rhoJk8aZKtNIRthJYfL+iioppi+rTfH3olMPr9nA==", "license": "Apache-2.0" }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.7.0.tgz", - "integrity": "sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "license": "MIT", "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", + "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^4.1.0", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } } }, "node_modules/accepts": { @@ -55,62 +87,61 @@ "node": ">= 0.6" } }, - "node_modules/body-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.1.0.tgz", - "integrity": "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==", + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.5.2", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "engines": { - "node": ">=18" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "ajv": "^8.0.0" }, - "engines": { - "node": ">=6.0" + "peerDependencies": { + "ajv": "^8.0.0" }, "peerDependenciesMeta": { - "supports-color": { + "ajv": { "optional": true } } }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", "dependencies": { - "side-channel": "^1.1.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { - "node": ">=0.6" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bytes": { @@ -152,15 +183,16 @@ } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -173,9 +205,9 @@ } }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -209,13 +241,27 @@ "node": ">= 0.10" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -235,16 +281,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -341,54 +377,57 @@ } }, "node_modules/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", - "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "peer": true, "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.0.1", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", - "content-type": "~1.0.4", - "cookie": "0.7.1", + "content-type": "^1.0.5", + "cookie": "^0.7.1", "cookie-signature": "^1.2.1", - "debug": "4.3.6", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "^2.0.0", - "fresh": "2.0.0", - "http-errors": "2.0.0", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", - "methods": "~1.1.2", "mime-types": "^3.0.0", - "on-finished": "2.4.1", - "once": "1.4.0", - "parseurl": "~1.3.3", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "router": "^2.0.0", - "safe-buffer": "5.2.1", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", "send": "^1.1.0", - "serve-static": "^2.1.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "^2.0.0", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, "engines": { "node": ">= 16" }, @@ -396,13 +435,35 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" + "express": ">= 4.11" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -413,32 +474,13 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" + "node": ">= 18.0.0" }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -539,32 +581,50 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.1.tgz", + "integrity": "sha512-hi9afu8g0lfJVLolxElAZGANCTTl6bewIdsRNhaywfP9K8BPf++F2z6OLrYGIinUwpRKzbZHMhPwvc0ZEpAwGw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/iconv-lite": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", - "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/immediate": { @@ -579,6 +639,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -600,6 +669,33 @@ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -651,15 +747,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -670,21 +757,25 @@ } }, "node_modules/mime-types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", - "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { - "mime-db": "^1.53.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/negotiator": { @@ -753,19 +844,29 @@ "node": ">= 0.8" } }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { - "node": ">=16" + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/pkce-challenge": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", - "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -791,12 +892,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -815,30 +916,18 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, "node_modules/readable-stream": { @@ -862,12 +951,23 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/router": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.1.0.tgz", - "integrity": "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" @@ -876,26 +976,6 @@ "node": ">= 18" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -928,77 +1008,48 @@ } }, "node_modules/send": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", - "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { - "debug": "^4.3.5", - "destroy": "^1.2.0", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", - "fresh": "^0.5.2", - "http-errors": "^2.0.0", - "mime-types": "^2.1.35", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" - } - }, - "node_modules/send/node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/serve-static": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", - "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", - "send": "^1.0.0" + "send": "^1.2.0" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setimmediate": { @@ -1013,6 +1064,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1086,9 +1158,9 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -1128,9 +1200,9 @@ } }, "node_modules/type-is": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", - "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -1156,15 +1228,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1174,6 +1237,21 @@ "node": ">= 0.8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1202,9 +1280,9 @@ } }, "node_modules/zod": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "peer": true, "funding": { @@ -1212,12 +1290,12 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "license": "ISC", "peerDependencies": { - "zod": "^3.24.1" + "zod": "^3.25 || ^4" } } } diff --git a/package.json b/package.json index 19d5365..2ac81a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angiejones/mcp-selenium", - "version": "0.2.0", + "version": "0.2.1", "description": "Selenium WebDriver MCP Server", "type": "module", "main": "src/lib/server.js", @@ -14,7 +14,7 @@ "author": "", "license": "ISC", "dependencies": { - "@modelcontextprotocol/sdk": "^1.7.0", + "@modelcontextprotocol/sdk": "^1.26.0", "selenium-webdriver": "^4.18.1" } } diff --git a/src/lib/accessibility-snapshot.js b/src/lib/accessibility-snapshot.js new file mode 100644 index 0000000..e4c1316 --- /dev/null +++ b/src/lib/accessibility-snapshot.js @@ -0,0 +1,69 @@ +// Browser-side script — walks DOM to build accessibility tree. +// Uses `var` intentionally: this is executed via WebDriver's executeScript in arbitrary +// browser contexts, so we avoid `const`/`let` for maximum compatibility. +var ROLE_MAP = { + A: 'link', BUTTON: 'button', INPUT: 'textbox', SELECT: 'combobox', + OPTION: 'option', TEXTAREA: 'textbox', IMG: 'img', TABLE: 'table', + THEAD: 'rowgroup', TBODY: 'rowgroup', TR: 'row', TH: 'columnheader', + TD: 'cell', UL: 'list', OL: 'list', LI: 'listitem', NAV: 'navigation', + MAIN: 'main', HEADER: 'banner', FOOTER: 'contentinfo', ASIDE: 'complementary', + FORM: 'form', SECTION: 'region', H1: 'heading', H2: 'heading', + H3: 'heading', H4: 'heading', H5: 'heading', H6: 'heading', + DIALOG: 'dialog', DETAILS: 'group', SUMMARY: 'button', + FIELDSET: 'group', LEGEND: 'legend', LABEL: 'label', + PROGRESS: 'progressbar', METER: 'meter' +}; +var INPUT_ROLES = { + checkbox: 'checkbox', radio: 'radio', button: 'button', + submit: 'button', reset: 'button', range: 'slider', + search: 'searchbox', email: 'textbox', url: 'textbox', + tel: 'textbox', number: 'spinbutton' +}; +var SKIP = { SCRIPT:1, STYLE:1, NOSCRIPT:1, TEMPLATE:1, SVG:1 }; + +function walk(el) { + if (!el) return null; + if (el.nodeType === 3) { + var t = el.textContent.trim(); + return t ? { role: 'text', name: t.substring(0, 200) } : null; + } + if (el.nodeType !== 1 || SKIP[el.tagName]) return null; + // Note: we check the HTML hidden attribute and aria-hidden, but intentionally + // skip getComputedStyle checks for display:none / visibility:hidden — calling + // getComputedStyle on every node forces style recalculation and is too expensive + // for large DOMs. If you need CSS-hidden filtering, add it here at the cost of + // performance: var cs = window.getComputedStyle(el); if (cs.display === 'none' || cs.visibility === 'hidden') return null; + if (el.hidden || el.getAttribute('aria-hidden') === 'true') return null; + + var tag = el.tagName; + var role = el.getAttribute('role') || (tag === 'INPUT' ? INPUT_ROLES[el.type] : null) || ROLE_MAP[tag] || null; + var name = el.getAttribute('aria-label') || el.getAttribute('alt') || el.getAttribute('title') + || el.getAttribute('placeholder') || el.getAttribute('name') || null; + var node = {}; + if (role) node.role = role; + if (name) node.name = name; + if (el.id) node.id = el.id; + if (/^H[1-6]$/.test(tag)) node.level = parseInt(tag[1], 10); + if (el.href) node.href = el.href; + if (el.disabled) node.disabled = true; + if (el.checked) node.checked = true; + if (el.required) node.required = true; + if (el.value && (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT')) node.value = el.value.substring(0, 200); + + var kids = []; + for (var i = 0; i < el.childNodes.length; i++) { + var c = walk(el.childNodes[i]); + if (c) kids.push(c); + } + + // Collapse: text-only node with no role gets merged up + if (!role && !name && !el.id && kids.length === 1 && kids[0].role === 'text') return kids[0]; + // Skip empty containers with no semantic meaning + if (!role && !name && !el.id && kids.length === 0) return null; + + if (kids.length > 0) node.children = kids; + // If the node has nothing useful, skip it + if (!node.role && !node.name && !node.id && !node.children) return null; + return node; +} +return walk(document.body); diff --git a/src/lib/server.js b/src/lib/server.js index 8ff5b11..b3cdb36 100755 --- a/src/lib/server.js +++ b/src/lib/server.js @@ -1,7 +1,9 @@ #!/usr/bin/env node +import { readFileSync } from 'fs'; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import pkg from 'selenium-webdriver'; const { Builder, By, Key, until, Actions, error } = pkg; @@ -111,6 +113,11 @@ async function setupBidi(driver, sessionId) { state.bidi.set(sessionId, bidi); } +// Browser-side script loaded from file and executed via WebDriver's executeScript. +const accessibilitySnapshotScript = readFileSync( + new URL('./accessibility-snapshot.js', import.meta.url), 'utf-8' +); + // Common schemas const browserOptionsSchema = z.object({ headless: z.boolean().optional().describe("Run browser in headless mode"), @@ -124,12 +131,14 @@ const locatorSchema = { }; // Browser Management Tools -server.tool( +server.registerTool( "start_browser", - "launches browser", { - browser: z.enum(["chrome", "firefox", "edge", "safari"]).describe("Browser to launch (chrome, firefox, edge, or safari)"), - options: browserOptionsSchema + description: "launches browser", + inputSchema: { + browser: z.enum(["chrome", "firefox", "edge", "safari"]).describe("Browser to launch (chrome, firefox, edge, or safari)"), + options: browserOptionsSchema + } }, async ({ browser, options = {} }) => { try { @@ -238,11 +247,13 @@ server.tool( } ); -server.tool( +server.registerTool( "navigate", - "navigates to a URL", { + description: "navigates to a URL", + inputSchema: { url: z.string().describe("URL to navigate to") + } }, async ({ url }) => { try { @@ -261,12 +272,14 @@ server.tool( ); // Element Interaction Tools -server.tool( +server.registerTool( "interact", - "performs a mouse action on an element", { + description: "performs a mouse action on an element", + inputSchema: { action: z.enum(["click", "doubleclick", "rightclick", "hover"]).describe("Mouse action to perform"), ...locatorSchema + } }, async ({ action, by, value, timeout = 10000 }) => { try { @@ -305,12 +318,14 @@ server.tool( } ); -server.tool( +server.registerTool( "send_keys", - "sends keys to an element, aka typing. Clears the field first.", { + description: "sends keys to an element, aka typing. Clears the field first.", + inputSchema: { ...locatorSchema, text: z.string().describe("Text to enter into the element") + } }, async ({ by, value, text, timeout = 10000 }) => { try { @@ -331,11 +346,13 @@ server.tool( } ); -server.tool( +server.registerTool( "get_element_text", - "gets the text content of an element", { + description: "gets the text content of an element", + inputSchema: { ...locatorSchema + } }, async ({ by, value, timeout = 10000 }) => { try { @@ -355,11 +372,13 @@ server.tool( } ); -server.tool( +server.registerTool( "press_key", - "simulates pressing a keyboard key", { + description: "simulates pressing a keyboard key", + inputSchema: { key: z.string().describe("Key to press (e.g., 'Enter', 'Tab', 'a', etc.)") + } }, async ({ key }) => { try { @@ -387,12 +406,14 @@ server.tool( } ); -server.tool( +server.registerTool( "upload_file", - "uploads a file using a file input element", { + description: "uploads a file using a file input element", + inputSchema: { ...locatorSchema, filePath: z.string().describe("Absolute path to the file to upload") + } }, async ({ by, value, filePath, timeout = 10000 }) => { try { @@ -412,11 +433,13 @@ server.tool( } ); -server.tool( +server.registerTool( "take_screenshot", - "captures a screenshot of the current page", { + description: "captures a screenshot of the current page", + inputSchema: { outputPath: z.string().optional().describe("Optional path where to save the screenshot. If not provided, returns an image/png content block.") + } }, async ({ outputPath }) => { try { @@ -444,10 +467,12 @@ server.tool( } ); -server.tool( +server.registerTool( "close_session", - "closes the current browser session", - {}, + { + description: "closes the current browser session", + inputSchema: {} + }, async () => { try { const driver = getDriver(); @@ -472,12 +497,14 @@ server.tool( ); // Element Utility Tools -server.tool( +server.registerTool( "get_element_attribute", - "gets the value of an attribute on an element", { + description: "gets the value of an attribute on an element", + inputSchema: { ...locatorSchema, attribute: z.string().describe("Name of the attribute to get (e.g., 'href', 'value', 'class')") + } }, async ({ by, value, attribute, timeout = 10000 }) => { try { @@ -497,12 +524,14 @@ server.tool( } ); -server.tool( +server.registerTool( "execute_script", - "executes JavaScript in the browser and returns the result. Use for advanced interactions not covered by other tools (e.g., drag and drop, scrolling, reading computed styles, manipulating the DOM directly).", { + description: "executes JavaScript in the browser and returns the result. Use for advanced interactions not covered by other tools (e.g., drag and drop, scrolling, reading computed styles, manipulating the DOM directly).", + inputSchema: { script: z.string().describe("JavaScript code to execute in the browser"), args: z.array(z.any()).optional().describe("Optional arguments to pass to the script (accessible via arguments[0], arguments[1], etc.)") + } }, async ({ script, args = [] }) => { try { @@ -524,12 +553,14 @@ server.tool( ); // Window/Tab Management -server.tool( +server.registerTool( "window", - "manages browser windows and tabs", { + description: "manages browser windows and tabs", + inputSchema: { action: z.enum(["list", "switch", "switch_latest", "close"]).describe("Window action to perform"), handle: z.string().optional().describe("Window handle (required for switch)") + } }, async ({ action, handle }) => { try { @@ -580,15 +611,17 @@ server.tool( ); // Frame Management -server.tool( +server.registerTool( "frame", - "switches focus to a frame or back to the main page", { + description: "switches focus to a frame or back to the main page", + inputSchema: { action: z.enum(["switch", "default"]).describe("Frame action to perform"), by: z.enum(["id", "css", "xpath", "name", "tag", "class"]).optional().describe("Locator strategy for frame element"), value: z.string().optional().describe("Value for the locator strategy"), index: z.number().optional().describe("Frame index (0-based)"), timeout: z.number().optional().describe("Max wait in ms") + } }, async ({ action, by, value, index, timeout = 10000 }) => { try { @@ -618,13 +651,15 @@ server.tool( ); // Alert/Dialog Handling -server.tool( +server.registerTool( "alert", - "handles a browser alert, confirm, or prompt dialog", { + description: "handles a browser alert, confirm, or prompt dialog", + inputSchema: { action: z.enum(["accept", "dismiss", "get_text", "send_text"]).describe("Action to perform on the alert"), text: z.string().optional().describe("Text to send (required for send_text)"), timeout: z.number().optional().describe("Max wait in ms") + } }, async ({ action, text, timeout = 5000 }) => { try { @@ -662,10 +697,11 @@ server.tool( // Cookie Management Tools -server.tool( +server.registerTool( "add_cookie", - "adds a cookie to the current browser session. The browser must be on a page from the cookie's domain before setting it.", { + description: "adds a cookie to the current browser session. The browser must be on a page from the cookie's domain before setting it.", + inputSchema: { name: z.string().describe("Name of the cookie"), value: z.string().describe("Value of the cookie"), domain: z.string().optional().describe("Domain the cookie is visible to"), @@ -673,6 +709,7 @@ server.tool( secure: z.boolean().optional().describe("Whether the cookie is a secure cookie"), httpOnly: z.boolean().optional().describe("Whether the cookie is HTTP only"), expiry: z.number().optional().describe("Expiry date of the cookie as a Unix timestamp (seconds since epoch)") + } }, async ({ name, value, domain, path, secure, httpOnly, expiry }) => { try { @@ -696,11 +733,13 @@ server.tool( } ); -server.tool( +server.registerTool( "get_cookies", - "retrieves cookies from the current browser session. Returns all cookies or a specific cookie by name.", { + description: "retrieves cookies from the current browser session. Returns all cookies or a specific cookie by name.", + inputSchema: { name: z.string().optional().describe("Name of a specific cookie to retrieve. If omitted, all cookies are returned.") + } }, async ({ name }) => { try { @@ -741,11 +780,13 @@ server.tool( } ); -server.tool( +server.registerTool( "delete_cookie", - "deletes cookies from the current browser session. Can delete a specific cookie by name or all cookies.", { + description: "deletes cookies from the current browser session. Can delete a specific cookie by name or all cookies.", + inputSchema: { name: z.string().optional().describe("Name of the cookie to delete. If omitted, all cookies are deleted.") + } }, async ({ name }) => { try { @@ -777,12 +818,14 @@ const diagnosticTypes = { network: { logKey: 'networkLogs', emptyMessage: 'No network activity captured' } }; -server.tool( +server.registerTool( "diagnostics", - "retrieves browser diagnostics (console logs, JS errors, or network activity) captured via WebDriver BiDi", { + description: "retrieves browser diagnostics (console logs, JS errors, or network activity) captured via WebDriver BiDi", + inputSchema: { type: z.enum(["console", "errors", "network"]).describe("Type of diagnostic data to retrieve"), clear: z.boolean().optional().describe("Clear after returning (default: false)") + } }, async ({ type, clear = false }) => { try { @@ -806,7 +849,7 @@ server.tool( ); // Resources -server.resource( +server.registerResource( "browser-status", "browser-status://current", { @@ -824,6 +867,28 @@ server.resource( }) ); +server.registerResource( + "accessibility-snapshot", + "accessibility://current", + { + description: "Accessibility tree snapshot of the current page. A compact, structured representation of interactive elements and text content, much smaller than full HTML. Useful for understanding page layout and finding elements to interact with.", + mimeType: "application/json" + }, + async (uri) => { + try { + const driver = state.drivers.get(state.currentSession); + //-32002 is not in the SDK but is noted in the MCP specification: + // https://modelcontextprotocol.io/specification/2025-11-25/server/resources#error-handling + if (!driver) throw new McpError(-32002, "No active browser session. Start a browser first."); + const tree = await driver.executeScript(accessibilitySnapshotScript) || {}; + return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(tree, null, 2) }] }; + } catch (e) { + if (e instanceof McpError) throw e; + throw new McpError(ErrorCode.InternalError, `Failed to capture accessibility snapshot: ${e.message}`); + } + } +); + // Cleanup handler async function cleanup() { for (const [sessionId, driver] of state.drivers) { diff --git a/test/mcp-client.mjs b/test/mcp-client.mjs index f838e34..24a2be0 100644 --- a/test/mcp-client.mjs +++ b/test/mcp-client.mjs @@ -103,6 +103,28 @@ export class McpClient { return resp.result.tools; } + /** + * List all available resources. + */ + async listResources() { + const resp = await this.#sendRequest('resources/list', {}); + if (resp.error) { + throw new Error(`RPC error listing resources: ${JSON.stringify(resp.error)}`); + } + return resp.result.resources; + } + + /** + * Read a resource by URI. + */ + async readResource(uri) { + const resp = await this.#sendRequest('resources/read', { uri }); + if (resp.error) { + throw new Error(`RPC error reading resource: ${JSON.stringify(resp.error)}`); + } + return resp.result; + } + /** * Stop the server process and clean up. */ diff --git a/test/navigation.test.mjs b/test/navigation.test.mjs index 3037918..6401a75 100644 --- a/test/navigation.test.mjs +++ b/test/navigation.test.mjs @@ -135,14 +135,11 @@ describe('Navigation & Element Locators', () => { }); it('should reject unsupported locator strategy via schema validation', async () => { - await assert.rejects( - () => client.callTool('get_element_text', { by: 'invalid', value: 'test' }), - (err) => { - assert.ok(err.message.includes('invalid_enum_value') || err.message.includes('Invalid'), - `Expected validation error, got: ${err.message}`); - return true; - } - ); + const result = await client.callTool('get_element_text', { by: 'invalid', value: 'test' }); + assert.strictEqual(result.isError, true, 'Expected isError: true for invalid enum value'); + const text = getResponseText(result); + assert.ok(text.includes('invalid') || text.includes('Invalid'), + `Expected validation error, got: ${text}`); }); it('should error when element not found', async () => { diff --git a/test/resources.test.mjs b/test/resources.test.mjs new file mode 100644 index 0000000..a394fe6 --- /dev/null +++ b/test/resources.test.mjs @@ -0,0 +1,92 @@ +/** + * MCP Resources — accessibility-snapshot tests. + * Requires a browser session with a loaded page. + */ + +import { describe, it, before, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { McpClient, fixture } from './mcp-client.mjs'; + +describe('Resources', () => { + let client; + + before(async () => { + client = new McpClient(); + await client.start(); + await client.callTool('start_browser', { + browser: 'chrome', + options: { headless: true, arguments: ['--no-sandbox', '--disable-dev-shm-usage'] }, + }); + await client.callTool('navigate', { url: fixture('locators.html') }); + }); + + after(async () => { + try { await client.callTool('close_session'); } catch { /* ignore */ } + await client.stop(); + }); + + describe('accessibility://current', () => { + it('should include page elements with roles, levels, and IDs', async () => { + const result = await client.readResource('accessibility://current'); + assert.equal(result.contents[0].mimeType, 'application/json'); + const tree = JSON.parse(result.contents[0].text); + + const headings = findNodes(tree, (n) => n.role === 'heading'); + assert.ok(headings.length > 0, 'Should find at least one heading'); + assert.equal(headings[0].level, 1, 'H1 should have level 1'); + + const buttons = findNodes(tree, (n) => n.role === 'button'); + const links = findNodes(tree, (n) => n.role === 'link'); + const textboxes = findNodes(tree, (n) => n.role === 'textbox'); + assert.ok(buttons.length > 0, 'Should find at least one button'); + assert.ok(links.length > 0, 'Should find at least one link'); + assert.ok(textboxes.length > 0, 'Should find at least one textbox'); + + const ids = findNodes(tree, (n) => n.id).map((n) => n.id); + assert.ok(ids.includes('title'), 'Should include #title'); + assert.ok(ids.includes('btn'), 'Should include #btn'); + assert.ok(ids.includes('input'), 'Should include #input'); + }); + + it('should not include script or style content', async () => { + await client.callTool('execute_script', { + script: ` + var s = document.createElement('script'); s.textContent = 'var secret = 42;'; document.body.appendChild(s); + var c = document.createElement('style'); c.textContent = 'body { color: red; }'; document.body.appendChild(c); + ` + }); + const result = await client.readResource('accessibility://current'); + const text = result.contents[0].text; + assert.ok(!text.includes('secret'), 'Tree should not contain script content'); + assert.ok(!text.includes('color: red'), 'Tree should not contain style content'); + }); + + // Separate client needed: the main client already has a browser session, + // and we need a clean session with no browser to test the no-session error path. + it('should return error code -32002 when reading with no session', async () => { + const freshClient = new McpClient(); + await freshClient.start(); + try { + await freshClient.readResource('accessibility://current'); + assert.fail('Should have thrown'); + } catch (e) { + assert.ok(e.message.includes('-32002') || e.message.includes('No active browser session'), `Expected -32002 error, got: ${e.message}`); + } finally { + await freshClient.stop(); + } + }); + }); +}); + +/** Recursively find nodes matching a predicate. */ +function findNodes(node, predicate) { + if (!node) return []; + const results = []; + if (predicate(node)) results.push(node); + if (node.children) { + for (const child of node.children) { + results.push(...findNodes(child, predicate)); + } + } + return results; +} diff --git a/test/server.test.mjs b/test/server.test.mjs index b09f050..591a135 100644 --- a/test/server.test.mjs +++ b/test/server.test.mjs @@ -1,5 +1,5 @@ /** - * MCP Server — connection and tool registration tests. + * MCP Server — connection, tool registration, and resource registration tests. * No browser needed for these. */ @@ -74,4 +74,20 @@ describe('MCP Server', () => { assert.equal(tool.inputSchema.type, 'object', `Tool "${tool.name}" schema should be type object`); } }); + + it('should register all expected resources', async () => { + const resources = await client.listResources(); + const uris = resources.map((r) => r.uri); + + const expected = [ + 'browser-status://current', + 'accessibility://current', + ]; + + for (const uri of expected) { + assert.ok(uris.includes(uri), `Missing resource: ${uri}`); + } + + assert.equal(uris.length, expected.length, `Expected ${expected.length} resources, got ${uris.length}: ${uris.join(', ')}`); + }); });