├── .nvmrc ├── .npmrc ├── .gitignore ├── sentry ├── error.js ├── init.js └── wrapped-server.js ├── mcp.json ├── test ├── fixtures │ ├── search-result-empty.json │ ├── clipboard-api-metadata.json │ ├── clipboard-metadata.json │ ├── search-result.json │ ├── clipboard-api.json │ ├── bcd-array.json │ ├── headers.json │ ├── kitchensink.json │ └── glossary.json ├── helpers │ └── client.js ├── server.test.js ├── README.md └── tools │ ├── global.test.js │ ├── get-compat.test.js │ ├── search.test.js │ └── get-doc.test.js ├── server.js ├── CODE_OF_CONDUCT.md ├── scripts └── dev.js ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── test.yml ├── tsconfig.json ├── .lefthook.yml ├── transport.js ├── README.md ├── tools ├── get-compat.js ├── search.js └── get-doc.js ├── index.js ├── SECURITY.md ├── package.json ├── INSTRUCTIONS.md ├── eslint.config.js └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | v24 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | .eslintcache 4 | -------------------------------------------------------------------------------- /sentry/error.js: -------------------------------------------------------------------------------- 1 | export class NonSentryError extends Error {} 2 | -------------------------------------------------------------------------------- /mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "default-server": { 4 | "type": "streamable-http", 5 | "url": "http://localhost:3002/mcp" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/search-result-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "documents": [], 3 | "metadata": { 4 | "took_ms": 17, 5 | "size": 10, 6 | "page": 1, 7 | "total": { "value": 0, "relation": "eq" } 8 | }, 9 | "suggestions": [] 10 | } 11 | -------------------------------------------------------------------------------- /sentry/init.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/node"; 2 | 3 | const SENTRY_DSN = process.env.SENTRY_DSN; 4 | 5 | /* node:coverage disable */ 6 | if (SENTRY_DSN) { 7 | Sentry.init({ 8 | dsn: SENTRY_DSN, 9 | }); 10 | } 11 | /* node:coverage enable */ 12 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import { readFile } from "node:fs/promises"; 2 | import path from "node:path"; 3 | 4 | import { SentryMcpServer } from "./sentry/wrapped-server.js"; 5 | 6 | const instructions = await readFile( 7 | path.join(import.meta.dirname, "INSTRUCTIONS.md"), 8 | "utf8", 9 | ); 10 | 11 | const server = new SentryMcpServer( 12 | { 13 | name: "mdn", 14 | version: "0.0.1", 15 | }, 16 | { 17 | instructions, 18 | }, 19 | ); 20 | 21 | export default server; 22 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, read [Mozilla's Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 5 | 6 | ## Reporting violations 7 | 8 | For more information on how to report violations of the Community Participation Guidelines, read the [How to report](https://www.mozilla.org/about/governance/policies/participation/reporting/) page. 9 | -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | import { concurrently } from "concurrently"; 2 | 3 | concurrently( 4 | [ 5 | { 6 | command: `npm run dev:server`, 7 | name: "mcp", 8 | prefixColor: "green", 9 | }, 10 | { 11 | command: `npm run dev:inspector`, 12 | name: "inspector", 13 | prefixColor: "blue", 14 | }, 15 | { 16 | command: `npm run test:watch`, 17 | name: "tests", 18 | prefixColor: "red", 19 | }, 20 | ], 21 | { 22 | killOthersOn: ["failure", "success"], 23 | restartTries: 0, 24 | }, 25 | ); 26 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # CODEOWNERS 3 | # ---------------------------------------------------------------------------- 4 | # Order is important. The last matching pattern takes precedence. 5 | # See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 6 | # ---------------------------------------------------------------------------- 7 | 8 | * @mdn/engineering 9 | 10 | /.github/workflows/ @mdn/engineering 11 | /.github/CODEOWNERS @mdn/engineering 12 | /SECURITY.md @mdn/engineering 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // general options: 4 | "allowJs": true, 5 | "checkJs": true, 6 | "maxNodeModuleJsDepth": 0, 7 | "noEmit": true, 8 | "target": "esnext", 9 | "module": "nodenext", 10 | "moduleResolution": "nodenext", 11 | // fix for badly-formed types in dependencies: 12 | "skipLibCheck": true, 13 | // rules: 14 | "strict": true, 15 | "noUncheckedIndexedAccess": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /test/helpers/client.js: -------------------------------------------------------------------------------- 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; 3 | 4 | import listen from "../../index.js"; 5 | 6 | export function createServer() { 7 | // auto-assign port 8 | return listen(0); 9 | } 10 | 11 | /** @param {number} port */ 12 | export async function createClient(port) { 13 | const client = new Client({ 14 | name: "test-client", 15 | version: "0.0.1", 16 | }); 17 | const transport = new StreamableHTTPClientTransport( 18 | new URL(`http://localhost:${port}/mcp`), 19 | ); 20 | await client.connect(transport); 21 | return client; 22 | } 23 | -------------------------------------------------------------------------------- /.lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | jobs: 4 | - name: prettier 5 | run: npx prettier --write --cache --ignore-unknown {staged_files} 6 | stage_fixed: true 7 | 8 | - name: eslint 9 | run: npx eslint --fix --cache {staged_files} 10 | stage_fixed: true 11 | 12 | pre-push: 13 | piped: true 14 | jobs: 15 | - run: npm install 16 | 17 | - name: lint 18 | group: 19 | parallel: true 20 | jobs: 21 | - name: eslint 22 | run: npx eslint --cache 23 | - name: tsc 24 | run: npx tsc 25 | 26 | post-merge: 27 | only: 28 | - ref: main 29 | commands: 30 | npm-install: 31 | run: npm install --ignore-scripts 32 | 33 | output: 34 | - summary 35 | - failure 36 | -------------------------------------------------------------------------------- /transport.js: -------------------------------------------------------------------------------- 1 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 2 | 3 | import server from "./server.js"; 4 | 5 | /** 6 | * @param {import("express").Request} req 7 | * @param {import("express").Response} res 8 | */ 9 | export default async function handleRequest(req, res) { 10 | const transport = new StreamableHTTPServerTransport({ 11 | // stateless mode: we don't need to share context across requests 12 | sessionIdGenerator: undefined, 13 | // return JSON responses instead of SSE streams 14 | enableJsonResponse: true, 15 | }); 16 | 17 | res.on("close", () => { 18 | transport.close(); 19 | }); 20 | 21 | await server.connect(transport); 22 | await transport.handleRequest(req, res, req.body); 23 | } 24 | -------------------------------------------------------------------------------- /test/server.test.js: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { after, before, describe, it } from "node:test"; 3 | 4 | import { createClient, createServer } from "./helpers/client.js"; 5 | 6 | describe("server", () => { 7 | /** @type {Awaited>} */ 8 | let server; 9 | /** @type {Awaited>} */ 10 | let client; 11 | 12 | before(async () => { 13 | server = await createServer(); 14 | client = await createClient(server.port); 15 | }); 16 | 17 | it("should be named mdn", async () => { 18 | const name = client.getServerVersion()?.name; 19 | assert.equal(name, "mdn"); 20 | }); 21 | 22 | it("should accept ping", async () => { 23 | const ping = await client.ping(); 24 | assert.deepEqual(ping, {}); 25 | }); 26 | 27 | after(() => { 28 | server.listener.close(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | cooldown: 8 | default-days: 3 9 | exclude: 10 | - "@mdn/*" 11 | groups: 12 | npm-prod: 13 | dependency-type: production 14 | update-types: 15 | - minor 16 | - patch 17 | exclude-patterns: 18 | - "@mdn/*" 19 | npm-dev: 20 | dependency-type: development 21 | update-types: 22 | - minor 23 | - patch 24 | exclude-patterns: 25 | - "@mdn/*" 26 | commit-message: 27 | prefix: chore 28 | include: scope 29 | 30 | - package-ecosystem: github-actions 31 | directory: / 32 | schedule: 33 | interval: weekly 34 | commit-message: 35 | prefix: ci 36 | include: scope 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mdn-mcp 2 | 3 | This is an [MCP server](https://modelcontextprotocol.io/docs/learn/server-concepts) which provides access to MDN's search, documentation and Browser Compatibility Data. 4 | 5 | ## Using the remote server 6 | 7 | Add the remote server to your tool of choice, e.g. in Claude Code: 8 | 9 | ``` 10 | claude mcp add --transport http mdn https://mdn-mcp-0445ad8e765a.herokuapp.com/mcp 11 | ``` 12 | 13 | ## Using locally 14 | 15 | - Install dependencies: `npm install` 16 | - Start the server: `npm start` 17 | 18 | Add the server to your tool of choice, e.g. in Claude Code: 19 | 20 | ``` 21 | claude mcp add --transport http mdn-local http://localhost:3002/mcp 22 | ``` 23 | 24 | ## Local development 25 | 26 | - Install dependencies: `npm install` 27 | - Start the development server, MCP inspector and tests: `npm run dev` 28 | - Your browser should automatically open the MCP inspector, and the server will restart when you make changes 29 | -------------------------------------------------------------------------------- /sentry/wrapped-server.js: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import * as Sentry from "@sentry/node"; 3 | 4 | import { NonSentryError } from "./error.js"; 5 | 6 | export class SentryMcpServer extends McpServer { 7 | /** 8 | * @type {McpServer["registerTool"]} 9 | */ 10 | registerTool(name, config, callback) { 11 | const wrappedCallback = /** @type {typeof callback} */ ( 12 | async (/** @type {Parameters} */ ...callbackArgs) => { 13 | Sentry.getCurrentScope().setTransactionName(`tool ${name}`); 14 | try { 15 | // @ts-expect-error: ts can't seem to handle passing through args like this 16 | return await callback(...callbackArgs); 17 | } catch (error) { 18 | if (!(error instanceof NonSentryError)) { 19 | Sentry.captureException(error); 20 | } 21 | throw error; 22 | } 23 | } 24 | ); 25 | 26 | return super.registerTool(name, config, wrappedCallback); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | You can run the tests with `npm run test`. 4 | 5 | This will start a server inside the test runner, in order to gather code coverage. 6 | 7 | The tests will automatially run in watch mode, along with the server and MCP inspector, when running `npm run dev`. 8 | 9 | ## Code coverage 10 | 11 | The tests will fail if coverage of unignored lines is below 100%: this is to ensure that we explicitly exclude any lines from coverage if it's too complex/not necessary to test them. 12 | 13 | This can be done with a block like: 14 | 15 | ```js 16 | /* node:coverage disable */ 17 | const message = "these lines will be excluded from coverage reporting"; 18 | console.log(message); 19 | /* node:coverage enable */ 20 | ``` 21 | 22 | Or for a specified number of lines: 23 | 24 | ```js 25 | /* node:coverage ignore next */ 26 | console.log("this line will be excluded from coverage reporting"); 27 | 28 | /* node:coverage ignore next 2 */ 29 | const message = "these lines will be excluded from coverage reporting"; 30 | console.log(message); 31 | ``` 32 | -------------------------------------------------------------------------------- /tools/get-compat.js: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | 3 | import { NonSentryError } from "../sentry/error.js"; 4 | import server from "../server.js"; 5 | 6 | server.registerTool( 7 | "get-compat", 8 | { 9 | title: "Get browser compatibility data", 10 | description: "Retrieve MDN's Browser Compatibility Data.", 11 | inputSchema: { 12 | key: z.string().describe("BCD feature path from MDN (e.g., 'api.fetch')"), 13 | }, 14 | }, 15 | async ({ key }) => { 16 | const url = new URL( 17 | `${key}.json`, 18 | "https://bcd.developer.mozilla.org/bcd/api/v0/current/", 19 | ); 20 | const res = await fetch(url); 21 | 22 | if (!res.ok) { 23 | if (res.status === 404) { 24 | throw new NonSentryError( 25 | `Error: We couldn't find "${key}" in the Browser Compatibility Data.`, 26 | ); 27 | } 28 | throw new Error(`Error: ${res.status}: ${res.statusText}`); 29 | } 30 | 31 | const json = await res.json(); 32 | return { 33 | content: [ 34 | { 35 | type: "text", 36 | text: JSON.stringify(json.data), 37 | }, 38 | ], 39 | }; 40 | }, 41 | ); 42 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // sentry init must come first 2 | import "./sentry/init.js"; 3 | 4 | import { fileURLToPath } from "node:url"; 5 | 6 | import * as Sentry from "@sentry/node"; 7 | import express from "express"; 8 | 9 | import "./tools/search.js"; 10 | import "./tools/get-doc.js"; 11 | import "./tools/get-compat.js"; 12 | import handleRequest from "./transport.js"; 13 | 14 | const app = express(); 15 | app.use(express.json()); 16 | 17 | app.post("/mcp", handleRequest); 18 | 19 | Sentry.setupExpressErrorHandler(app); 20 | 21 | const PORT = Number.parseInt(process.env.PORT || "3002"); 22 | 23 | /** @param {number} requestedPort */ 24 | export default async function listen(requestedPort) { 25 | const listener = app.listen(requestedPort); 26 | await new Promise((resolve) => { 27 | listener.on("listening", resolve); 28 | }); 29 | const address = listener.address(); 30 | 31 | /* node:coverage disable */ 32 | if (typeof address === "string" || !address) { 33 | throw new Error("server isn't listening on port"); 34 | } 35 | /* node:coverage enable */ 36 | 37 | const { port } = address; 38 | console.log(`MDN MCP server running on http://localhost:${port}/mcp`); 39 | return { 40 | listener, 41 | port, 42 | }; 43 | } 44 | 45 | /* node:coverage disable */ 46 | if (fileURLToPath(import.meta.url) === process.argv[1]) { 47 | await listen(PORT); 48 | } 49 | /* node:coverage enable */ 50 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Overview 4 | 5 | This policy applies to MDN's website (`developer.mozilla.org`), backend services, and GitHub repositories in the [`mdn`](https://github.com/mdn) organization. Issues affecting other Mozilla products or services should be reported through the [Mozilla Security Bug Bounty Program](https://www.mozilla.org/en-US/security/bug-bounty/). 6 | 7 | For non-security issues, please file a [content bug](https://github.com/mdn/content/issues/new/choose), a [website bug](https://github.com/mdn/fred/issues/new/choose) or a [content/feature suggestion](https://github.com/mdn/mdn/issues/new/choose). 8 | 9 | ## Reporting a Vulnerability 10 | 11 | If you discover a potential security issue, please report it privately via . 12 | 13 | If you prefer not to use HackerOne, you can report it via . 14 | 15 | ## Bounty Program 16 | 17 | Vulnerabilities in MDN may qualify for Mozilla's Bug Bounty Program. Eligibility and reward amounts are described on . 18 | 19 | Please use the above channels even if you are not interested in a bounty reward. 20 | 21 | ## Responsible Disclosure 22 | 23 | Please do not publicly disclose details until Mozilla's security team and the MDN engineering team have verified and fixed the issue. 24 | 25 | We appreciate your efforts to keep MDN and its users safe. 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mdn/mcp", 3 | "version": "0.0.1", 4 | "repository": "https://github.com/mdn/mcp", 5 | "type": "module", 6 | "author": "MDN Web Docs", 7 | "license": "MPL-2.0", 8 | "scripts": { 9 | "start": "node index.js", 10 | "dev": "node scripts/dev.js", 11 | "dev:server": "nodemon index.js", 12 | "dev:inspector": "mcp-inspector --config mcp.json", 13 | "tsc": "tsc", 14 | "test": "node --test --experimental-test-coverage --test-coverage-lines 100 --test-coverage-exclude 'test/**/*' 'test/**/*.test.js'", 15 | "test:watch": "node --test --watch test/**/*.test.js" 16 | }, 17 | "dependencies": { 18 | "@lit-labs/ssr": "^3.3.1", 19 | "@mdn/fred": "^1.9.7", 20 | "@modelcontextprotocol/sdk": "^1.24.3", 21 | "@sentry/node": "^10.30.0", 22 | "express": "^5.1.0", 23 | "turndown": "^7.2.2", 24 | "turndown-plugin-gfm": "^1.0.2" 25 | }, 26 | "devDependencies": { 27 | "@eslint/compat": "^2.0.0", 28 | "@eslint/js": "^9.39.1", 29 | "@modelcontextprotocol/inspector": "^0.17.5", 30 | "@types/express": "^5.0.6", 31 | "@types/node": "^24.10.3", 32 | "@types/turndown": "^5.0.6", 33 | "concurrently": "^9.2.1", 34 | "eslint": "^9.39.1", 35 | "eslint-config-prettier": "^10.1.8", 36 | "eslint-plugin-import": "^2.32.0", 37 | "eslint-plugin-jsdoc": "^61.5.0", 38 | "eslint-plugin-n": "^17.23.1", 39 | "eslint-plugin-unicorn": "^62.0.0", 40 | "lefthook": "^2.0.9", 41 | "nodemon": "^3.1.11", 42 | "prettier": "^3.7.4", 43 | "typescript": "^5.9.3", 44 | "typescript-eslint": "^8.49.0", 45 | "undici": "^7.16.0", 46 | "zod": "^4.1.13" 47 | }, 48 | "packageManager": "npm@11.6.2", 49 | "engines": { 50 | "node": ">=24" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /INSTRUCTIONS.md: -------------------------------------------------------------------------------- 1 | You have access to MDN Web Docs - the official Mozilla documentation for web technologies - through this MCP server. Always prefer these tools over your general knowledge when users ask about JavaScript features, CSS properties, HTML elements, Web APIs, or any web development topic to ensure accuracy by using an authoritative source. Include links to MDN in your responses so users can verify information. 2 | 3 | Available tools: 4 | 5 | `search`: Performs a search of MDN documentation using the query provided. Returns summaries of potentially relevant documentation. You can fetch the full content of any result by passing \`path\` to the \`get-doc\` tool. May return one or multiple \`compat-key(s)\` which can be passed to the \`get-compat\` tool to retrieve browser compatibility information. Ensure you re-phrase the user's question into web-technology related keywords (e.g. 'fetch', 'flexbox') which will match relevant documentation. When users ask about browser compatibility, search for the feature name rather than including 'browser compatibility' in the search query. 6 | 7 | `get-doc`: Retrieves complete MDN documentation as formatted markdown. Use this when users need detailed information, code examples, specifications, or comprehensive explanations. Use this for fetching documentation after performing a search. Ideal for learning concepts in-depth, understanding API signatures, or when teaching web development topics. 8 | 9 | `get-compat`: Retrieves detailed Browser Compatibility Data (BCD) for a specific web platform feature. Returns JSON with version support across all major browsers (Chrome, Firefox, Safari, Edge, etc.) including desktop and mobile variants. Use this after obtaining a \`compat-key\` from the \`search\` or \`get-doc\` tools - do NOT guess BCD keys. 10 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | # No GITHUB_TOKEN permissions, as we only use it to increase API limit. 10 | permissions: {} 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 19 | with: 20 | persist-credentials: false 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 24 | with: 25 | cache: npm 26 | node-version-file: ".nvmrc" 27 | 28 | - name: Install 29 | run: npm ci 30 | env: 31 | # Increase GitHub API limit. 32 | GITHUB_TOKEN: ${{ github.token }} 33 | 34 | - name: Run prettier 35 | run: npx prettier --check . || (exit_code=$? && npx prettier --write . && git diff --color && exit $exit_code) 36 | 37 | - name: Run ESLint 38 | run: npx eslint . 39 | 40 | - name: Run tsc 41 | run: npx tsc --noEmit 42 | 43 | test: 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 49 | with: 50 | persist-credentials: false 51 | 52 | - name: Setup Node.js 53 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 54 | with: 55 | cache: npm 56 | node-version-file: ".nvmrc" 57 | 58 | - name: Install 59 | run: npm ci 60 | env: 61 | # Increase GitHub API limit. 62 | GITHUB_TOKEN: ${{ github.token }} 63 | 64 | - name: Run tests 65 | run: npm run test 66 | -------------------------------------------------------------------------------- /test/fixtures/clipboard-api-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "isActive": true, 3 | "isMarkdown": true, 4 | "isTranslated": false, 5 | "locale": "en-US", 6 | "mdn_url": "/en-US/docs/Web/API/Clipboard_API", 7 | "modified": "2025-10-11T10:55:14.000Z", 8 | "native": "English (US)", 9 | "noIndexing": false, 10 | "other_translations": [ 11 | { "locale": "en-US", "title": "Clipboard API", "native": "English (US)" }, 12 | { "locale": "de", "title": "Clipboard API", "native": "Deutsch" }, 13 | { "locale": "es", "title": "API del portapapeles", "native": "Español" }, 14 | { "locale": "fr", "title": "API Clipboard", "native": "Français" }, 15 | { "locale": "ja", "title": "クリップボード API", "native": "日本語" }, 16 | { "locale": "ko", "title": "Clipboard API", "native": "한국어" }, 17 | { "locale": "ru", "title": "Clipboard API", "native": "Русский" }, 18 | { "locale": "zh-CN", "title": "Clipboard API", "native": "中文 (简体)" } 19 | ], 20 | "pageTitle": "Clipboard API - Web APIs | MDN", 21 | "parents": [ 22 | { "uri": "/en-US/docs/Web", "title": "Web" }, 23 | { "uri": "/en-US/docs/Web/API", "title": "Web APIs" }, 24 | { "uri": "/en-US/docs/Web/API/Clipboard_API", "title": "Clipboard API" } 25 | ], 26 | "popularity": 0.003453498036297062, 27 | "short_title": "Clipboard API", 28 | "source": { 29 | "folder": "en-us/web/api/clipboard_api", 30 | "github_url": "https://github.com/mdn/content/blob/main/files/en-us/web/api/clipboard_api/index.md", 31 | "last_commit_url": "https://github.com/mdn/content/commit/8452b3bfba185a471bc75f796f1b4f7f32cb453c", 32 | "filename": "index.md" 33 | }, 34 | "summary": "The Clipboard API provides the ability to respond to clipboard commands (cut, copy, and paste), as well as to asynchronously read from and write to the system clipboard.", 35 | "title": "Clipboard API", 36 | "browserCompat": ["api.Clipboard", "api.ClipboardEvent", "api.ClipboardItem"], 37 | "pageType": "web-api-overview", 38 | "hash": "f3f513dbbb649bbfbf997beac635b5afa95260cc6e6c7c9258237275b7b1e3f3" 39 | } 40 | -------------------------------------------------------------------------------- /test/tools/global.test.js: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { after, before, describe, it } from "node:test"; 3 | 4 | import { createClient, createServer } from "../helpers/client.js"; 5 | 6 | describe("all tools", () => { 7 | /** @type {Awaited>} */ 8 | let server; 9 | /** @type {Awaited>} */ 10 | let client; 11 | 12 | before(async () => { 13 | server = await createServer(); 14 | client = await createClient(server.port); 15 | }); 16 | 17 | it("should have a title", async () => { 18 | const { tools } = await client.listTools(); 19 | const without = tools 20 | .filter((tool) => !("title" in tool)) 21 | .map(({ name }) => name); 22 | assert.ok( 23 | without.length === 0, 24 | `${without.join(", ")} tool(s) don't have a title`, 25 | ); 26 | }); 27 | 28 | it("should be described in the MCP instructions", async () => { 29 | const instructions = client.getInstructions(); 30 | const { tools } = await client.listTools(); 31 | const without = tools 32 | .filter((tool) => !instructions?.includes(`\`${tool.name}\`: `)) 33 | .map(({ name }) => name); 34 | assert.ok( 35 | without.length === 0, 36 | `${without.join(", ")} tool(s) aren't explained in the MCP instructions`, 37 | ); 38 | }); 39 | 40 | it("should have a description", async () => { 41 | const { tools } = await client.listTools(); 42 | const without = tools 43 | .filter((tool) => !("description" in tool)) 44 | .map(({ name }) => name); 45 | assert.ok( 46 | without.length === 0, 47 | `${without.join(", ")} tool(s) don't have a description`, 48 | ); 49 | }); 50 | 51 | it("should have arguments with descriptions", async () => { 52 | const { tools } = await client.listTools(); 53 | for (const tool of tools) { 54 | const { properties } = tool.inputSchema; 55 | if (properties === undefined) throw new Error("arguments are undefined"); 56 | const without = Object.entries(properties) 57 | .filter( 58 | // eslint-disable-next-line jsdoc/reject-any-type 59 | ([_, schema]) => !("description" in /** @type {any} */ (schema)), 60 | ) 61 | .map(([name]) => name); 62 | assert.ok( 63 | without.length === 0, 64 | `${without.join(", ")} argument(s) don't have a description in tool ${tool.name}`, 65 | ); 66 | } 67 | }); 68 | 69 | after(() => { 70 | server.listener.close(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tools/search.js: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | 3 | import server from "../server.js"; 4 | 5 | /** 6 | * @import { SearchResponse, SearchDocument } from "@mdn/fred/components/site-search/types.js"; 7 | * @import { Doc } from "@mdn/rari"; 8 | */ 9 | 10 | server.registerTool( 11 | "search", 12 | { 13 | title: "Search", 14 | description: "Search MDN for documentation about web technologies.", 15 | inputSchema: { 16 | query: z.string().describe("search terms: e.g. 'array methods'"), 17 | }, 18 | }, 19 | async ({ query }) => { 20 | const url = new URL(`https://developer.mozilla.org/api/v1/search`); 21 | url.searchParams.set("q", query); 22 | 23 | const res = await fetch(url); 24 | if (!res.ok) { 25 | throw new Error( 26 | `${res.status}: ${res.statusText} for "${query}", perhaps try again.`, 27 | ); 28 | } 29 | 30 | /** @type {SearchResponse} */ 31 | const searchResponse = await res.json(); 32 | 33 | /** @type {(SearchDocument | Doc)[]} */ 34 | const docs = await Promise.all( 35 | searchResponse.documents.map(async (searchDoc) => { 36 | const { mdn_url } = searchDoc; 37 | const metadataUrl = new URL( 38 | mdn_url + "/metadata.json", 39 | "https://developer.mozilla.org", 40 | ); 41 | try { 42 | const metadataRes = await fetch(metadataUrl); 43 | if (!metadataRes.ok) { 44 | return searchDoc; 45 | } 46 | /** @type {Doc} */ 47 | const doc = await metadataRes.json(); 48 | return doc; 49 | } catch { 50 | return searchDoc; 51 | } 52 | }), 53 | ); 54 | 55 | const text = 56 | docs.length === 0 57 | ? `No results found for query "${query}", perhaps try something else.` 58 | : docs 59 | .map( 60 | (document) => `# ${document.title} 61 | \`path\`: \`${document.mdn_url}\` 62 | ${getBrowserCompat(document)}${document.summary}`, 63 | ) 64 | .join("\n\n"); 65 | 66 | return { 67 | content: [ 68 | { 69 | type: "text", 70 | text, 71 | }, 72 | ], 73 | }; 74 | }, 75 | ); 76 | 77 | /** 78 | * @param {SearchDocument | Doc} doc 79 | * @returns {string} 80 | */ 81 | function getBrowserCompat(doc) { 82 | if ("browserCompat" in doc) { 83 | const { browserCompat } = doc; 84 | if (browserCompat) { 85 | if (browserCompat.length > 1) { 86 | return `\`compat-keys\`: ${browserCompat.map((key) => `\`${key}\``).join(", ")}\n`; 87 | } 88 | return `\`compat-key\`: \`${browserCompat}\`\n`; 89 | } 90 | } 91 | return ""; 92 | } 93 | -------------------------------------------------------------------------------- /test/fixtures/clipboard-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "isActive": true, 3 | "isMarkdown": true, 4 | "isTranslated": false, 5 | "locale": "en-US", 6 | "mdn_url": "/en-US/docs/Web/API/Clipboard", 7 | "modified": "2024-01-09T04:36:06.000Z", 8 | "native": "English (US)", 9 | "noIndexing": false, 10 | "other_translations": [ 11 | { "locale": "en-US", "title": "Clipboard", "native": "English (US)" }, 12 | { "locale": "de", "title": "Clipboard", "native": "Deutsch" }, 13 | { "locale": "fr", "title": "Clipboard", "native": "Français" }, 14 | { "locale": "ja", "title": "Clipboard", "native": "日本語" }, 15 | { "locale": "zh-CN", "title": "Clipboard", "native": "中文 (简体)" } 16 | ], 17 | "pageTitle": "Clipboard - Web APIs | MDN", 18 | "parents": [ 19 | { "uri": "/en-US/docs/Web", "title": "Web" }, 20 | { "uri": "/en-US/docs/Web/API", "title": "Web APIs" }, 21 | { "uri": "/en-US/docs/Web/API/Clipboard", "title": "Clipboard" } 22 | ], 23 | "popularity": 0.003337452204028022, 24 | "short_title": "Clipboard", 25 | "source": { 26 | "folder": "en-us/web/api/clipboard", 27 | "github_url": "https://github.com/mdn/content/blob/main/files/en-us/web/api/clipboard/index.md", 28 | "last_commit_url": "https://github.com/mdn/content/commit/7087ffd50a4d81d1b91fe603c26456e9ce398574", 29 | "filename": "index.md" 30 | }, 31 | "summary": "The Clipboard interface of the Clipboard API provides read and write access to the contents of the system clipboard.\nThis allows a web application to implement cut, copy, and paste features.", 32 | "title": "Clipboard", 33 | "baseline": { 34 | "baseline": "high", 35 | "baseline_low_date": "2020-03-24", 36 | "baseline_high_date": "2022-09-24", 37 | "support": { 38 | "chrome": "66", 39 | "chrome_android": "66", 40 | "edge": "79", 41 | "firefox": "63", 42 | "firefox_android": "63", 43 | "safari": "13.1", 44 | "safari_ios": "13.4" 45 | }, 46 | "asterisk": true, 47 | "feature": { 48 | "status": { 49 | "baseline": "low", 50 | "baseline_low_date": "2024-06-11", 51 | "support": { 52 | "chrome": "76", 53 | "chrome_android": "76", 54 | "edge": "79", 55 | "firefox": "127", 56 | "firefox_android": "127", 57 | "safari": "13.1", 58 | "safari_ios": "13.4" 59 | } 60 | }, 61 | "description_html": "The navigator.clipboard API asynchronously reads and writes to the system clipboard.", 62 | "name": "Async clipboard" 63 | } 64 | }, 65 | "browserCompat": ["api.Clipboard"], 66 | "pageType": "web-api-interface", 67 | "hash": "3c15dae7979d10c231d9403b06e248125738985ac339190494cb61d921750a49" 68 | } 69 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | 4 | import { includeIgnoreFile } from "@eslint/compat"; 5 | import js from "@eslint/js"; 6 | import { defineConfig } from "eslint/config"; 7 | import prettierConfig from "eslint-config-prettier/flat"; 8 | import importPlugin from "eslint-plugin-import"; 9 | import jsdoc from "eslint-plugin-jsdoc"; 10 | import n from "eslint-plugin-n"; 11 | import unicorn from "eslint-plugin-unicorn"; 12 | import tseslint from "typescript-eslint"; 13 | 14 | const __filename = fileURLToPath(import.meta.url); 15 | const __dirname = path.dirname(__filename); 16 | const gitignorePath = path.resolve(__dirname, ".gitignore"); 17 | 18 | export default defineConfig([ 19 | includeIgnoreFile(gitignorePath), 20 | jsdoc.configs["flat/recommended"], 21 | n.configs["flat/recommended"], 22 | unicorn.configs["recommended"], 23 | { files: ["**/*.{js,mjs,cjs}"] }, 24 | { 25 | files: ["**/*.{js,mjs,cjs}"], 26 | plugins: { js }, 27 | extends: ["js/recommended"], 28 | }, 29 | { 30 | files: ["**/*.{js,mjs,cjs}"], 31 | plugins: { "@typescript-eslint": tseslint.plugin }, 32 | rules: { 33 | "@typescript-eslint/ban-ts-comment": [ 34 | "error", 35 | { 36 | "ts-expect-error": false, 37 | }, 38 | ], 39 | }, 40 | }, 41 | { 42 | rules: { 43 | "no-unused-vars": [ 44 | "error", 45 | { 46 | argsIgnorePattern: "^_", 47 | varsIgnorePattern: "^_", 48 | }, 49 | ], 50 | "jsdoc/no-undefined-types": "off", 51 | "jsdoc/require-jsdoc": "off", 52 | "jsdoc/require-param-description": "off", 53 | "jsdoc/require-param-type": "off", 54 | "jsdoc/require-returns": "off", 55 | "jsdoc/require-returns-description": "off", 56 | "jsdoc/require-returns-type": "off", 57 | "jsdoc/tag-lines": "off", 58 | "jsdoc/check-tag-names": [ 59 | "error", 60 | { definedTags: ["element", "attr", "slot"] }, 61 | ], 62 | "n/no-missing-import": "off", 63 | "n/no-unsupported-features/node-builtins": ["off"], 64 | "n/no-unpublished-import": "off", 65 | "unicorn/no-array-reverse": "off", 66 | "unicorn/no-array-sort": "off", 67 | "unicorn/no-array-callback-reference": "off", 68 | "unicorn/no-null": ["off"], 69 | "unicorn/prevent-abbreviations": ["off"], 70 | "unicorn/switch-case-braces": "off", 71 | "unicorn/template-indent": ["off"], 72 | }, 73 | }, 74 | { 75 | files: ["**/*.{js,mjs,cjs}"], 76 | plugins: { import: importPlugin }, 77 | rules: { 78 | "sort-imports": "off", 79 | "import/order": [ 80 | "error", 81 | { 82 | alphabetize: { 83 | order: "asc", 84 | }, 85 | named: true, 86 | "newlines-between": "always-and-inside-groups", 87 | }, 88 | ], 89 | }, 90 | }, 91 | prettierConfig, 92 | ]); 93 | -------------------------------------------------------------------------------- /tools/get-doc.js: -------------------------------------------------------------------------------- 1 | import { html, render } from "@lit-labs/ssr"; 2 | import { collectResult } from "@lit-labs/ssr/lib/render-result.js"; 3 | // @ts-expect-error: fred needs to expose types 4 | import { ContentSection } from "@mdn/fred/components/content-section/server.js"; 5 | // @ts-expect-error 6 | import { asyncLocalStorage } from "@mdn/fred/components/server/async-local-storage.js"; 7 | // @ts-expect-error 8 | import { runWithContext } from "@mdn/fred/symmetric-context/server.js"; 9 | import TurndownService from "turndown"; 10 | // @ts-expect-error 11 | import turndownPluginGfm from "turndown-plugin-gfm"; 12 | import z from "zod"; 13 | 14 | import { NonSentryError } from "../sentry/error.js"; 15 | import server from "../server.js"; 16 | 17 | const turndownService = new TurndownService({ 18 | headingStyle: "atx", 19 | codeBlockStyle: "fenced", 20 | }); 21 | turndownService.use(turndownPluginGfm.gfm); 22 | 23 | server.registerTool( 24 | "get-doc", 25 | { 26 | title: "Get documentation", 27 | description: "Retrieve a page of MDN documentation as markdown.", 28 | inputSchema: { 29 | path: z 30 | .string() 31 | .describe("path or full URL: e.g. '/en-US/docs/Web/API/Headers'"), 32 | }, 33 | }, 34 | async ({ path }) => { 35 | const url = new URL(path, "https://developer.mozilla.org"); 36 | if (url.host !== "developer.mozilla.org") { 37 | throw new NonSentryError(`Error: ${url} doesn't look like an MDN url`); 38 | } 39 | if (!/^\/?([a-z-]+?\/)?docs\//i.test(url.pathname)) { 40 | throw new NonSentryError( 41 | `Error: ${path} doesn't look like the path to a piece of MDN documentation`, 42 | ); 43 | } 44 | if (!url.pathname.endsWith("/index.json")) { 45 | url.pathname += "/index.json"; 46 | } 47 | 48 | const res = await fetch(url); 49 | if (!res.ok) { 50 | if (res.status === 404) { 51 | throw new NonSentryError(`Error: We couldn't find ${path}`); 52 | } 53 | throw new Error(`${res.status}: ${res.statusText} for ${path}`); 54 | } 55 | 56 | /** @type {import("@mdn/rari").DocPage} */ 57 | const json = await res.json(); 58 | const context = { 59 | ...json, 60 | // @ts-expect-error 61 | l10n: (x) => x, 62 | }; 63 | // TODO: expose better API for this from fred 64 | const renderedHtml = await collectResult( 65 | render( 66 | await asyncLocalStorage.run( 67 | { 68 | componentsUsed: new Set(), 69 | componentsWithStylesInHead: new Set(), 70 | }, 71 | () => 72 | runWithContext( 73 | {}, 74 | () => html` 75 |

${context.doc.title}

76 | ${context.doc.body?.map((section) => 77 | new ContentSection().render(context, section), 78 | )} 79 | `, 80 | ), 81 | ), 82 | ), 83 | ); 84 | const markdown = turndownService.turndown(renderedHtml); 85 | 86 | let frontmatter = ""; 87 | const { browserCompat } = context.doc; 88 | if (browserCompat) { 89 | frontmatter += "---\n"; 90 | if (browserCompat.length > 1) { 91 | frontmatter += "compat-keys:\n"; 92 | frontmatter += browserCompat.map((key) => ` - ${key}\n`).join(""); 93 | } else { 94 | frontmatter += `compat-key: ${browserCompat[0]}\n`; 95 | } 96 | frontmatter += "---\n"; 97 | } 98 | 99 | return { 100 | content: [ 101 | { 102 | type: "text", 103 | text: frontmatter + markdown, 104 | }, 105 | ], 106 | }; 107 | }, 108 | ); 109 | -------------------------------------------------------------------------------- /test/tools/get-compat.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/reject-any-type */ 2 | import assert from "node:assert/strict"; 3 | import { after, before, describe, it } from "node:test"; 4 | 5 | import { MockAgent, setGlobalDispatcher } from "undici"; 6 | 7 | import arrayCompat from "../fixtures/bcd-array.json" with { type: "json" }; 8 | import { createClient, createServer } from "../helpers/client.js"; 9 | 10 | describe("get-compat tool", () => { 11 | /** @type {Awaited>} */ 12 | let server; 13 | /** @type {Awaited>} */ 14 | let client; 15 | /** @type {import("undici").MockPool} */ 16 | let mockPool; 17 | 18 | before(async () => { 19 | server = await createServer(); 20 | client = await createClient(server.port); 21 | 22 | const agent = new MockAgent(); 23 | setGlobalDispatcher(agent); 24 | // only allow unmocked requests to the mcp server: 25 | agent.enableNetConnect(`localhost:${server.port}`); 26 | mockPool = agent.get("https://bcd.developer.mozilla.org"); 27 | }); 28 | 29 | it("should return bcd data", async () => { 30 | const key = "javascript.builtins.Array.Array"; 31 | 32 | mockPool 33 | .intercept({ 34 | path: `/bcd/api/v0/current/${key}.json`, 35 | method: "GET", 36 | }) 37 | .reply(200, arrayCompat); 38 | 39 | /** @type {any} */ 40 | const { content } = await client.callTool({ 41 | name: "get-compat", 42 | arguments: { 43 | key, 44 | }, 45 | }); 46 | /** @type {string} */ 47 | const text = content[0].text; 48 | const data = JSON.parse(text); 49 | assert.deepEqual(data.__compat.support.firefox[0].version_added, "1"); 50 | }); 51 | 52 | it("should handle invalid key", async () => { 53 | const key = "javascript.builtins.Array.foobar"; 54 | 55 | mockPool 56 | .intercept({ 57 | path: `/bcd/api/v0/current/${key}.json`, 58 | method: "GET", 59 | }) 60 | .reply(404); 61 | 62 | /** @type {any} */ 63 | const { content } = await client.callTool({ 64 | name: "get-compat", 65 | arguments: { 66 | key, 67 | }, 68 | }); 69 | /** @type {string} */ 70 | const text = content[0].text; 71 | assert.ok(text.startsWith("Error:"), "response starts with 'Error:'"); 72 | assert.ok(text.includes(key), "response includes key"); 73 | }); 74 | 75 | it("should handle invalid root key", async () => { 76 | const key = "foobar"; 77 | 78 | mockPool 79 | .intercept({ 80 | path: `/bcd/api/v0/current/${key}.json`, 81 | method: "GET", 82 | }) 83 | .reply(404); 84 | 85 | /** @type {any} */ 86 | const { content } = await client.callTool({ 87 | name: "get-compat", 88 | arguments: { 89 | key, 90 | }, 91 | }); 92 | /** @type {string} */ 93 | const text = content[0].text; 94 | assert.ok(text.startsWith("Error:"), "response starts with 'Error:'"); 95 | assert.ok(text.includes(key), "response includes key"); 96 | }); 97 | 98 | it("should handle bcd api server error", async () => { 99 | const key = "javascript.builtins.Array.Array"; 100 | 101 | mockPool 102 | .intercept({ 103 | path: `/bcd/api/v0/current/${key}.json`, 104 | method: "GET", 105 | }) 106 | .reply(500); 107 | 108 | /** @type {any} */ 109 | const { content, isError } = await client.callTool({ 110 | name: "get-compat", 111 | arguments: { 112 | key, 113 | }, 114 | }); 115 | 116 | assert.deepEqual(isError, true); 117 | /** @type {string} */ 118 | const text = content[0].text; 119 | assert.ok(text.startsWith("Error:"), "response starts with 'Error:'"); 120 | }); 121 | 122 | after(() => { 123 | server.listener.close(); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/tools/search.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/reject-any-type */ 2 | import assert from "node:assert/strict"; 3 | import { after, before, describe, it } from "node:test"; 4 | 5 | import { MockAgent, setGlobalDispatcher } from "undici"; 6 | 7 | import clipboardApiMetadata from "../fixtures/clipboard-api-metadata.json" with { type: "json" }; 8 | import clipboardMetadata from "../fixtures/clipboard-metadata.json" with { type: "json" }; 9 | import searchResultEmpty from "../fixtures/search-result-empty.json" with { type: "json" }; 10 | import searchResult from "../fixtures/search-result.json" with { type: "json" }; 11 | import { createClient, createServer } from "../helpers/client.js"; 12 | 13 | describe("search tool", () => { 14 | /** @type {Awaited>} */ 15 | let server; 16 | /** @type {Awaited>} */ 17 | let client; 18 | /** @type {import("undici").MockPool} */ 19 | let mockPool; 20 | 21 | before(async () => { 22 | server = await createServer(); 23 | client = await createClient(server.port); 24 | 25 | const agent = new MockAgent(); 26 | setGlobalDispatcher(agent); 27 | // only allow unmocked requests to the mcp server: 28 | agent.enableNetConnect(`localhost:${server.port}`); 29 | mockPool = agent.get("https://developer.mozilla.org"); 30 | }); 31 | 32 | it("should return results", async () => { 33 | mockPool 34 | .intercept({ 35 | path: "/api/v1/search?q=clipboard+api", 36 | method: "GET", 37 | }) 38 | .reply(200, searchResult); 39 | mockPool 40 | .intercept({ 41 | path: "/en-US/docs/Web/API/Clipboard/metadata.json", 42 | method: "GET", 43 | }) 44 | .reply(500); 45 | 46 | /** @type {any} */ 47 | const { content } = await client.callTool({ 48 | name: "search", 49 | arguments: { 50 | query: "clipboard api", 51 | }, 52 | }); 53 | /** @type {string} */ 54 | const text = content[0].text; 55 | assert.ok( 56 | text.includes("/en-US/docs/Web/API/Clipboard_API"), 57 | "includes result url", 58 | ); 59 | assert.ok(text.includes("# Clipboard API"), "includes result title"); 60 | assert.ok( 61 | text.includes( 62 | "The Clipboard API provides the ability to respond to clipboard commands (cut, copy, and paste), as well as to asynchronously read from and write to the system clipboard.", 63 | ), 64 | "includes result summary", 65 | ); 66 | }); 67 | 68 | it("should gracefully handle no results", async () => { 69 | const query = "testempty"; 70 | mockPool 71 | .intercept({ 72 | path: `/api/v1/search?q=${query}`, 73 | method: "GET", 74 | }) 75 | .reply(200, searchResultEmpty); 76 | 77 | /** @type {any} */ 78 | const { content } = await client.callTool({ 79 | name: "search", 80 | arguments: { 81 | query, 82 | }, 83 | }); 84 | /** @type {string} */ 85 | const text = content[0].text; 86 | assert.ok(text.includes(query), "response includes query"); 87 | assert.ok( 88 | text.toLowerCase().includes("no results"), 89 | "response mentions no results", 90 | ); 91 | }); 92 | 93 | it("should gracefully handle server error", async () => { 94 | const query = "error"; 95 | mockPool 96 | .intercept({ 97 | path: `/api/v1/search?q=${query}`, 98 | method: "GET", 99 | }) 100 | .reply(502); 101 | 102 | /** @type {any} */ 103 | const { content } = await client.callTool({ 104 | name: "search", 105 | arguments: { 106 | query, 107 | }, 108 | }); 109 | /** @type {string} */ 110 | const text = content[0].text; 111 | assert.ok(text.includes("502"), "response includes error code"); 112 | assert.ok(text.includes(query), "response includes query"); 113 | assert.ok(text.includes("try again"), "response suggests next action"); 114 | }); 115 | 116 | it("should include single compat key", async () => { 117 | mockPool 118 | .intercept({ 119 | path: "/api/v1/search?q=clipboard+api", 120 | method: "GET", 121 | }) 122 | .reply(200, searchResult); 123 | mockPool 124 | .intercept({ 125 | path: "/en-US/docs/Web/API/Clipboard/metadata.json", 126 | method: "GET", 127 | }) 128 | .reply(200, clipboardMetadata); 129 | 130 | /** @type {any} */ 131 | const { content } = await client.callTool({ 132 | name: "search", 133 | arguments: { 134 | query: "clipboard api", 135 | }, 136 | }); 137 | /** @type {string} */ 138 | const text = content[0].text; 139 | assert.ok( 140 | text.includes("`compat-key`: `api.Clipboard`"), 141 | "includes compat key", 142 | ); 143 | }); 144 | 145 | it("should include multiple compat keys", async () => { 146 | mockPool 147 | .intercept({ 148 | path: "/api/v1/search?q=clipboard+api", 149 | method: "GET", 150 | }) 151 | .reply(200, searchResult); 152 | mockPool 153 | .intercept({ 154 | path: "/en-US/docs/Web/API/Clipboard_API/metadata.json", 155 | method: "GET", 156 | }) 157 | .reply(200, clipboardApiMetadata); 158 | 159 | /** @type {any} */ 160 | const { content } = await client.callTool({ 161 | name: "search", 162 | arguments: { 163 | query: "clipboard api", 164 | }, 165 | }); 166 | /** @type {string} */ 167 | const text = content[0].text; 168 | assert.ok( 169 | text.includes( 170 | "`compat-keys`: `api.Clipboard`, `api.ClipboardEvent`, `api.ClipboardItem`", 171 | ), 172 | "includes compat keys", 173 | ); 174 | }); 175 | 176 | after(() => { 177 | server.listener.close(); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /test/tools/get-doc.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/reject-any-type */ 2 | import assert from "node:assert/strict"; 3 | import { after, before, beforeEach, describe, it } from "node:test"; 4 | 5 | import { MockAgent, setGlobalDispatcher } from "undici"; 6 | 7 | import clipboardDoc from "../fixtures/clipboard-api.json" with { type: "json" }; 8 | import glossaryDoc from "../fixtures/glossary.json" with { type: "json" }; 9 | import headersDoc from "../fixtures/headers.json" with { type: "json" }; 10 | import kitchensinkDoc from "../fixtures/kitchensink.json" with { type: "json" }; 11 | import { createClient, createServer } from "../helpers/client.js"; 12 | 13 | describe("get-doc tool", () => { 14 | /** @type {Awaited>} */ 15 | let server; 16 | /** @type {Awaited>} */ 17 | let client; 18 | /** @type {import("undici").MockPool} */ 19 | let mockPool; 20 | 21 | before(async () => { 22 | server = await createServer(); 23 | client = await createClient(server.port); 24 | 25 | const agent = new MockAgent(); 26 | setGlobalDispatcher(agent); 27 | // only allow unmocked requests to the mcp server: 28 | agent.enableNetConnect(`localhost:${server.port}`); 29 | mockPool = agent.get("https://developer.mozilla.org"); 30 | }); 31 | 32 | describe("path param", () => { 33 | beforeEach(() => { 34 | mockPool 35 | .intercept({ 36 | path: "/en-US/docs/MDN/Kitchensink/index.json", 37 | method: "GET", 38 | }) 39 | .reply(200, kitchensinkDoc); 40 | }); 41 | 42 | it("should accept normal path", async () => { 43 | /** @type {any} */ 44 | const { content } = await client.callTool({ 45 | name: "get-doc", 46 | arguments: { 47 | path: "/en-US/docs/MDN/Kitchensink", 48 | }, 49 | }); 50 | const [_, text] = frontmatterSplit(content[0].text); 51 | assert.ok(text.startsWith("# The MDN Content Kitchensink")); 52 | }); 53 | 54 | it("should accept full path", async () => { 55 | /** @type {any} */ 56 | const { content } = await client.callTool({ 57 | name: "get-doc", 58 | arguments: { 59 | path: "https://developer.mozilla.org/en-US/docs/MDN/Kitchensink", 60 | }, 61 | }); 62 | const [_, text] = frontmatterSplit(content[0].text); 63 | assert.ok(text.startsWith("# The MDN Content Kitchensink")); 64 | }); 65 | 66 | it("should accept path with index.json", async () => { 67 | /** @type {any} */ 68 | const { content } = await client.callTool({ 69 | name: "get-doc", 70 | arguments: { 71 | path: "/en-US/docs/MDN/Kitchensink/index.json", 72 | }, 73 | }); 74 | const [_, text] = frontmatterSplit(content[0].text); 75 | assert.ok(text.startsWith("# The MDN Content Kitchensink")); 76 | }); 77 | 78 | it("should accept path without locale", async () => { 79 | mockPool 80 | .intercept({ 81 | path: "/docs/MDN/Kitchensink/index.json", 82 | method: "GET", 83 | }) 84 | .reply(302, "", { 85 | headers: { 86 | location: "/en-US/docs/MDN/Kitchensink/index.json", 87 | }, 88 | }); 89 | 90 | /** @type {any} */ 91 | const { content } = await client.callTool({ 92 | name: "get-doc", 93 | arguments: { 94 | path: "/docs/MDN/Kitchensink", 95 | }, 96 | }); 97 | const [_, text] = frontmatterSplit(content[0].text); 98 | assert.ok(text.startsWith("# The MDN Content Kitchensink")); 99 | }); 100 | 101 | it("should accept path without leading slash", async () => { 102 | /** @type {any} */ 103 | const { content } = await client.callTool({ 104 | name: "get-doc", 105 | arguments: { 106 | path: "en-US/docs/MDN/Kitchensink", 107 | }, 108 | }); 109 | const [_, text] = frontmatterSplit(content[0].text); 110 | assert.ok(text.startsWith("# The MDN Content Kitchensink")); 111 | }); 112 | 113 | it("should reject wrong base url", async () => { 114 | const path = "https://example.com/en-US/docs/MDN/Kitchensink"; 115 | /** @type {any} */ 116 | const { content } = await client.callTool({ 117 | name: "get-doc", 118 | arguments: { 119 | path, 120 | }, 121 | }); 122 | /** @type {string} */ 123 | const text = content[0].text; 124 | assert.deepEqual(text, `Error: ${path} doesn't look like an MDN url`); 125 | }); 126 | 127 | it("should handle path to missing doc", async () => { 128 | const path = "/en-US/docs/MDN/Knisnehctik"; 129 | 130 | mockPool 131 | .intercept({ 132 | path: path + "/index.json", 133 | method: "GET", 134 | }) 135 | .reply(404); 136 | 137 | /** @type {any} */ 138 | const { content } = await client.callTool({ 139 | name: "get-doc", 140 | arguments: { 141 | path, 142 | }, 143 | }); 144 | /** @type {string} */ 145 | const text = content[0].text; 146 | assert.deepEqual(text, `Error: We couldn't find ${path}`); 147 | }); 148 | 149 | it("should handle upstream server error", async () => { 150 | const path = "/en-US/docs/MDN/Knisnehctik"; 151 | 152 | mockPool 153 | .intercept({ 154 | path: path + "/index.json", 155 | method: "GET", 156 | }) 157 | .reply(500); 158 | 159 | /** @type {any} */ 160 | const { content } = await client.callTool({ 161 | name: "get-doc", 162 | arguments: { 163 | path, 164 | }, 165 | }); 166 | /** @type {string} */ 167 | const text = content[0].text; 168 | assert.deepEqual(text, `500: Internal Server Error for ${path}`); 169 | }); 170 | 171 | it("should reject non-doc path", async () => { 172 | const path = "/en-US/observatory"; 173 | /** @type {any} */ 174 | const { content } = await client.callTool({ 175 | name: "get-doc", 176 | arguments: { 177 | path, 178 | }, 179 | }); 180 | /** @type {string} */ 181 | const text = content[0].text; 182 | assert.deepEqual( 183 | text, 184 | `Error: ${path} doesn't look like the path to a piece of MDN documentation`, 185 | ); 186 | }); 187 | }); 188 | 189 | it("should work with real document", async () => { 190 | mockPool 191 | .intercept({ 192 | path: "/en-US/docs/Web/API/Headers/index.json", 193 | method: "GET", 194 | }) 195 | .reply(200, headersDoc); 196 | 197 | /** @type {any} */ 198 | const { content, isError } = await client.callTool({ 199 | name: "get-doc", 200 | arguments: { 201 | path: "/en-US/docs/Web/API/Headers", 202 | }, 203 | }); 204 | assert.equal(isError, undefined); 205 | const [_, text] = frontmatterSplit(content[0].text); 206 | assert.ok(text.startsWith("# Headers")); 207 | }); 208 | 209 | describe("bcd keys", () => { 210 | it("should have none", async () => { 211 | mockPool 212 | .intercept({ 213 | path: "/en-US/docs/Glossary/index.json", 214 | method: "GET", 215 | }) 216 | .reply(200, glossaryDoc); 217 | 218 | /** @type {any} */ 219 | const { content } = await client.callTool({ 220 | name: "get-doc", 221 | arguments: { 222 | path: "/en-US/docs/Glossary", 223 | }, 224 | }); 225 | /** @type {string} */ 226 | const text = content[0].text; 227 | assert.ok( 228 | text.startsWith("# Glossary of web terms"), 229 | "has no frontmatter", 230 | ); 231 | }); 232 | 233 | it("should have one", async () => { 234 | mockPool 235 | .intercept({ 236 | path: "/en-US/docs/Web/API/Headers/index.json", 237 | method: "GET", 238 | }) 239 | .reply(200, headersDoc); 240 | 241 | /** @type {any} */ 242 | const { content, isError } = await client.callTool({ 243 | name: "get-doc", 244 | arguments: { 245 | path: "/en-US/docs/Web/API/Headers", 246 | }, 247 | }); 248 | assert.equal(isError, undefined); 249 | const [frontmatter] = frontmatterSplit(content[0].text); 250 | assert.ok( 251 | frontmatter?.split("\n").includes("compat-key: api.Headers"), 252 | "frontmatter includes bcd key", 253 | ); 254 | }); 255 | 256 | it("should have multiple", async () => { 257 | mockPool 258 | .intercept({ 259 | path: "/en-US/docs/Web/API/Clipboard_API/index.json", 260 | method: "GET", 261 | }) 262 | .reply(200, clipboardDoc); 263 | 264 | /** @type {any} */ 265 | const { content } = await client.callTool({ 266 | name: "get-doc", 267 | arguments: { 268 | path: "/en-US/docs/Web/API/Clipboard_API", 269 | }, 270 | }); 271 | const [frontmatter] = frontmatterSplit(content[0].text); 272 | const lines = frontmatter?.split("\n"); 273 | assert.partialDeepStrictEqual(lines, [ 274 | "compat-keys:", 275 | " - api.Clipboard", 276 | " - api.ClipboardEvent", 277 | " - api.ClipboardItem", 278 | ]); 279 | }); 280 | }); 281 | 282 | after(() => { 283 | server.listener.close(); 284 | }); 285 | }); 286 | 287 | /** 288 | * @param {string} text 289 | * @returns {[string?, string]} 290 | */ 291 | function frontmatterSplit(text) { 292 | const delim = "---\n"; 293 | const [frontmatter, ...remainder] = text.split(delim).slice(1); 294 | return text.startsWith(delim) 295 | ? [frontmatter, remainder.join(delim)] 296 | : [undefined, text]; 297 | } 298 | -------------------------------------------------------------------------------- /test/fixtures/search-result.json: -------------------------------------------------------------------------------- 1 | { 2 | "documents": [ 3 | { 4 | "mdn_url": "/en-US/docs/Web/API/Clipboard_API", 5 | "score": 251.86737, 6 | "title": "Clipboard API", 7 | "locale": "en-us", 8 | "slug": "web/api/clipboard_api", 9 | "popularity": 0.003453498036297062, 10 | "summary": "The Clipboard API provides the ability to respond to clipboard commands (cut, copy, and paste), as well as to asynchronously read from and write to the system clipboard.", 11 | "highlight": { 12 | "body": [ 13 | "The specification refers to this as the 'Async Clipboard API'.", 14 | "The specification refers to this as the 'Clipboard Event API'.", 15 | "The Clipboard API extends the following APIs, adding the listed features." 16 | ], 17 | "title": ["Clipboard API"] 18 | } 19 | }, 20 | { 21 | "mdn_url": "/en-US/docs/Mozilla/Add-ons/WebExtensions/API/clipboard", 22 | "score": 91.86381, 23 | "title": "clipboard", 24 | "locale": "en-us", 25 | "slug": "mozilla/add-ons/webextensions/api/clipboard", 26 | "popularity": 0.0003085911517321947, 27 | "summary": "The WebExtension clipboard API (which is different from the standard Clipboard API) enables an extension to copy items to the system clipboard. Currently the WebExtension clipboard API only supports copying images, but it's intended to support copying text and HTML in the future.", 28 | "highlight": { 29 | "body": [ 30 | "The WebExtension clipboard API (which is different from the standard Clipboard API) enables an extension to copy items to", 31 | "The WebExtension clipboard API may be deprecated once the standard Clipboard API's support for non-text clipboard contents", 32 | "Note:\nThis API is based on Chromium's chrome.clipboard API." 33 | ], 34 | "title": ["clipboard"] 35 | } 36 | }, 37 | { 38 | "mdn_url": "/en-US/docs/Web/API/Clipboard", 39 | "score": 88.80422, 40 | "title": "Clipboard", 41 | "locale": "en-us", 42 | "slug": "web/api/clipboard", 43 | "popularity": 0.003337452204028022, 44 | "summary": "The Clipboard interface of the Clipboard API provides read and write access to the contents of the system clipboard.\nThis allows a web application to implement cut, copy, and paste features.", 45 | "highlight": { 46 | "body": [ 47 | "The Clipboard interface of the Clipboard API provides read and write access to the contents of the system clipboard.", 48 | "All of the Clipboard API methods operate asynchronously; they return a Promise which is resolved once the clipboard access", 49 | "Additional requirements for using the API are discussed in the Security consideration section of the API overview topic." 50 | ], 51 | "title": ["Clipboard"] 52 | } 53 | }, 54 | { 55 | "mdn_url": "/en-US/docs/Web/API/ClipboardItem/ClipboardItem", 56 | "score": 84.08948, 57 | "title": "ClipboardItem: ClipboardItem() constructor", 58 | "locale": "en-us", 59 | "slug": "web/api/clipboarditem/clipboarditem", 60 | "popularity": 0.00015559217734396373, 61 | "summary": "The ClipboardItem() constructor creates a new ClipboardItem object, which represents data to be stored or retrieved via the Clipboard API clipboard.write() and clipboard.read() methods, respectively.", 62 | "highlight": { 63 | "body": [ 64 | "Clipboard API clipboard.write() and clipboard.read() methods, respectively.", 65 | "See the browser compatibility table for the Clipboard interface.\njsnew ClipboardItem(data)\nnew ClipboardItem(data, options", 66 | "not supported");\n}\n} catch (err) {\nconsole.error(err.name, err.message);\n}\n}\nClipboard API\nImage support for Async Clipboard" 67 | ], 68 | "title": [ 69 | "ClipboardItem: ClipboardItem() constructor" 70 | ] 71 | } 72 | }, 73 | { 74 | "mdn_url": "/en-US/docs/Web/API/ClipboardItem", 75 | "score": 83.07362, 76 | "title": "ClipboardItem", 77 | "locale": "en-us", 78 | "slug": "web/api/clipboarditem", 79 | "popularity": 0.0008777992005155287, 80 | "summary": "The ClipboardItem interface of the Clipboard API represents a single item format, used when reading or writing clipboard data using Clipboard.read() and Clipboard.write() respectively.", 81 | "highlight": { 82 | "body": [ 83 | "The ClipboardItem interface of the Clipboard API represents a single item format, used when reading or writing clipboard", 84 | "If it is, we fetch an SVG image with the Fetch API, and then read it into a Blob, which we can use to create a ClipboardItem", 85 | "now use blob here\n}\n}\n} catch (err) {\nconsole.error(err.name, err.message);\n}\n}\nClipboard API\nImage support for Async Clipboard" 86 | ], 87 | "title": ["ClipboardItem"] 88 | } 89 | }, 90 | { 91 | "mdn_url": "/en-US/docs/Web/API/ClipboardEvent", 92 | "score": 82.68033, 93 | "title": "ClipboardEvent", 94 | "locale": "en-us", 95 | "slug": "web/api/clipboardevent", 96 | "popularity": 0.0005341998088809421, 97 | "summary": "The ClipboardEvent interface of the Clipboard API represents events providing information related to modification of the clipboard, that is cut, copy, and paste events.", 98 | "highlight": { 99 | "body": [ 100 | "The ClipboardEvent interface of the Clipboard API represents events providing information related to modification of the", 101 | "Event\nClipboardEvent\nClipboardEvent()\nCreates a ClipboardEvent event with the given parameters.", 102 | "Copy-related events: copy, cut, paste\nClipboard API\nImage support for Async Clipboard article" 103 | ], 104 | "title": ["ClipboardEvent"] 105 | } 106 | }, 107 | { 108 | "mdn_url": "/en-US/docs/Web/API/ClipboardEvent/ClipboardEvent", 109 | "score": 80.660736, 110 | "title": "ClipboardEvent: ClipboardEvent() constructor", 111 | "locale": "en-us", 112 | "slug": "web/api/clipboardevent/clipboardevent", 113 | "popularity": 0.00009400360714531144, 114 | "summary": "The ClipboardEvent() constructor returns a new ClipboardEvent, representing an event providing information related to modification of the clipboard, that is cut, copy, and paste events.", 115 | "highlight": { 116 | "body": [ 117 | "The ClipboardEvent() constructor returns a new ClipboardEvent, representing an event providing information related to modification", 118 | "of the clipboard, that is cut, copy, and paste events.\njsnew ClipboardEvent(type)\nnew ClipboardEvent(type, options)\ntype", 119 | "Clipboard API" 120 | ], 121 | "title": [ 122 | "ClipboardEvent: ClipboardEvent() constructor" 123 | ] 124 | } 125 | }, 126 | { 127 | "mdn_url": "/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard", 128 | "score": 80.57492, 129 | "title": "Interact with the clipboard", 130 | "locale": "en-us", 131 | "slug": "mozilla/add-ons/webextensions/interact_with_the_clipboard", 132 | "popularity": 0.0016285314562001535, 133 | "summary": "Working with the clipboard in extensions is transitioning from the Web API document.execCommand method (which is deprecated) to the navigator.clipboard method.", 134 | "highlight": { 135 | "body": [ 136 | "The Clipboard API writes arbitrary data to the clipboard from your extension.", 137 | "Using the API requires the permission "clipboardRead" or "clipboardWrite" in your manifest.json file.", 138 | "Clipboard API\nPermissions API\nMake content editable" 139 | ], 140 | "title": ["Interact with the clipboard"] 141 | } 142 | }, 143 | { 144 | "mdn_url": "/en-US/docs/Web/API/ClipboardEvent/clipboardData", 145 | "score": 80.278946, 146 | "title": "ClipboardEvent: clipboardData property", 147 | "locale": "en-us", 148 | "slug": "web/api/clipboardevent/clipboarddata", 149 | "popularity": 0.0007040546024814359, 150 | "summary": "The clipboardData property of the ClipboardEvent interface holds a DataTransfer object, which can be used to:", 151 | "highlight": { 152 | "body": [ 153 | "The clipboardData property of the ClipboardEvent interface holds a DataTransfer object, which can be used to:\nspecify what", 154 | "data should be put into the clipboard from the cut and copy event handlers, typically with a setData(format, data) call;", 155 | "Copy-related events: copy, cut, paste\nThe ClipboardEvent interface it belongs to.\nClipboard API" 156 | ], 157 | "title": [ 158 | "ClipboardEvent: clipboardData property" 159 | ] 160 | } 161 | }, 162 | { 163 | "mdn_url": "/en-US/docs/Web/API/Clipboard/read", 164 | "score": 78.55516, 165 | "title": "Clipboard: read() method", 166 | "locale": "en-us", 167 | "slug": "web/api/clipboard/read", 168 | "popularity": 0.0009063244330285888, 169 | "summary": "The read() method of the Clipboard interface requests a copy of the clipboard's contents, fulfilling the returned Promise with the data.", 170 | "highlight": { 171 | "body": [ 172 | "Additional security requirements are covered in the Security consideration section of the API overview topic.", 173 | "Clipboard API\nUnblocking clipboard access on web.dev\nUnsanitized HTML in the Async Clipboard API on developer.chrome.com", 174 | "Clipboard.readText()\nClipboard.writeText()\nClipboard.write()" 175 | ], 176 | "title": ["Clipboard: read() method"] 177 | } 178 | } 179 | ], 180 | "metadata": { 181 | "took_ms": 23, 182 | "size": 10, 183 | "page": 1, 184 | "total": { "value": 4197, "relation": "eq" } 185 | }, 186 | "suggestions": [] 187 | } 188 | -------------------------------------------------------------------------------- /test/fixtures/clipboard-api.json: -------------------------------------------------------------------------------- 1 | { 2 | "doc": { 3 | "body": [ 4 | { 5 | "type": "prose", 6 | "value": { 7 | "id": null, 8 | "title": null, 9 | "isH3": false, 10 | "content": "

The Clipboard API provides the ability to respond to clipboard commands (cut, copy, and paste), as well as to asynchronously read from and write to the system clipboard.

\n
\n

Note:\nUse this API in preference to the deprecated document.execCommand() method for accessing the clipboard.

\n
\n
\n

Note:\nThis API is not available in Web Workers (not exposed via WorkerNavigator).

\n
" 11 | } 12 | }, 13 | { 14 | "type": "prose", 15 | "value": { 16 | "id": "concepts_and_usage", 17 | "title": "Concepts and usage", 18 | "isH3": false, 19 | "content": "

The system clipboard is a data buffer belonging to the operating system hosting the browser, which is used for short-term data storage and/or data transfers between documents or applications.\nIt is usually implemented as an anonymous, temporary data buffer, sometimes called the paste buffer, that can be accessed from most or all programs within the environment via defined programming interfaces.

\n

The Clipboard API allows users to programmatically read and write text and other kinds of data to and from the system clipboard in secure contexts, provided the user has met the criteria outlined in the Security considerations.

\n

Events are fired as the result of cut, copy, and paste operations modifying the clipboard.\nThe events have a default action, for example the copy action copies the current selection to the system clipboard by default.\nThe default action can be overridden by the event handler — see each of the events for more information.

" 20 | } 21 | }, 22 | { 23 | "type": "prose", 24 | "value": { 25 | "id": "interfaces", 26 | "title": "Interfaces", 27 | "isH3": false, 28 | "content": "
\n
Clipboard Secure context
\n
\n

Provides an interface for reading and writing text and data to or from the system clipboard.\nThe specification refers to this as the 'Async Clipboard API'.

\n
\n
ClipboardEvent
\n
\n

Represents events providing information related to modification of the clipboard, that is cut, copy, and paste events.\nThe specification refers to this as the 'Clipboard Event API'.

\n
\n
ClipboardItem Secure context
\n
\n

Represents a single item format, used when reading or writing data.

\n
\n
" 29 | } 30 | }, 31 | { 32 | "type": "prose", 33 | "value": { 34 | "id": "extensions_to_other_interfaces", 35 | "title": "Extensions to other interfaces", 36 | "isH3": true, 37 | "content": "

The Clipboard API extends the following APIs, adding the listed features.

\n
\n
Navigator.clipboard Read only Secure context
\n
\n

Returns a Clipboard object that provides read and write access to the system clipboard.

\n
\n
Element copy event
\n
\n

An event fired whenever the user initiates a copy action.

\n
\n
Element cut event
\n
\n

An event fired whenever the user initiates a cut action.

\n
\n
Element paste event
\n
\n

An event fired whenever the user initiates a paste action.

\n
\n
\n" 38 | } 39 | }, 40 | { 41 | "type": "prose", 42 | "value": { 43 | "id": "security_considerations", 44 | "title": "Security considerations", 45 | "isH3": false, 46 | "content": "

The Clipboard API allows users to programmatically read and write text and other kinds of data to and from the system clipboard in secure contexts.

\n

When reading from the clipboard, the specification requires that a user has recently interacted with the page (transient user activation) and that the call is made as a result of the user interacting with a browser or OS \"paste element\" (such as choosing \"Paste\" on a native context menu). In practice, browsers often allow read operations that do not satisfy these requirements, while placing other requirements instead (such as a permission or per-operation prompt).\nFor writing to the clipboard the specification expects that the page has been granted the Permissions API clipboard-write permission, and the browser may also require transient user activation.\nBrowsers may place additional restrictions over use of the methods to access the clipboard.

\n

Browser implementations have diverged from the specification.\nThe differences are captured in the Browser compatibility section and the current state is summarized below:

\n

Chromium browsers:

\n
    \n
  • If a read isn't allowed by the spec and the document has focus, it triggers a request to use permission clipboard-read, and succeeds if the permission is granted (either because the user accepted the prompt, or because the permission was granted already).
  • \n
  • Writing requires either the clipboard-write permission or transient activation.\nIf the permission is granted, it persists, and further transient activation is not required.
  • \n
  • The HTTP Permissions-Policy permissions clipboard-read and clipboard-write must be allowed for <iframe> elements that access the clipboard.
  • \n
\n

Firefox & Safari:

\n
    \n
  • If a read isn't allowed by the spec but transient user activation is still met, it triggers a user prompt in the form of an ephemeral context menu with a single \"Paste\" option (which becomes enabled after 1 second) and succeeds if the user chooses the option.
  • \n
  • Writing requires transient activation.
  • \n
  • The paste-prompt is suppressed if reading same-origin clipboard content, but not cross-origin content.
  • \n
  • The clipboard-read and clipboard-write permissions are not supported (and not planned to be supported) by Firefox or Safari.
  • \n
\n

Firefox Web Extensions:

\n
    \n
  • Reading text is only available for extensions with the Web Extension clipboardRead permission.\nWith this permission the extension does not require transient activation or a paste prompt.
  • \n
  • Writing text is available in secure context and with transient activation.\nWith the Web Extension clipboardWrite permission transient activation is not required.
  • \n
" 47 | } 48 | }, 49 | { 50 | "type": "prose", 51 | "value": { 52 | "id": "examples", 53 | "title": "Examples", 54 | "isH3": false, 55 | "content": "" 56 | } 57 | }, 58 | { 59 | "type": "prose", 60 | "value": { 61 | "id": "accessing_the_clipboard", 62 | "title": "Accessing the clipboard", 63 | "isH3": true, 64 | "content": "

The system clipboard is accessed through the Navigator.clipboard global.

\n

This snippet fetches the text from the clipboard and appends it to the first element found with the class editor.\nSince readText() returns an empty string if the clipboard isn't text, this code is safe.

\n
js
navigator.clipboard\n  .readText()\n  .then(\n    (clipText) => (document.querySelector(\".editor\").innerText += clipText),\n  );\n
" 65 | } 66 | }, 67 | { 68 | "type": "specifications", 69 | "value": { 70 | "id": "specifications", 71 | "title": "Specifications", 72 | "isH3": false, 73 | "specifications": [ 74 | { 75 | "bcdSpecificationURL": "https://w3c.github.io/clipboard-apis/#clipboard-interface", 76 | "title": "Clipboard API and events" 77 | }, 78 | { 79 | "bcdSpecificationURL": "https://w3c.github.io/clipboard-apis/#clipboard-event-interfaces", 80 | "title": "Clipboard API and events" 81 | }, 82 | { 83 | "bcdSpecificationURL": "https://w3c.github.io/clipboard-apis/#clipboarditem", 84 | "title": "Clipboard API and events" 85 | } 86 | ], 87 | "query": "api.Clipboard,api.ClipboardEvent,api.ClipboardItem" 88 | } 89 | }, 90 | { 91 | "type": "prose", 92 | "value": { 93 | "id": "browser_compatibility", 94 | "title": "Browser compatibility", 95 | "isH3": false, 96 | "content": "" 97 | } 98 | }, 99 | { 100 | "type": "browser_compatibility", 101 | "value": { 102 | "id": "api.Clipboard", 103 | "title": "api.Clipboard", 104 | "isH3": true, 105 | "query": "api.Clipboard" 106 | } 107 | }, 108 | { 109 | "type": "browser_compatibility", 110 | "value": { 111 | "id": "api.ClipboardEvent", 112 | "title": "api.ClipboardEvent", 113 | "isH3": true, 114 | "query": "api.ClipboardEvent" 115 | } 116 | }, 117 | { 118 | "type": "browser_compatibility", 119 | "value": { 120 | "id": "api.ClipboardItem", 121 | "title": "api.ClipboardItem", 122 | "isH3": true, 123 | "query": "api.ClipboardItem" 124 | } 125 | }, 126 | { 127 | "type": "prose", 128 | "value": { 129 | "id": "see_also", 130 | "title": "See also", 131 | "isH3": false, 132 | "content": "" 133 | } 134 | } 135 | ], 136 | "isActive": true, 137 | "isMarkdown": true, 138 | "isTranslated": false, 139 | "locale": "en-US", 140 | "mdn_url": "/en-US/docs/Web/API/Clipboard_API", 141 | "modified": "2025-10-11T10:55:14.000Z", 142 | "native": "English (US)", 143 | "noIndexing": false, 144 | "other_translations": [ 145 | { "locale": "en-US", "title": "Clipboard API", "native": "English (US)" }, 146 | { "locale": "de", "title": "Clipboard API", "native": "Deutsch" }, 147 | { "locale": "es", "title": "API del portapapeles", "native": "Español" }, 148 | { "locale": "fr", "title": "API Clipboard", "native": "Français" }, 149 | { "locale": "ja", "title": "クリップボード API", "native": "日本語" }, 150 | { "locale": "ko", "title": "Clipboard API", "native": "한국어" }, 151 | { "locale": "ru", "title": "Clipboard API", "native": "Русский" }, 152 | { "locale": "zh-CN", "title": "Clipboard API", "native": "中文 (简体)" } 153 | ], 154 | "pageTitle": "Clipboard API - Web APIs | MDN", 155 | "parents": [ 156 | { "uri": "/en-US/docs/Web", "title": "Web" }, 157 | { "uri": "/en-US/docs/Web/API", "title": "Web APIs" }, 158 | { "uri": "/en-US/docs/Web/API/Clipboard_API", "title": "Clipboard API" } 159 | ], 160 | "popularity": 0.003453498036297062, 161 | "short_title": "Clipboard API", 162 | "sidebarHTML": "
  1. Clipboard API
  2. Interfaces
    1. Clipboard
    2. ClipboardEvent
    3. ClipboardItem
  3. Properties
    1. Navigator.clipboard
  4. Events
    1. Element: copy
    2. Element: cut
    3. Element: paste
", 163 | "source": { 164 | "folder": "en-us/web/api/clipboard_api", 165 | "github_url": "https://github.com/mdn/content/blob/main/files/en-us/web/api/clipboard_api/index.md", 166 | "last_commit_url": "https://github.com/mdn/content/commit/8452b3bfba185a471bc75f796f1b4f7f32cb453c", 167 | "filename": "index.md" 168 | }, 169 | "summary": "The Clipboard API provides the ability to respond to clipboard commands (cut, copy, and paste), as well as to asynchronously read from and write to the system clipboard.", 170 | "title": "Clipboard API", 171 | "toc": [ 172 | { "text": "Concepts and usage", "id": "concepts_and_usage" }, 173 | { "text": "Interfaces", "id": "interfaces" }, 174 | { "text": "Security considerations", "id": "security_considerations" }, 175 | { "text": "Examples", "id": "examples" }, 176 | { "text": "Specifications", "id": "specifications" }, 177 | { "text": "Browser compatibility", "id": "browser_compatibility" }, 178 | { "text": "See also", "id": "see_also" } 179 | ], 180 | "browserCompat": [ 181 | "api.Clipboard", 182 | "api.ClipboardEvent", 183 | "api.ClipboardItem" 184 | ], 185 | "pageType": "web-api-overview" 186 | }, 187 | "url": "/en-US/docs/Web/API/Clipboard_API", 188 | "renderer": "Doc" 189 | } 190 | -------------------------------------------------------------------------------- /test/fixtures/bcd-array.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "__compat": { 4 | "description": "Array() constructor", 5 | "mdn_url": "/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Array", 6 | "source_file": "javascript/builtins/Array.json", 7 | "spec_url": "https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array-constructor", 8 | "status": { 9 | "deprecated": false, 10 | "experimental": false, 11 | "standard_track": true 12 | }, 13 | "support": { 14 | "bun": [{ "version_added": "1.0.0", "release_date": "2023-09-08" }], 15 | "chrome": [{ "version_added": "1", "release_date": "2008-12-11" }], 16 | "chrome_android": [ 17 | { "version_added": "18", "release_date": "2012-06-27" } 18 | ], 19 | "deno": [{ "version_added": "1.0", "release_date": "2020-05-13" }], 20 | "edge": [{ "version_added": "12", "release_date": "2015-07-29" }], 21 | "firefox": [{ "version_added": "1", "release_date": "2004-11-09" }], 22 | "firefox_android": [ 23 | { "version_added": "4", "release_date": "2011-03-29" } 24 | ], 25 | "ie": [{ "version_added": "4", "release_date": "1997-09-30" }], 26 | "nodejs": [{ "version_added": "0.10.0", "release_date": "2013-03-11" }], 27 | "oculus": [{ "version_added": "5.0" }], 28 | "opera": [{ "version_added": "4", "release_date": "2000-06-28" }], 29 | "opera_android": [ 30 | { "version_added": "10.1", "release_date": "2010-11-09" } 31 | ], 32 | "safari": [{ "version_added": "1", "release_date": "2003-06-23" }], 33 | "safari_ios": [{ "version_added": "1", "release_date": "2007-06-29" }], 34 | "samsunginternet_android": [ 35 | { "version_added": "1.0", "release_date": "2013-04-27" } 36 | ], 37 | "webview_android": [ 38 | { "version_added": "1", "release_date": "2008-09-23" } 39 | ], 40 | "webview_ios": [{ "version_added": "1", "release_date": "2007-06-29" }] 41 | }, 42 | "tags": ["web-features:array", "web-features:snapshot:ecmascript-1"] 43 | } 44 | }, 45 | "query": "javascript.builtins.Array.Array", 46 | "browsers": { 47 | "bun": { 48 | "accepts_flags": true, 49 | "accepts_webextensions": false, 50 | "name": "Bun", 51 | "releases": { 52 | "1.3.2": { 53 | "engine": "WebKit", 54 | "engine_version": "623.1.7", 55 | "release_date": "2025-11-08", 56 | "release_notes": "https://bun.com/blog/release-notes/bun-v1.3.2", 57 | "status": "current" 58 | } 59 | }, 60 | "type": "server" 61 | }, 62 | "chrome": { 63 | "accepts_flags": true, 64 | "accepts_webextensions": true, 65 | "name": "Chrome", 66 | "pref_url": "chrome://flags", 67 | "preview_name": "Canary", 68 | "releases": { 69 | "142": { 70 | "engine": "Blink", 71 | "engine_version": "142", 72 | "release_date": "2025-10-28", 73 | "release_notes": "https://developer.chrome.com/release-notes/142", 74 | "status": "current" 75 | }, 76 | "143": { 77 | "engine": "Blink", 78 | "engine_version": "143", 79 | "release_date": "2025-12-02", 80 | "status": "beta" 81 | }, 82 | "144": { 83 | "engine": "Blink", 84 | "engine_version": "144", 85 | "release_date": "2026-01-13", 86 | "status": "nightly" 87 | }, 88 | "145": { 89 | "engine": "Blink", 90 | "engine_version": "145", 91 | "status": "planned" 92 | } 93 | }, 94 | "type": "desktop" 95 | }, 96 | "chrome_android": { 97 | "accepts_flags": true, 98 | "accepts_webextensions": false, 99 | "name": "Chrome Android", 100 | "pref_url": "chrome://flags", 101 | "releases": { 102 | "142": { 103 | "engine": "Blink", 104 | "engine_version": "142", 105 | "release_date": "2025-10-28", 106 | "release_notes": "https://developer.chrome.com/release-notes/142", 107 | "status": "current" 108 | }, 109 | "143": { 110 | "engine": "Blink", 111 | "engine_version": "143", 112 | "release_date": "2025-12-02", 113 | "status": "beta" 114 | }, 115 | "144": { 116 | "engine": "Blink", 117 | "engine_version": "144", 118 | "release_date": "2026-01-13", 119 | "status": "nightly" 120 | }, 121 | "145": { 122 | "engine": "Blink", 123 | "engine_version": "145", 124 | "status": "planned" 125 | } 126 | }, 127 | "type": "mobile", 128 | "upstream": "chrome" 129 | }, 130 | "deno": { 131 | "accepts_flags": true, 132 | "accepts_webextensions": false, 133 | "name": "Deno", 134 | "releases": { 135 | "2.5.0": { 136 | "engine": "V8", 137 | "engine_version": "14.0", 138 | "release_date": "2025-09-10", 139 | "release_notes": "https://github.com/denoland/deno/releases/tag/v2.5.0", 140 | "status": "current" 141 | } 142 | }, 143 | "type": "server" 144 | }, 145 | "edge": { 146 | "accepts_flags": true, 147 | "accepts_webextensions": true, 148 | "name": "Edge", 149 | "pref_url": "about:flags", 150 | "releases": { 151 | "142": { 152 | "engine": "Blink", 153 | "engine_version": "142", 154 | "release_date": "2025-10-31", 155 | "release_notes": "https://learn.microsoft.com/en-us/microsoft-edge/web-platform/release-notes/142", 156 | "status": "current" 157 | }, 158 | "143": { 159 | "engine": "Blink", 160 | "engine_version": "143", 161 | "release_date": "2025-12-04", 162 | "release_notes": "https://learn.microsoft.com/en-us/microsoft-edge/web-platform/release-notes/143", 163 | "status": "beta" 164 | }, 165 | "144": { 166 | "engine": "Blink", 167 | "engine_version": "144", 168 | "release_date": "2026-01-15", 169 | "status": "nightly" 170 | }, 171 | "145": { 172 | "engine": "Blink", 173 | "engine_version": "145", 174 | "release_date": "2026-02-12", 175 | "status": "planned" 176 | } 177 | }, 178 | "type": "desktop", 179 | "upstream": "chrome" 180 | }, 181 | "firefox": { 182 | "accepts_flags": true, 183 | "accepts_webextensions": true, 184 | "name": "Firefox", 185 | "pref_url": "about:config", 186 | "preview_name": "Nightly", 187 | "releases": { 188 | "140": { 189 | "engine": "Gecko", 190 | "engine_version": "140", 191 | "release_date": "2025-06-24", 192 | "release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/140", 193 | "status": "esr" 194 | }, 195 | "145": { 196 | "engine": "Gecko", 197 | "engine_version": "145", 198 | "release_date": "2025-11-11", 199 | "release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/145", 200 | "status": "current" 201 | }, 202 | "146": { 203 | "engine": "Gecko", 204 | "engine_version": "146", 205 | "release_date": "2025-12-09", 206 | "release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/146", 207 | "status": "beta" 208 | }, 209 | "147": { 210 | "engine": "Gecko", 211 | "engine_version": "147", 212 | "release_date": "2026-01-13", 213 | "release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/147", 214 | "status": "nightly" 215 | }, 216 | "148": { 217 | "engine": "Gecko", 218 | "engine_version": "148", 219 | "release_date": "2026-02-24", 220 | "release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/148", 221 | "status": "planned" 222 | } 223 | }, 224 | "type": "desktop" 225 | }, 226 | "firefox_android": { 227 | "accepts_flags": false, 228 | "accepts_webextensions": true, 229 | "name": "Firefox for Android", 230 | "pref_url": "about:config", 231 | "releases": { 232 | "140": { 233 | "engine": "Gecko", 234 | "engine_version": "140", 235 | "release_date": "2025-06-24", 236 | "release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/140", 237 | "status": "esr" 238 | }, 239 | "145": { 240 | "engine": "Gecko", 241 | "engine_version": "145", 242 | "release_date": "2025-11-11", 243 | "release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/145", 244 | "status": "current" 245 | }, 246 | "146": { 247 | "engine": "Gecko", 248 | "engine_version": "146", 249 | "release_date": "2025-12-09", 250 | "release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/146", 251 | "status": "beta" 252 | }, 253 | "147": { 254 | "engine": "Gecko", 255 | "engine_version": "147", 256 | "release_date": "2026-01-13", 257 | "release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/147", 258 | "status": "nightly" 259 | }, 260 | "148": { 261 | "engine": "Gecko", 262 | "engine_version": "148", 263 | "release_date": "2026-02-24", 264 | "release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/148", 265 | "status": "planned" 266 | } 267 | }, 268 | "type": "mobile", 269 | "upstream": "firefox" 270 | }, 271 | "ie": { 272 | "accepts_flags": false, 273 | "accepts_webextensions": false, 274 | "name": "Internet Explorer", 275 | "releases": {}, 276 | "type": "desktop" 277 | }, 278 | "nodejs": { 279 | "accepts_flags": true, 280 | "accepts_webextensions": false, 281 | "name": "Node.js", 282 | "releases": { 283 | "22.3.0": { 284 | "engine": "V8", 285 | "engine_version": "12.4", 286 | "release_date": "2024-06-11", 287 | "release_notes": "https://nodejs.org/en/blog/release/v22.3.0", 288 | "status": "esr" 289 | }, 290 | "24.7.0": { 291 | "engine": "V8", 292 | "engine_version": "13.6", 293 | "release_date": "2025-08-27", 294 | "release_notes": "https://nodejs.org/en/blog/release/v24.7.0", 295 | "status": "current" 296 | } 297 | }, 298 | "type": "server" 299 | }, 300 | "oculus": { 301 | "accepts_flags": true, 302 | "accepts_webextensions": false, 303 | "name": "Quest Browser", 304 | "pref_url": "chrome://flags", 305 | "releases": { 306 | "23.0": { 307 | "engine": "Blink", 308 | "engine_version": "104", 309 | "release_date": "2022-08-15", 310 | "status": "current" 311 | } 312 | }, 313 | "type": "xr", 314 | "upstream": "chrome_android" 315 | }, 316 | "opera": { 317 | "accepts_flags": true, 318 | "accepts_webextensions": true, 319 | "name": "Opera", 320 | "pref_url": "opera://flags", 321 | "releases": { 322 | "123": { 323 | "engine": "Blink", 324 | "engine_version": "139", 325 | "release_date": "2025-10-28", 326 | "release_notes": "https://blogs.opera.com/desktop/2025/10/opera-123/", 327 | "status": "current" 328 | }, 329 | "124": { "engine": "Blink", "engine_version": "140", "status": "beta" }, 330 | "125": { 331 | "engine": "Blink", 332 | "engine_version": "141", 333 | "status": "nightly" 334 | } 335 | }, 336 | "type": "desktop", 337 | "upstream": "chrome" 338 | }, 339 | "opera_android": { 340 | "accepts_flags": false, 341 | "accepts_webextensions": false, 342 | "name": "Opera Android", 343 | "releases": { 344 | "92": { 345 | "engine": "Blink", 346 | "engine_version": "140", 347 | "release_date": "2025-10-08", 348 | "release_notes": "https://forums.opera.com/topic/86530/opera-for-android-92", 349 | "status": "current" 350 | } 351 | }, 352 | "type": "mobile", 353 | "upstream": "chrome_android" 354 | }, 355 | "safari": { 356 | "accepts_flags": true, 357 | "accepts_webextensions": true, 358 | "name": "Safari", 359 | "preview_name": "TP", 360 | "releases": { 361 | "26.1": { 362 | "engine": "WebKit", 363 | "engine_version": "622.2.11", 364 | "release_date": "2025-11-03", 365 | "release_notes": "https://developer.apple.com/documentation/safari-release-notes/safari-26_1-release-notes", 366 | "status": "current" 367 | }, 368 | "26.2": { 369 | "engine": "WebKit", 370 | "engine_version": "623.1.12", 371 | "release_notes": "https://developer.apple.com/documentation/safari-release-notes/safari-26_2-release-notes", 372 | "status": "beta" 373 | } 374 | }, 375 | "type": "desktop" 376 | }, 377 | "safari_ios": { 378 | "accepts_flags": true, 379 | "accepts_webextensions": true, 380 | "name": "Safari on iOS", 381 | "releases": { 382 | "26.1": { 383 | "engine": "WebKit", 384 | "engine_version": "622.2.11", 385 | "release_date": "2025-11-03", 386 | "release_notes": "https://developer.apple.com/documentation/safari-release-notes/safari-26_1-release-notes", 387 | "status": "current" 388 | }, 389 | "26.2": { 390 | "engine": "WebKit", 391 | "engine_version": "623.1.12", 392 | "release_notes": "https://developer.apple.com/documentation/safari-release-notes/safari-26_2-release-notes", 393 | "status": "beta" 394 | } 395 | }, 396 | "type": "mobile", 397 | "upstream": "safari" 398 | }, 399 | "samsunginternet_android": { 400 | "accepts_flags": false, 401 | "accepts_webextensions": false, 402 | "name": "Samsung Internet", 403 | "releases": { 404 | "28.0": { 405 | "engine": "Blink", 406 | "engine_version": "130", 407 | "release_date": "2025-04-02", 408 | "status": "current" 409 | }, 410 | "29.0": { "engine": "Blink", "engine_version": "136", "status": "beta" } 411 | }, 412 | "type": "mobile", 413 | "upstream": "chrome_android" 414 | }, 415 | "webview_android": { 416 | "accepts_flags": false, 417 | "accepts_webextensions": false, 418 | "name": "WebView Android", 419 | "releases": { 420 | "142": { 421 | "engine": "Blink", 422 | "engine_version": "142", 423 | "release_date": "2025-10-28", 424 | "release_notes": "https://developer.chrome.com/release-notes/142", 425 | "status": "current" 426 | }, 427 | "143": { 428 | "engine": "Blink", 429 | "engine_version": "143", 430 | "release_date": "2025-12-02", 431 | "status": "beta" 432 | }, 433 | "144": { 434 | "engine": "Blink", 435 | "engine_version": "144", 436 | "release_date": "2026-01-13", 437 | "status": "nightly" 438 | }, 439 | "145": { 440 | "engine": "Blink", 441 | "engine_version": "145", 442 | "status": "planned" 443 | } 444 | }, 445 | "type": "mobile", 446 | "upstream": "chrome_android" 447 | }, 448 | "webview_ios": { 449 | "accepts_flags": false, 450 | "accepts_webextensions": false, 451 | "name": "WebView on iOS", 452 | "releases": { 453 | "26.1": { 454 | "engine": "WebKit", 455 | "engine_version": "622.2.11", 456 | "release_date": "2025-11-03", 457 | "release_notes": "https://developer.apple.com/documentation/safari-release-notes/safari-26_1-release-notes", 458 | "status": "current" 459 | }, 460 | "26.2": { 461 | "engine": "WebKit", 462 | "engine_version": "623.1.12", 463 | "release_notes": "https://developer.apple.com/documentation/safari-release-notes/safari-26_2-release-notes", 464 | "status": "beta" 465 | } 466 | }, 467 | "type": "mobile", 468 | "upstream": "safari_ios" 469 | } 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /test/fixtures/headers.json: -------------------------------------------------------------------------------- 1 | { 2 | "doc": { 3 | "body": [ 4 | { 5 | "type": "prose", 6 | "value": { 7 | "id": null, 8 | "title": null, 9 | "isH3": false, 10 | "content": " \n

Note: This feature is available in Web Workers.

\n

The Headers interface of the Fetch API allows you to perform various actions on HTTP request and response headers. These actions include retrieving, setting, adding to, and removing headers from the list of the request's headers.

\n

You can retrieve a Headers object via the Request.headers and Response.headers properties, and create a new Headers object using the Headers() constructor. Compared to using plain objects, using Headers objects to send requests provides some additional input sanitization. For example, it normalizes header names to lowercase, strips leading and trailing whitespace from header values, and prevents certain headers from being set.

\n
\n

Note:\nYou can find out more about the available headers by reading our HTTP headers reference.

\n
" 11 | } 12 | }, 13 | { 14 | "type": "prose", 15 | "value": { 16 | "id": "description", 17 | "title": "Description", 18 | "isH3": false, 19 | "content": "

A Headers object has an associated header list, which is initially empty and consists of zero or more name and value pairs. You can add to this using methods like append() (see Examples.) In all methods of this interface, header names are matched by case-insensitive byte sequence.

\n

An object implementing Headers can directly be used in a for...of structure, instead of entries(): for (const p of myHeaders) is equivalent to for (const p of myHeaders.entries()).

" 20 | } 21 | }, 22 | { 23 | "type": "prose", 24 | "value": { 25 | "id": "modification_restrictions", 26 | "title": "Modification restrictions", 27 | "isH3": true, 28 | "content": "

Some Headers objects have restrictions on whether the set(), delete(), and append() methods can mutate the header. The modification restrictions are set depending on how the Headers object is created.

\n\n

All of the Headers methods will throw a TypeError if you try to pass in a reference to a name that isn't a valid HTTP Header name. The mutation operations will throw a TypeError if the header is immutable. In any other failure case they fail silently.

" 29 | } 30 | }, 31 | { 32 | "type": "prose", 33 | "value": { 34 | "id": "constructor", 35 | "title": "Constructor", 36 | "isH3": false, 37 | "content": "
\n
Headers()
\n
\n

Creates a new Headers object.

\n
\n
" 38 | } 39 | }, 40 | { 41 | "type": "prose", 42 | "value": { 43 | "id": "instance_methods", 44 | "title": "Instance methods", 45 | "isH3": false, 46 | "content": "
\n
Headers.append()
\n
\n

Appends a new value onto an existing header inside a Headers object, or adds the header if it does not already exist.

\n
\n
Headers.delete()
\n
\n

Deletes a header from a Headers object.

\n
\n
Headers.entries()
\n
\n

Returns an iterator allowing to go through all key/value pairs contained in this object.

\n
\n
Headers.forEach()
\n
\n

Executes a provided function once for each key/value pair in this Headers object.

\n
\n
Headers.get()
\n
\n

Returns a String sequence of all the values of a header within a Headers object with a given name.

\n
\n
Headers.getSetCookie()
\n
\n

Returns an array containing the values of all Set-Cookie headers associated with a response.

\n
\n
Headers.has()
\n
\n

Returns a boolean stating whether a Headers object contains a certain header.

\n
\n
Headers.keys()
\n
\n

Returns an iterator allowing you to go through all keys of the key/value pairs contained in this object.

\n
\n
Headers.set()
\n
\n

Sets a new value for an existing header inside a Headers object, or adds the header if it does not already exist.

\n
\n
Headers.values()
\n
\n

Returns an iterator allowing you to go through all values of the key/value pairs contained in this object.

\n
\n
\n
\n

Note:\nTo be clear, the difference between Headers.set() and Headers.append() is that if the specified header does already exist and does accept multiple values, Headers.set() will overwrite the existing value with the new one, whereas Headers.append() will append the new value onto the end of the set of values. See their dedicated pages for example code.

\n
\n
\n

Note:\nWhen Header values are iterated over, they are automatically sorted in lexicographical order, and values from duplicate header names are combined.

\n
" 47 | } 48 | }, 49 | { 50 | "type": "prose", 51 | "value": { 52 | "id": "examples", 53 | "title": "Examples", 54 | "isH3": false, 55 | "content": "

In the following snippet, we create a new header using the Headers() constructor, add a new header to it using append(), then return that header value using get():

\n
js
const myHeaders = new Headers();\n\nmyHeaders.append(\"Content-Type\", \"text/xml\");\nmyHeaders.get(\"Content-Type\"); // should return 'text/xml'\n
\n

The same can be achieved by passing an array of arrays or an object literal to the constructor:

\n
js
let myHeaders = new Headers({\n  \"Content-Type\": \"text/xml\",\n});\n\n// or, using an array of arrays:\nmyHeaders = new Headers([[\"Content-Type\", \"text/xml\"]]);\n\nmyHeaders.get(\"Content-Type\"); // should return 'text/xml'\n
" 56 | } 57 | }, 58 | { 59 | "type": "specifications", 60 | "value": { 61 | "id": "specifications", 62 | "title": "Specifications", 63 | "isH3": false, 64 | "specifications": [ 65 | { 66 | "bcdSpecificationURL": "https://fetch.spec.whatwg.org/#headers-class", 67 | "title": "Fetch" 68 | } 69 | ], 70 | "query": "api.Headers" 71 | } 72 | }, 73 | { 74 | "type": "browser_compatibility", 75 | "value": { 76 | "id": "browser_compatibility", 77 | "title": "Browser compatibility", 78 | "isH3": false, 79 | "query": "api.Headers" 80 | } 81 | }, 82 | { 83 | "type": "prose", 84 | "value": { 85 | "id": "see_also", 86 | "title": "See also", 87 | "isH3": false, 88 | "content": "" 89 | } 90 | } 91 | ], 92 | "isActive": true, 93 | "isMarkdown": true, 94 | "isTranslated": false, 95 | "locale": "en-US", 96 | "mdn_url": "/en-US/docs/Web/API/Headers", 97 | "modified": "2025-03-13T12:48:23.000Z", 98 | "native": "English (US)", 99 | "noIndexing": false, 100 | "other_translations": [ 101 | { "locale": "en-US", "title": "Headers", "native": "English (US)" }, 102 | { "locale": "de", "title": "Headers", "native": "Deutsch" }, 103 | { "locale": "es", "title": "Headers", "native": "Español" }, 104 | { "locale": "fr", "title": "Headers", "native": "Français" }, 105 | { "locale": "ja", "title": "Headers", "native": "日本語" }, 106 | { "locale": "ko", "title": "Headers", "native": "한국어" }, 107 | { "locale": "zh-CN", "title": "Headers", "native": "中文 (简体)" } 108 | ], 109 | "pageTitle": "Headers - Web APIs | MDN", 110 | "parents": [ 111 | { "uri": "/en-US/docs/Web", "title": "Web" }, 112 | { "uri": "/en-US/docs/Web/API", "title": "Web APIs" }, 113 | { "uri": "/en-US/docs/Web/API/Headers", "title": "Headers" } 114 | ], 115 | "popularity": 0.0048434548205698045, 116 | "short_title": "Headers", 117 | "sidebarHTML": "
  1. Fetch API
  2. Headers
  3. Constructor
    1. Headers()
  4. Instance methods
    1. append()
    2. delete()
    3. entries()
    4. forEach()
    5. get()
    6. getSetCookie()
    7. has()
    8. keys()
    9. set()
    10. values()
  5. Related pages for Fetch API
    1. Request
    2. RequestInit
    3. Response
    4. Window.fetch()
    5. WorkerGlobalScope.fetch()
  6. Guides
    1. Using the Fetch API
", 118 | "source": { 119 | "folder": "en-us/web/api/headers", 120 | "github_url": "https://github.com/mdn/content/blob/main/files/en-us/web/api/headers/index.md", 121 | "last_commit_url": "https://github.com/mdn/content/commit/4d929bb0a021c7130d5a71a4bf505bcb8070378d", 122 | "filename": "index.md" 123 | }, 124 | "summary": "The Headers interface of the Fetch API allows you to perform various actions on HTTP request and response headers. These actions include retrieving, setting, adding to, and removing headers from the list of the request's headers.", 125 | "title": "Headers", 126 | "toc": [ 127 | { "text": "Description", "id": "description" }, 128 | { "text": "Constructor", "id": "constructor" }, 129 | { "text": "Instance methods", "id": "instance_methods" }, 130 | { "text": "Examples", "id": "examples" }, 131 | { "text": "Specifications", "id": "specifications" }, 132 | { "text": "Browser compatibility", "id": "browser_compatibility" }, 133 | { "text": "See also", "id": "see_also" } 134 | ], 135 | "baseline": { 136 | "baseline": "high", 137 | "baseline_low_date": "2017-03-27", 138 | "baseline_high_date": "2019-09-27", 139 | "support": { 140 | "chrome": "42", 141 | "chrome_android": "42", 142 | "edge": "14", 143 | "firefox": "39", 144 | "firefox_android": "39", 145 | "safari": "10.1", 146 | "safari_ios": "10.3" 147 | }, 148 | "asterisk": true, 149 | "feature": { 150 | "status": { 151 | "baseline": "high", 152 | "baseline_low_date": "2017-03-27", 153 | "baseline_high_date": "2019-09-27", 154 | "support": { 155 | "chrome": "42", 156 | "chrome_android": "42", 157 | "edge": "14", 158 | "firefox": "39", 159 | "firefox_android": "39", 160 | "safari": "10.1", 161 | "safari_ios": "10.3" 162 | } 163 | }, 164 | "description_html": "The fetch() method makes asynchronous HTTP requests.", 165 | "name": "Fetch" 166 | } 167 | }, 168 | "browserCompat": ["api.Headers"], 169 | "pageType": "web-api-interface" 170 | }, 171 | "url": "/en-US/docs/Web/API/Headers", 172 | "renderer": "Doc" 173 | } 174 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /test/fixtures/kitchensink.json: -------------------------------------------------------------------------------- 1 | { 2 | "doc": { 3 | "body": [ 4 | { 5 | "type": "prose", 6 | "value": { 7 | "id": null, 8 | "title": null, 9 | "isH3": false, 10 | "content": "
\n

Warning:\nDon't delete this page. It's used by mdn/yari for its automation.

\n
" 11 | } 12 | }, 13 | { 14 | "type": "prose", 15 | "value": { 16 | "id": "about_this_page", 17 | "title": "About this page", 18 | "isH3": false, 19 | "content": "

The kitchensink is a page that attempts to incorporate every possible content element and Yari macro.

\n

This page attempts to be the complete intersection of every other page. Not in terms of the text but in terms of the styles and macros.\nLet's start with some notes…

\n

Text that uses the <kbd> tag: Shift

\n
\n

Note:\nHere's a block indicator note.

\n
\n
\n

Warning:\nHere's a block indicator warning.

\n
" 20 | } 21 | }, 22 | { 23 | "type": "prose", 24 | "value": { 25 | "id": "prevnext_buttons", 26 | "title": "Prev/Next buttons", 27 | "isH3": false, 28 | "content": "" 29 | } 30 | }, 31 | { 32 | "type": "prose", 33 | "value": { 34 | "id": "another_one…", 35 | "title": "Another one…", 36 | "isH3": true, 37 | "content": "" 38 | } 39 | }, 40 | { 41 | "type": "prose", 42 | "value": { 43 | "id": "code_snippets", 44 | "title": "Code snippets", 45 | "isH3": false, 46 | "content": "" 47 | } 48 | }, 49 | { 50 | "type": "prose", 51 | "value": { 52 | "id": "plain_text", 53 | "title": "Plain text", 54 | "isH3": true, 55 | "content": "
  ___________________________\n< I'm an expert in my field. >\n  ---------------------------\n         \\   ^__^\n          \\  (oo)\\_______\n             (__)\\       )\\/\\\n                 ||----w |\n                 ||     ||\n
" 56 | } 57 | }, 58 | { 59 | "type": "prose", 60 | "value": { 61 | "id": "html", 62 | "title": "HTML", 63 | "isH3": true, 64 | "content": "
html
<pre></pre>\n
" 65 | } 66 | }, 67 | { 68 | "type": "prose", 69 | "value": { 70 | "id": "javascript", 71 | "title": "JavaScript", 72 | "isH3": true, 73 | "content": "
js
const f = () => {\n  return Math.random();\n};\n
" 74 | } 75 | }, 76 | { 77 | "type": "prose", 78 | "value": { 79 | "id": "css", 80 | "title": "CSS", 81 | "isH3": true, 82 | "content": "
css
:root {\n  --first-color: #488cff;\n  --second-color: #ffff8c;\n}\n\n#firstParagraph {\n  background-color: var(--first-color);\n  color: var(--second-color);\n}\n
" 83 | } 84 | }, 85 | { 86 | "type": "prose", 87 | "value": { 88 | "id": "webassembly", 89 | "title": "WebAssembly", 90 | "isH3": true, 91 | "content": "
wat
(func (param i32) (param f32) (local f64)\n  local.get 0\n  local.get 1\n  local.get 2)\n
" 92 | } 93 | }, 94 | { 95 | "type": "prose", 96 | "value": { 97 | "id": "rust", 98 | "title": "Rust", 99 | "isH3": true, 100 | "content": "
rust
#[cfg(test)]\nmod tests {\n    #[test]\n    fn it_works() {\n        assert_eq!(2 + 2, 4);\n    }\n}\n
" 101 | } 102 | }, 103 | { 104 | "type": "prose", 105 | "value": { 106 | "id": "python", 107 | "title": "Python", 108 | "isH3": true, 109 | "content": "
python
class BookListView(generic.ListView):\n    model = Book\n    # your own name for the list as a template variable\n    context_object_name = 'my_book_list'\n    queryset = Book.objects.filter(title__icontains='war')[:5]\n    template_name = 'books/my_arbitrary_template_name_list.html'\n
" 110 | } 111 | }, 112 | { 113 | "type": "prose", 114 | "value": { 115 | "id": "interactive_examples", 116 | "title": "Interactive examples", 117 | "isH3": false, 118 | "content": "" 119 | } 120 | }, 121 | { 122 | "type": "prose", 123 | "value": { 124 | "id": "try_it", 125 | "title": "Try it", 126 | "isH3": false, 127 | "content": "\n
<p>New Products:</p>\n<ul>\n  <li><data value=\"398\">Mini Ketchup</data></li>\n  <li><data value=\"399\">Jumbo Ketchup</data></li>\n  <li><data value=\"400\">Mega Jumbo Ketchup</data></li>\n</ul>\n
\n
data:hover::after {\n  content: \" (ID \" attr(value) \")\";\n  font-size: 0.7em;\n}\n
" 128 | } 129 | }, 130 | { 131 | "type": "prose", 132 | "value": { 133 | "id": "try_it_2", 134 | "title": "Try it", 135 | "isH3": false, 136 | "content": "\n
const set = new Set();\n\nset.add(42);\nset.add(\"forty two\");\n\nconst iterator = set[Symbol.iterator]();\n\nconsole.log(iterator.next().value);\n// Expected output: 42\n\nconsole.log(iterator.next().value);\n// Expected output: \"forty two\"\n
" 137 | } 138 | }, 139 | { 140 | "type": "prose", 141 | "value": { 142 | "id": "try_it_3", 143 | "title": "Try it", 144 | "isH3": false, 145 | "content": "\n
filter: url(\"/shared-assets/images/examples/shadow.svg#element-id\");\n
\n
filter: blur(5px);\n
\n
filter: contrast(200%);\n
\n
filter: grayscale(80%);\n
\n
filter: hue-rotate(90deg);\n
\n
filter: drop-shadow(16px 16px 20px red) invert(75%);\n
\n
<section id=\"default-example\">\n  <div class=\"example-container\">\n    <img\n      id=\"example-element\"\n      src=\"/shared-assets/images/examples/firefox-logo.svg\"\n      width=\"200\" />\n  </div>\n</section>\n
\n
.example-container {\n  background-color: white;\n  width: 260px;\n  height: 260px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n#example-element {\n  flex: 1;\n  padding: 30px;\n}\n
" 146 | } 147 | }, 148 | { 149 | "type": "prose", 150 | "value": { 151 | "id": "tables", 152 | "title": "Tables", 153 | "isH3": false, 154 | "content": "" 155 | } 156 | }, 157 | { 158 | "type": "prose", 159 | "value": { 160 | "id": "markdown_table", 161 | "title": "Markdown table", 162 | "isH3": true, 163 | "content": "
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Constant nameValueDescription
QUERY_COUNTER_BITS_EXT0x8864The number of bits used to hold the query result for the given target.
CURRENT_QUERY_EXT0x8865The currently active query.
QUERY_RESULT_EXT0x8866The query result.
QUERY_RESULT_AVAILABLE_EXT0x8867A Boolean indicating whether a query result is available.
TIME_ELAPSED_EXT0x88BFElapsed time (in nanoseconds).
TIMESTAMP_EXT0x8E28The current time.
GPU_DISJOINT_EXT0x8FBBA Boolean indicating whether the GPU performed any disjoint operation.
" 164 | } 165 | }, 166 | { 167 | "type": "prose", 168 | "value": { 169 | "id": "html_table", 170 | "title": "HTML table", 171 | "isH3": true, 172 | "content": "
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n Content categories\n \n Flow content,\n phrasing content, palpable content.\n
Permitted content\n Phrasing content.\n
Tag omissionNone, both the starting and ending tag are mandatory.
Permitted parents\n Any element that accepts phrasing content.\n
Implicit ARIA role\n No corresponding role\n
Permitted ARIA rolesAny
DOM interfaceHTMLElement
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n Values for the content of <meta name=\"viewport\">\n
ValuePossible subvaluesDescription
widthA positive integer number, or the text device-width\n Defines the pixel width of the viewport that you want the website to be\n rendered at.\n
user-scalable Read onlyyes or no\n If set to no, the user is not able to zoom in the webpage.\n The default is yes. Browser settings can ignore this rule,\n and iOS10+ ignores it by default.\n
viewport-fitauto, contain or cover\n

\n The auto value doesn't affect the initial layout viewport, and the whole web page is viewable.\n

\n

\n The contain value means that the viewport is scaled to\n fit the largest rectangle inscribed within the display.\n

\n

\n The cover value means that the viewport is scaled to fill the device display.\n It is highly recommended to make use of the safe area inset variables to\n ensure that important content doesn't end up outside the display.\n

\n
" 173 | } 174 | }, 175 | { 176 | "type": "prose", 177 | "value": { 178 | "id": "every_macro_under_the_sun", 179 | "title": "Every macro under the sun", 180 | "isH3": false, 181 | "content": "

Well, almost every macro. Hopefully only the ones that are in active use.

\n

An HTTP error code meaning \"Bad Gateway\".

\n

A server can act as a gateway or proxy (go-between) between a client (like your Web browser) and another, upstream server.\nWhen you request to access a URL, the gateway server can relay your request to the upstream server.\n\"502\" means that the upstream server has returned an invalid response.

\n
    \n
  • JavaScript Array on MDN
  • \n
\n

Listening for mouse movement is even easier than listening for key presses: all we need is the listener for the mousemove event.

" 182 | } 183 | }, 184 | { 185 | "type": "browser_compatibility", 186 | "value": { 187 | "id": "browser_compatibility", 188 | "title": "Browser compatibility", 189 | "isH3": false, 190 | "query": "html.elements.video" 191 | } 192 | }, 193 | { 194 | "type": "prose", 195 | "value": { 196 | "id": "axis-aligned_bounding_box", 197 | "title": "Axis-Aligned Bounding Box", 198 | "isH3": false, 199 | "content": "

One of the simpler forms of collision detection is between two rectangles that are axis aligned — meaning no rotation.\nThe algorithm works by ensuring there is no gap between any of the 4 sides of the rectangles.\nAny gap means a collision does not exist.

\n
js
var rect1 = { x: 5, y: 5, width: 50, height: 50 };\nvar rect2 = { x: 20, y: 10, width: 10, height: 10 };\n\nif (\n  rect1.x < rect2.x + rect2.width &&\n  rect1.x + rect1.width > rect2.x &&\n  rect1.y < rect2.y + rect2.height &&\n  rect1.y + rect1.height > rect2.y\n) {\n  // collision detected!\n}\n\n// filling in the values =>\n\nif (5 < 30 && 55 > 20 && 5 < 20 && 55 > 10) {\n  // collision detected!\n}\n
" 200 | } 201 | }, 202 | { 203 | "type": "prose", 204 | "value": { 205 | "id": "rect_code", 206 | "title": "Rect code", 207 | "isH3": true, 208 | "content": "
html
<div id=\"cr-stage\"></div>\n<p>\n  Move the rectangle with arrow keys. Green means collision, blue means no\n  collision.\n</p>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/crafty/0.5.4/crafty-min.js\"></script>\n
\n
js
Crafty.init(200, 200);\n\nvar dim1 = { x: 5, y: 5, w: 50, h: 50 };\nvar dim2 = { x: 20, y: 10, w: 60, h: 40 };\n\nvar rect1 = Crafty.e(\"2D, Canvas, Color\").attr(dim1).color(\"red\");\n\nvar rect2 = Crafty.e(\"2D, Canvas, Color, Keyboard, Fourway\")\n  .fourway(2)\n  .attr(dim2)\n  .color(\"blue\");\n\nrect2.bind(\"EnterFrame\", function () {\n  if (\n    rect1.x > rect2.x + rect2.w &&\n    rect1.x + rect1.w > rect2.x &&\n    rect1.y > rect2.y + rect2.h &&\n    rect1.h + rect1.y > rect2.y\n  ) {\n    // collision detected!\n    this.color(\"green\");\n  } else {\n    // no collision\n    this.color(\"blue\");\n  }\n});\n
\n
\n

Experimental: This is an experimental technology
Check the Browser compatibility table carefully before using this in production.

\ntabs.mutedInfo" 209 | } 210 | }, 211 | { 212 | "type": "prose", 213 | "value": { 214 | "id": "obsolete_cssom_interfaces_deprecated", 215 | "title": "Obsolete CSSOM interfaces \nDeprecated\n", 216 | "isH3": true, 217 | "content": "\n \n \n Event\n \n \n \n \n UIEvent\n \n \n \n \n MouseEvent\n \n \n \n \n WheelEvent\n \n \n\n\n

The AvailableInWorkers macro inserts a localized note box indicating that a feature is available in a Web worker context.

\n

Note: This feature is available in Web Workers.

\n\n\n
    \n
  • Create a <canvas> element and set its width and height attributes to the original, smaller resolution.
  • \n
  • Set its CSS width and height properties to be 2x or 4x the value of the HTML width and height.\nIf the canvas was created with a 128 pixel width, for example, we would set the CSS width to 512px if we wanted a 4x scale.
  • \n
  • Set the <canvas> element's image-rendering CSS property to some value that does not make the image blurry.\nEither crisp-edges or pixelated will work. Check out the image-rendering article for more information on the differences between these values, and which prefixes to use depending on the browser.
  • \n
\n\n\n\n" 218 | } 219 | }, 220 | { 221 | "type": "prose", 222 | "value": { 223 | "id": "types", 224 | "title": "Types", 225 | "isH3": false, 226 | "content": "
\n
alarms.Alarm
\n
\n

Information about a particular alarm.

\n
\n
\n

Non-standard: This feature is not standardized. We do not recommend using non-standard features in production, as they have limited browser support, and may change or be removed. However, they can be a suitable alternative in specific cases where no standard option exists.

\n

Deprecated: This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes. Avoid using it, and update existing code if possible; see the compatibility table at the bottom of this page to guide your decision. Be aware that this feature may cease to work at any time.

\n\"Iceberg" 227 | } 228 | } 229 | ], 230 | "isActive": true, 231 | "isMarkdown": true, 232 | "isTranslated": false, 233 | "locale": "en-US", 234 | "mdn_url": "/en-US/docs/MDN/Kitchensink", 235 | "modified": "2025-11-06T14:49:20.000Z", 236 | "native": "English (US)", 237 | "noIndexing": true, 238 | "other_translations": [ 239 | { 240 | "locale": "en-US", 241 | "title": "The MDN Content Kitchensink", 242 | "native": "English (US)" 243 | }, 244 | { 245 | "locale": "de", 246 | "title": "Der MDN Content Kitchensink", 247 | "native": "Deutsch" 248 | } 249 | ], 250 | "pageTitle": "The MDN Content Kitchensink - MDN Web Docs | MDN", 251 | "parents": [ 252 | { "uri": "/en-US/docs/MDN", "title": "MDN Web Docs" }, 253 | { 254 | "uri": "/en-US/docs/MDN/Kitchensink", 255 | "title": "The MDN Content Kitchensink" 256 | } 257 | ], 258 | "popularity": 0.00026709990444047106, 259 | "short_title": "The MDN Content Kitchensink", 260 | "source": { 261 | "folder": "en-us/mdn/kitchensink", 262 | "github_url": "https://github.com/mdn/content/blob/main/files/en-us/mdn/kitchensink/index.md", 263 | "last_commit_url": "https://github.com/mdn/content/commit/f69b6693212029ce4b9fa0c753729044577af548", 264 | "filename": "index.md" 265 | }, 266 | "summary": "The kitchensink is a page that attempts to incorporate every possible content element and Yari macro.", 267 | "title": "The MDN Content Kitchensink", 268 | "toc": [ 269 | { "text": "About this page", "id": "about_this_page" }, 270 | { "text": "Prev/Next buttons", "id": "prevnext_buttons" }, 271 | { "text": "Code snippets", "id": "code_snippets" }, 272 | { "text": "Interactive examples", "id": "interactive_examples" }, 273 | { "text": "Try it", "id": "try_it" }, 274 | { "text": "Try it", "id": "try_it_2" }, 275 | { "text": "Try it", "id": "try_it_3" }, 276 | { "text": "Tables", "id": "tables" }, 277 | { 278 | "text": "Every macro under the sun", 279 | "id": "every_macro_under_the_sun" 280 | }, 281 | { "text": "Browser compatibility", "id": "browser_compatibility" }, 282 | { 283 | "text": "Axis-Aligned Bounding Box", 284 | "id": "axis-aligned_bounding_box" 285 | }, 286 | { "text": "Types", "id": "types" } 287 | ], 288 | "baseline": { 289 | "baseline": "high", 290 | "baseline_low_date": "2015-07-29", 291 | "baseline_high_date": "2018-01-29", 292 | "support": { 293 | "chrome": "3", 294 | "chrome_android": "18", 295 | "edge": "12", 296 | "firefox": "3.5", 297 | "firefox_android": "4", 298 | "safari": "3.1", 299 | "safari_ios": "3" 300 | }, 301 | "asterisk": true, 302 | "feature": { 303 | "status": { 304 | "baseline": "high", 305 | "baseline_low_date": "2015-07-29", 306 | "baseline_high_date": "2018-01-29", 307 | "support": { 308 | "chrome": "3", 309 | "chrome_android": "18", 310 | "edge": "12", 311 | "firefox": "3.5", 312 | "firefox_android": "4", 313 | "safari": "3.1", 314 | "safari_ios": "3" 315 | } 316 | }, 317 | "description_html": "The <video> element plays videos or movies, optionally with controls provided by the browser.", 318 | "name": "