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(', ')}`);
+ });
});