diff --git a/.gitignore b/.gitignore index 9ac87def537..f07f5437439 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ hugo node_modules/ resources/ src/.hugo_build.lock +bin/.hugo/ diff --git a/README.md b/README.md index 95dc9f48b46..8b79c4b5804 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,35 @@ This commit triggers a third GitHub workflow that will deploy the latest version _Theme updates do not trigger a deployment, consequently following a theme update, the docs website must be deployed manually._ +## AI assistant access (MCP) + +The documentation is also exposed to AI assistants through a +[Model Context Protocol](https://modelcontextprotocol.io/) server, so that tools like +Claude, Cursor or ChatGPT answer PrestaShop questions from the actual documentation. + +The Hugo build publishes three machine-readable artifacts alongside the site: + +| Artifact | Contents | +| --- | --- | +| `//index.md` | raw markdown source of every page | +| [`/llms.txt`](https://devdocs.prestashop-project.org/llms.txt) | index of the supported documentation version, per the [llms.txt convention](https://llmstxt.org/) | +| [`/mcp-index.json`](https://devdocs.prestashop-project.org/mcp-index.json) | compact page catalogue | + +The MCP server itself is a small stateless HTTP service in [`mcp-server/`](mcp-server/) — +GitHub Pages can only serve static files, and MCP requires an endpoint that answers `POST` +requests. It holds no data of its own: it reads the artifacts above and delegates search to +the site's existing Algolia DocSearch index, so **documentation changes go live without +redeploying it**. + +To run and test the whole thing locally: + +```bash +npm run mcp:local # builds the site, serves it, starts the Worker on :8787 +npm run mcp:test # in another terminal +``` + +See [`mcp-server/README.md`](mcp-server/README.md) for setup, deployment and usage. + ## License Content from this documentation is licensed under the [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/bin/mcp-local.sh b/bin/mcp-local.sh new file mode 100755 index 00000000000..44f77f82573 --- /dev/null +++ b/bin/mcp-local.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# +# Runs the whole MCP stack locally: +# +# 1. builds the site with the same Hugo version CI uses (downloaded on first run) +# 2. serves src/public over HTTP, standing in for GitHub Pages +# 3. runs the Cloudflare Worker against it +# +# The MCP endpoint ends up at http://127.0.0.1:8787/mcp +# +# Pass --no-build to skip the Hugo build and reuse the existing src/public. +# +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# Must match the version pinned in .github/workflows/build.yml. A newer Hugo will not +# build this site: the theme still uses getJSON, removed in Hugo 0.158. +HUGO_VERSION="0.121.1" +HUGO_DIR="${ROOT}/bin/.hugo/${HUGO_VERSION}" +HUGO_BIN="${HUGO_DIR}/hugo" +SITE_PORT="${SITE_PORT:-8788}" +MCP_PORT="${MCP_PORT:-8787}" + +log() { printf '\033[36m▸\033[0m %s\n' "$*"; } +die() { printf '\033[31m✗ %s\033[0m\n' "$*" >&2; exit 1; } + +install_hugo() { + [ -x "$HUGO_BIN" ] && return + + local os arch asset + os="$(uname -s)" + arch="$(uname -m)" + case "$os" in + Darwin) asset="darwin-universal" ;; + Linux) + case "$arch" in + x86_64) asset="linux-amd64" ;; + aarch64|arm64) asset="linux-arm64" ;; + *) die "Unsupported Linux architecture: $arch" ;; + esac + ;; + *) die "Unsupported OS: $os. Build the site yourself and rerun with --no-build." ;; + esac + + log "Downloading Hugo ${HUGO_VERSION} (${asset})…" + mkdir -p "$HUGO_DIR" + curl -fsSL -o "${HUGO_DIR}/hugo.tar.gz" \ + "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_${asset}.tar.gz" + tar -xzf "${HUGO_DIR}/hugo.tar.gz" -C "$HUGO_DIR" hugo + rm -f "${HUGO_DIR}/hugo.tar.gz" +} + +build_site() { + install_hugo + local configs="config.yml" + # config-local.yml is gitignored; it disables the git-info lookup that would + # otherwise need DEVDOCS_GITHUB_READ_TOKEN. + [ -f "${ROOT}/src/config-local.yml" ] && configs="config.yml,config-local.yml" + + log "Building the site with Hugo ${HUGO_VERSION}…" + (cd "${ROOT}/src" && "$HUGO_BIN" --config "$configs" --quiet) + + [ -f "${ROOT}/src/public/mcp-index.json" ] || die "Build produced no mcp-index.json." + log "Built $(find "${ROOT}/src/public" -name index.md | wc -l | tr -d ' ') markdown artifacts." +} + +serve_site() { + log "Serving src/public on http://127.0.0.1:${SITE_PORT} (standing in for GitHub Pages)…" + (cd "${ROOT}/src/public" && python3 -m http.server "$SITE_PORT" --bind 127.0.0.1 >/dev/null 2>&1) & + SITE_PID=$! + + for _ in $(seq 1 40); do + curl -sf -o /dev/null "http://127.0.0.1:${SITE_PORT}/mcp-index.json" && return + sleep 0.25 + done + die "Static server did not come up on port ${SITE_PORT}." +} + +port_pids() { lsof -nP -tiTCP:"$1" -sTCP:LISTEN 2>/dev/null || true; } + +# Signalling the PIDs we launched is not enough on its own: `npx` sits in front of a +# chain of node processes, so terminating it can leave the real wrangler process alive +# and still holding the port. The contract we actually care about is "both ports are +# free afterwards", so verify that and force it if the polite shutdown stalls. +cleanup() { + trap - EXIT INT TERM + log "Shutting down…" + + for pid in "${WRANGLER_PID:-}" "${SITE_PID:-}"; do + [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true + done + + for _ in $(seq 1 20); do + [ -z "$(port_pids "$MCP_PORT")$(port_pids "$SITE_PORT")" ] && return + sleep 0.25 + done + + for port in "$MCP_PORT" "$SITE_PORT"; do + pids="$(port_pids "$port")" + [ -n "$pids" ] && kill -9 $pids 2>/dev/null || true + done +} +trap cleanup EXIT INT TERM + +# Fail early and clearly rather than half-starting on top of a previous run. +for port_pair in "$SITE_PORT:static server" "$MCP_PORT:MCP endpoint"; do + port="${port_pair%%:*}" + if lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then + die "Port ${port} (${port_pair#*:}) is already in use — a previous run may still be alive. + Inspect it with: lsof -nP -iTCP:${port} -sTCP:LISTEN + Then stop it, or rerun with SITE_PORT/MCP_PORT set to free ports." + fi +done + +[ "${1:-}" = "--no-build" ] || build_site +[ -d "${ROOT}/src/public" ] || die "No src/public. Run without --no-build first." + +serve_site + +[ -d "${ROOT}/mcp-server/node_modules" ] || (log "Installing Worker dependencies…" && cd "${ROOT}/mcp-server" && npm install --silent) + +cat </index.md on GitHub Pages │ +│ list_sections ► /mcp-index.json │ +└─────────────────────────────────────────────────────────────────┘ + ▲ + │ https + Claude / Cursor / ChatGPT / … +``` + +The Worker stores nothing. Search relevance comes from the same Algolia DocSearch index +that powers the search box on the website, and page content is read from the markdown +files the Hugo build already publishes. **New documentation is live as soon as GitHub +Pages redeploys — the Worker never needs redeploying for content changes.** + +## Tools + +| Tool | Purpose | +| --- | --- | +| `search_docs(query, limit?, section?)` | Full-text search, returns pages with the heading path of each match | +| `get_doc(path, offset?, max_chars?)` | Full markdown source of one page, with paging for long pages | +| `list_sections(section?)` | Browse the documentation tree | + +Only the currently supported documentation version (PrestaShop 9, per +`params.versions.current` in `src/config.yml`) is served. `get_doc` rejects paths outside +that version. + +## Using it + +Once deployed, point any MCP client at the `/mcp` endpoint. + +```bash +claude mcp add --transport http prestashop-devdocs https:///mcp +``` + +Or, in a `mcp.json` / `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "prestashop-devdocs": { + "type": "http", + "url": "https:///mcp" + } + } +} +``` + +No authentication — the server is public and read-only, exactly like the website it reads. + +## Running it locally + +One command brings up the whole stack — it builds the site with the Hugo version CI uses +(downloaded to `bin/.hugo/` on first run), serves `src/public` in place of GitHub Pages, +and starts the Worker against it: + +```bash +npm run mcp:local # from the repository root +``` + +The MCP endpoint lands on `http://127.0.0.1:8787/mcp`. Add `-- --no-build` to skip the +Hugo build and reuse the existing `src/public`. `SITE_PORT` and `MCP_PORT` override the +ports. + +> Do not use a system-wide `hugo` for this. The theme still calls `getJSON`, removed in +> Hugo 0.158, so anything newer fails to build the site at all. The script pins 0.121.1 to +> match `.github/workflows/build.yml`. + +### Testing it + +```bash +npm run mcp:test # 38 checks: transport rules, lifecycle, every tool +``` + +With the official [MCP Inspector](https://github.com/modelcontextprotocol/inspector) — +`--cli` for one-shot calls, or drop it for the browser UI: + +```bash +npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8787/mcp \ + --transport http --method tools/list + +npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8787/mcp \ + --transport http --method tools/call \ + --tool-name search_docs --tool-arg 'query=actionValidateOrder' +``` + +Or wire the local server into Claude Code and use it for real: + +```bash +claude mcp add --transport http devdocs-local http://127.0.0.1:8787/mcp +claude mcp remove devdocs-local # when you are done +``` + +Plain `curl` works too — it is only JSON-RPC over `POST`: + +```bash +curl -s -X POST http://127.0.0.1:8787/mcp \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", + "params":{"name":"search_docs","arguments":{"query":"actionValidateOrder"}}}' +``` + +### Working on the Worker alone + +`npm run dev` in this directory starts just the Worker, reading the **live** +`https://devdocs.prestashop-project.org/`. Useful for iterating on search, but `get_doc` +and `list_sections` will 404 until the artifacts from this branch are actually deployed. + +## Search behaviour + +`search_docs` queries Algolia twice. The first pass requires every word of the query to +match; only if that finds nothing does it retry with Algolia's default behaviour of +dropping words until something matches, and then labels the results as approximate. + +This matters: on the lenient default, a question the documentation does not cover comes +back with a page of confident-looking irrelevant hits — `"zzzqqqxyzzy no such thing"` +returns 358 of them — which is precisely the material a model will hallucinate from. The +strict-first pass returns zero for that, and long natural-language questions still get +answers through the labelled fallback. + +## Deployment + +**Deployment is owned by the platform/devops team — this repository does not deploy the +server.** There is deliberately no CI workflow and there are no deployment secrets here. + +Whichever host: Algolia DocSearch is free for open source and already in use by the +website, and the compute involved is negligible on any platform's free tier. + +## Protocol notes + +- Transport: Streamable HTTP, stateless (no `Mcp-Session-Id`, no SSE stream). +- `POST /mcp` returns `application/json`; `GET`/`DELETE /mcp` return `405`, which the spec + permits for servers that do not offer server-initiated streams. +- Negotiates protocol versions `2025-11-25` down to `2024-11-05`. +- CORS is fully open. The server is unauthenticated, read-only and holds no user data, so + the DNS-rebinding threat the `Origin` check defends against does not apply; allowing all + origins is what makes it usable from browser-based clients. diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json new file mode 100644 index 00000000000..092bfe81734 --- /dev/null +++ b/mcp-server/package-lock.json @@ -0,0 +1,2073 @@ +{ + "name": "prestashop-devdocs-mcp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "prestashop-devdocs-mcp", + "version": "1.0.0", + "devDependencies": { + "@cloudflare/workers-types": "^4.20250109.0", + "esbuild": "^0.24.2", + "typescript": "^5.7.3", + "wrangler": "^3.101.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", + "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", + "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.14", + "workerd": "^1.20250124.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz", + "integrity": "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz", + "integrity": "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz", + "integrity": "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz", + "integrity": "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz", + "integrity": "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260702.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260702.1.tgz", + "integrity": "sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild-plugins/node-globals-polyfill": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", + "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild-plugins/node-modules-polyfill": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", + "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "rollup-plugin-node-polyfills": "^0.2.1" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "printable-characters": "^1.0.42" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true, + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exsolve": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/miniflare": { + "version": "3.20250718.3", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250718.3.tgz", + "integrity": "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "stoppable": "1.1.0", + "undici": "^5.28.5", + "workerd": "1.20250718.0", + "ws": "8.18.0", + "youch": "3.3.4", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/rollup-plugin-inject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", + "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1", + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "node_modules/rollup-plugin-node-polyfills": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", + "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rollup-plugin-inject": "^3.0.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/stacktracey": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.2.0.tgz", + "integrity": "sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.14", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz", + "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "exsolve": "^1.0.1", + "ohash": "^2.0.10", + "pathe": "^2.0.3", + "ufo": "^1.5.4" + } + }, + "node_modules/workerd": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250718.0.tgz", + "integrity": "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250718.0", + "@cloudflare/workerd-darwin-arm64": "1.20250718.0", + "@cloudflare/workerd-linux-64": "1.20250718.0", + "@cloudflare/workerd-linux-arm64": "1.20250718.0", + "@cloudflare/workerd-windows-64": "1.20250718.0" + } + }, + "node_modules/wrangler": { + "version": "3.114.17", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.114.17.tgz", + "integrity": "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.3.4", + "@cloudflare/unenv-preset": "2.0.2", + "@esbuild-plugins/node-globals-polyfill": "0.2.3", + "@esbuild-plugins/node-modules-polyfill": "0.2.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.17.19", + "miniflare": "3.20250718.3", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.14", + "workerd": "1.20250718.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=16.17.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20250408.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", + "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^0.7.1", + "mustache": "^4.2.0", + "stacktracey": "^2.1.8" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/mcp-server/package.json b/mcp-server/package.json new file mode 100644 index 00000000000..1f32fa6ccc2 --- /dev/null +++ b/mcp-server/package.json @@ -0,0 +1,17 @@ +{ + "name": "prestashop-devdocs-mcp", + "version": "1.0.0", + "private": true, + "description": "MCP server exposing the PrestaShop developer documentation to AI assistants", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250109.0", + "esbuild": "^0.24.2", + "typescript": "^5.7.3", + "wrangler": "^3.101.0" + } +} diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts new file mode 100644 index 00000000000..693ecb46a6d --- /dev/null +++ b/mcp-server/src/index.ts @@ -0,0 +1,635 @@ +/** + * PrestaShop Developer Documentation — MCP server. + * + * A stateless Streamable HTTP MCP server running on Cloudflare Workers. It owns no + * data of its own: search is delegated to the Algolia DocSearch index that already + * powers the search box on devdocs.prestashop-project.org, and page content is read + * from the static artifacts published to GitHub Pages by the Hugo build + * (`//index.md` and `/mcp-index.json`). + * + * Transport: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports + */ + +export interface Env { + DOCS_BASE_URL: string; + DOCS_VERSION: string; + ALGOLIA_APP_ID: string; + ALGOLIA_API_KEY: string; + ALGOLIA_INDEX: string; +} + +const SERVER_NAME = "prestashop-devdocs"; +const SERVER_VERSION = "1.0.0"; + +/** Newest first. The first entry is what we advertise when the client asks for something we don't know. */ +const SUPPORTED_PROTOCOL_VERSIONS = [ + "2025-11-25", + "2025-06-18", + "2025-03-26", + "2024-11-05", +]; + +/** How long a fetched artifact stays cached inside a single Worker isolate. */ +const INDEX_TTL_MS = 15 * 60 * 1000; + +// --------------------------------------------------------------------------- +// JSON-RPC plumbing +// --------------------------------------------------------------------------- + +type JsonRpcId = string | number | null; + +interface JsonRpcRequest { + jsonrpc: "2.0"; + id?: JsonRpcId; + method: string; + params?: Record; +} + +const ERR_PARSE = -32700; +const ERR_INVALID_REQUEST = -32600; +const ERR_METHOD_NOT_FOUND = -32601; +const ERR_INVALID_PARAMS = -32602; +const ERR_INTERNAL = -32603; + +function rpcResult(id: JsonRpcId, result: unknown) { + return { jsonrpc: "2.0" as const, id, result }; +} + +function rpcError(id: JsonRpcId, code: number, message: string) { + return { jsonrpc: "2.0" as const, id, error: { code, message } }; +} + +const CORS_HEADERS: Record = { + // This server is public, read-only and holds no credentials or user data, so any + // origin may talk to it. That is what makes it usable from browser-based clients. + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Accept, MCP-Protocol-Version, Mcp-Session-Id, Authorization", + "Access-Control-Expose-Headers": "MCP-Protocol-Version", + "Access-Control-Max-Age": "86400", +}; + +function jsonResponse(body: unknown, status = 200, extra: Record = {}) { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json", ...CORS_HEADERS, ...extra }, + }); +} + +// --------------------------------------------------------------------------- +// Static artifacts published by the Hugo build +// --------------------------------------------------------------------------- + +interface IndexPage { + path: string; + markdown: string; + title: string; + description: string; + section: string; +} + +interface IndexSection { + slug: string; + title: string; + path: string; + description: string; + pageCount: number; +} + +interface DocsIndex { + site: string; + docsVersion: string; + generated: string; + sections: IndexSection[]; + pages: IndexPage[]; +} + +let cachedIndex: { at: number; value: DocsIndex } | null = null; + +/** + * Note: the Cache API and `cf.cacheTtl` are bypassed on *.workers.dev, so this + * in-isolate cache is the one that actually does the work there. On a custom + * domain the edge cache kicks in on top of it. + */ +async function loadIndex(env: Env): Promise { + const now = Date.now(); + if (cachedIndex && now - cachedIndex.at < INDEX_TTL_MS) return cachedIndex.value; + + const url = new URL("mcp-index.json", env.DOCS_BASE_URL).toString(); + const res = await fetch(url, { + cf: { cacheTtl: 900, cacheEverything: true }, + } as RequestInit); + if (!res.ok) throw new Error(`Could not load ${url} (HTTP ${res.status})`); + + const value = (await res.json()) as DocsIndex; + cachedIndex = { at: now, value }; + return value; +} + +/** + * Turns anything a model might plausibly pass — a full URL, a site path, a path + * ending in .md — into the canonical `/9/section/page/` form, or null if it points + * outside the supported documentation version. + */ +function normalizeDocPath(input: string, version: string): string | null { + let p = String(input || "").trim(); + if (!p) return null; + + if (/^https?:\/\//i.test(p)) { + try { + p = new URL(p).pathname; + } catch { + return null; + } + } + + p = p.split("#")[0].split("?")[0]; + if (p.includes("..")) return null; + if (!p.startsWith("/")) p = `/${p}`; + + if (p.endsWith("/index.md")) p = p.slice(0, -"index.md".length); + else if (p.endsWith(".md")) p = `${p.slice(0, -".md".length)}/`; + if (!p.endsWith("/")) p = `${p}/`; + + if (!p.startsWith(`/${version}/`)) return null; + return p; +} + +// --------------------------------------------------------------------------- +// Algolia DocSearch +// --------------------------------------------------------------------------- + +interface AlgoliaHit { + url: string; + content: string | null; + hierarchy: Record; + _snippetResult?: { content?: { value: string } }; +} + +function stripHighlight(s: string): string { + return s.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim(); +} + +function hitBreadcrumb(hit: AlgoliaHit): string { + return ["lvl0", "lvl1", "lvl2", "lvl3", "lvl4"] + .map((k) => hit.hierarchy?.[k]) + .filter((v): v is string => Boolean(v)) + .map(stripHighlight) + .join(" › "); +} + +/** + * `strict` requires every word of the query to match. Algolia's default is to drop + * words until something matches, which means a query about something the docs simply + * do not cover still comes back with a page of confident-looking irrelevant hits — + * exactly the material a model will happily hallucinate from. We search strictly + * first and only fall back to the lenient behaviour when that finds nothing, so loose + * results can be labelled as such. + */ +async function algoliaSearch( + env: Env, + query: string, + hitsPerPage: number, + strict: boolean, +): Promise { + const url = `https://${env.ALGOLIA_APP_ID}-dsn.algolia.net/1/indexes/${encodeURIComponent(env.ALGOLIA_INDEX)}/query`; + const res = await fetch(url, { + method: "POST", + headers: { + "X-Algolia-API-Key": env.ALGOLIA_API_KEY, + "X-Algolia-Application-Id": env.ALGOLIA_APP_ID, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query, + hitsPerPage, + facetFilters: [[`version:${env.DOCS_VERSION}`]], + attributesToRetrieve: ["url", "content", "hierarchy"], + attributesToSnippet: ["content:40"], + snippetEllipsisText: "…", + ...(strict ? { removeWordsIfNoResults: "none" } : {}), + }), + }); + + if (!res.ok) { + throw new Error(`Algolia search failed (HTTP ${res.status}): ${stripHighlight(await res.text()).slice(0, 200)}`); + } + const body = (await res.json()) as { hits?: AlgoliaHit[] }; + return body.hits ?? []; +} + +// --------------------------------------------------------------------------- +// Tools +// --------------------------------------------------------------------------- + +const TOOLS = [ + { + name: "search_docs", + title: "Search PrestaShop developer documentation", + description: + "Full-text search across the official PrestaShop 9 developer documentation " + + "(devdocs.prestashop-project.org). Returns matching pages with the heading path of " + + "each match and the doc path to pass to `get_doc`. Use this first for any question " + + "about hooks, modules, themes, the Admin API, the webservice, the console, CQRS, " + + "grids, forms, upgrading, or contributing to PrestaShop.", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "Search terms, e.g. 'register a hook from a module' or 'actionValidateOrder'.", + }, + limit: { + type: "integer", + description: "Maximum number of pages to return (default 8, max 25).", + minimum: 1, + maximum: 25, + }, + section: { + type: "string", + description: + "Optional top-level section to restrict results to, e.g. 'modules', 'development', " + + "'themes', 'webservice', 'admin-api', 'basics', 'testing', 'contribute'. " + + "Call `list_sections` to see all of them.", + }, + }, + required: ["query"], + }, + annotations: { readOnlyHint: true, openWorldHint: true }, + }, + { + name: "get_doc", + title: "Read a documentation page", + description: + "Fetch the full markdown source of one documentation page. Accepts a doc path " + + "('/9/modules/concepts/hooks/'), a full devdocs URL, or a path to the .md file. " + + "Use the paths returned by `search_docs` or `list_sections`.", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Doc path or full devdocs.prestashop-project.org URL of the page to read.", + }, + offset: { + type: "integer", + description: "Character offset to start reading from, for paging through long pages (default 0).", + minimum: 0, + }, + max_chars: { + type: "integer", + description: "Maximum number of characters to return (default 40000, max 120000).", + minimum: 1000, + maximum: 120000, + }, + }, + required: ["path"], + }, + annotations: { readOnlyHint: true, openWorldHint: true }, + }, + { + name: "list_sections", + title: "Browse the documentation tree", + description: + "List the top-level sections of the PrestaShop 9 developer documentation, or, when " + + "`section` is given, every page inside that section. Use this to get an overview " + + "before searching, or to enumerate a topic exhaustively.", + inputSchema: { + type: "object", + properties: { + section: { + type: "string", + description: "Section slug to expand, e.g. 'modules'. Omit to list all sections.", + }, + }, + }, + annotations: { readOnlyHint: true, openWorldHint: true }, + }, +]; + +function toolText(text: string, isError = false) { + return { content: [{ type: "text", text }], isError }; +} + +async function runSearchDocs(env: Env, args: Record) { + const query = typeof args.query === "string" ? args.query.trim() : ""; + if (!query) return toolText("`query` is required and must be a non-empty string.", true); + + const limit = Math.min(Math.max(Number(args.limit) || 8, 1), 25); + const section = typeof args.section === "string" ? args.section.trim().replace(/^\/|\/$/g, "") : ""; + + const base = new URL(env.DOCS_BASE_URL); + // Over-fetch: DocSearch indexes one record per heading, so several hits routinely + // collapse into the same page once we group them. + const overFetch = Math.min(limit * 5, 100); + + const group = (hits: AlgoliaHit[]) => { + const pages = new Map(); + for (const hit of hits) { + if (!hit.url) continue; + let pathname: string; + try { + pathname = new URL(hit.url).pathname; + } catch { + continue; + } + const docPath = pathname.split("#")[0]; + if (!docPath.startsWith(`/${env.DOCS_VERSION}/`)) continue; + if (section && !docPath.startsWith(`/${env.DOCS_VERSION}/${section}/`)) continue; + + let entry = pages.get(docPath); + if (!entry) { + if (pages.size >= limit) continue; + entry = { url: new URL(docPath, base).toString(), matches: [] }; + pages.set(docPath, entry); + } + + const crumb = hitBreadcrumb(hit); + const snippet = stripHighlight(hit._snippetResult?.content?.value ?? hit.content ?? ""); + const line = [crumb, snippet].filter(Boolean).join(" — "); + if (line && entry.matches.length < 3 && !entry.matches.includes(line)) entry.matches.push(line); + } + return pages; + }; + + let pages = group(await algoliaSearch(env, query, overFetch, true)); + let approximate = false; + if (pages.size === 0) { + pages = group(await algoliaSearch(env, query, overFetch, false)); + approximate = true; + } + + if (pages.size === 0) { + const scope = section ? ` in section '${section}'` : ""; + return toolText( + `No results for "${query}"${scope} in the PrestaShop ${env.DOCS_VERSION} documentation.\n\n` + + "Try broader or different terms, drop the `section` filter, or call `list_sections` to browse the tree.", + ); + } + + const lines: string[] = approximate + ? [ + `No page matches all the terms in "${query}". These ${pages.size} page(s) are ` + + "APPROXIMATE matches on some of the terms only — treat them as leads, verify with " + + "`get_doc` before relying on them, and say so if the documentation does not actually " + + "cover the question.", + "", + ] + : [ + `${pages.size} page(s) matching "${query}" in the PrestaShop ${env.DOCS_VERSION} developer documentation:`, + "", + ]; + let i = 1; + for (const [docPath, entry] of pages) { + lines.push(`${i}. ${docPath}`); + lines.push(` url: ${entry.url}`); + for (const m of entry.matches) lines.push(` • ${m}`); + lines.push(""); + i++; + } + lines.push("Call `get_doc` with one of the paths above to read the full page."); + return toolText(lines.join("\n")); +} + +async function runGetDoc(env: Env, args: Record) { + const raw = typeof args.path === "string" ? args.path : ""; + const docPath = normalizeDocPath(raw, env.DOCS_VERSION); + if (!docPath) { + return toolText( + `Invalid path: ${JSON.stringify(raw)}. Expected a path inside the supported documentation ` + + `version, e.g. "/${env.DOCS_VERSION}/modules/concepts/hooks/" or a full ` + + `${env.DOCS_BASE_URL} URL. Only PrestaShop ${env.DOCS_VERSION} is served by this server.`, + true, + ); + } + + const offset = Math.max(Number(args.offset) || 0, 0); + const maxChars = Math.min(Math.max(Number(args.max_chars) || 40000, 1000), 120000); + + const mdUrl = new URL(`${docPath.slice(1)}index.md`, env.DOCS_BASE_URL).toString(); + const res = await fetch(mdUrl, { cf: { cacheTtl: 900, cacheEverything: true } } as RequestInit); + + if (res.status === 404) { + return toolText( + `No page at ${docPath} (fetched ${mdUrl}, HTTP 404).\n\n` + + "Use `search_docs` or `list_sections` to find the correct path.", + true, + ); + } + if (!res.ok) return toolText(`Failed to fetch ${mdUrl} (HTTP ${res.status}).`, true); + + const body = await res.text(); + const slice = body.slice(offset, offset + maxChars); + const end = offset + slice.length; + + let out = slice; + if (offset > 0) out = `[…truncated, resuming at character ${offset} of ${body.length}]\n\n${out}`; + if (end < body.length) { + out += `\n\n[…truncated at character ${end} of ${body.length}. Call get_doc again with offset=${end} to continue.]`; + } + return toolText(out); +} + +async function runListSections(env: Env, args: Record) { + const index = await loadIndex(env); + const section = typeof args.section === "string" ? args.section.trim().replace(/^\/|\/$/g, "") : ""; + + if (!section) { + const lines = [ + `Top-level sections of the PrestaShop ${index.docsVersion} developer documentation ` + + `(${index.pages.length} pages, index generated ${index.generated}):`, + "", + ]; + for (const s of index.sections) { + lines.push(`- ${s.slug} — ${s.title} (${s.pageCount} pages) → ${s.path}`); + if (s.description) lines.push(` ${s.description}`); + } + lines.push("", "Call `list_sections` with a `section` slug to list its pages, or `search_docs` to search."); + return toolText(lines.join("\n")); + } + + const known = index.sections.find((s) => s.slug === section); + const pages = index.pages.filter((p) => p.section === section); + if (!known && pages.length === 0) { + return toolText( + `Unknown section '${section}'. Available: ${index.sections.map((s) => s.slug).join(", ")}.`, + true, + ); + } + + const lines = [`${pages.length} page(s) in section '${section}'${known ? ` (${known.title})` : ""}:`, ""]; + for (const p of pages) { + lines.push(`- ${p.path} — ${p.title}${p.description ? `: ${p.description}` : ""}`); + } + lines.push("", "Call `get_doc` with one of the paths above to read a page."); + return toolText(lines.join("\n")); +} + +async function callTool(env: Env, name: string, args: Record) { + switch (name) { + case "search_docs": + return runSearchDocs(env, args); + case "get_doc": + return runGetDoc(env, args); + case "list_sections": + return runListSections(env, args); + default: + return null; + } +} + +// --------------------------------------------------------------------------- +// MCP method dispatch +// --------------------------------------------------------------------------- + +async function handleRpc(msg: JsonRpcRequest, env: Env): Promise { + const id = msg.id ?? null; + const params = (msg.params ?? {}) as Record; + + switch (msg.method) { + case "initialize": { + const requested = typeof params.protocolVersion === "string" ? params.protocolVersion : ""; + const negotiated = SUPPORTED_PROTOCOL_VERSIONS.includes(requested) + ? requested + : SUPPORTED_PROTOCOL_VERSIONS[0]; + return rpcResult(id, { + protocolVersion: negotiated, + capabilities: { tools: { listChanged: false } }, + serverInfo: { + name: SERVER_NAME, + title: "PrestaShop Developer Documentation", + version: SERVER_VERSION, + }, + instructions: + `Official developer documentation for PrestaShop ${env.DOCS_VERSION} ` + + `(${env.DOCS_BASE_URL}). Start with \`search_docs\`; read whole pages with \`get_doc\`; ` + + "use `list_sections` to browse. Prefer these tools over recalling PrestaShop APIs from " + + "memory — PrestaShop 9 runs on Symfony 6.4 and PHP 8.1+, and differs substantially " + + "from 1.7 and 8.x.", + }); + } + + case "ping": + return rpcResult(id, {}); + + case "tools/list": + return rpcResult(id, { tools: TOOLS }); + + case "tools/call": { + const name = typeof params.name === "string" ? params.name : ""; + const args = (params.arguments ?? {}) as Record; + try { + const result = await callTool(env, name, args); + if (result === null) return rpcError(id, ERR_INVALID_PARAMS, `Unknown tool: ${name}`); + return rpcResult(id, result); + } catch (err) { + // Tool-level failures are reported in-band so the model can react to them. + return rpcResult(id, toolText(`Tool '${name}' failed: ${(err as Error).message}`, true)); + } + } + + // Advertised as unsupported in `capabilities`, but some clients probe anyway. + case "resources/list": + return rpcResult(id, { resources: [] }); + case "resources/templates/list": + return rpcResult(id, { resourceTemplates: [] }); + case "prompts/list": + return rpcResult(id, { prompts: [] }); + + default: + // Notifications (no id) that we don't care about are simply acknowledged. + if (msg.id === undefined) return null; + return rpcError(id, ERR_METHOD_NOT_FOUND, `Method not found: ${msg.method}`); + } +} + +// --------------------------------------------------------------------------- +// HTTP entry point +// --------------------------------------------------------------------------- + +const LANDING_PAGE = (env: Env) => ` + +PrestaShop DevDocs MCP server + +

PrestaShop DevDocs MCP server

+

Model Context Protocol server for the +PrestaShop ${env.DOCS_VERSION} developer documentation.

+

Endpoint

+
POST /mcp   (Streamable HTTP)
+

Add it to Claude Code

+
claude mcp add --transport http prestashop-devdocs https://YOUR-WORKER-HOST/mcp
+

Tools

+
    +${TOOLS.map((t) => `
  • ${t.name} — ${t.title}
  • `).join("\n")} +
+

Read-only and unauthenticated. Source: +PrestaShop/devdocs-site.

+`; + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (request.method === "OPTIONS") { + return new Response(null, { status: 204, headers: CORS_HEADERS }); + } + + if (url.pathname !== "/mcp") { + if (url.pathname === "/" && request.method === "GET") { + return new Response(LANDING_PAGE(env), { + headers: { "Content-Type": "text/html; charset=utf-8", ...CORS_HEADERS }, + }); + } + return jsonResponse({ error: "Not found. The MCP endpoint is at /mcp." }, 404); + } + + // The spec allows a stateless server to decline server-initiated streams. + if (request.method === "GET" || request.method === "DELETE") { + return jsonResponse(rpcError(null, ERR_INVALID_REQUEST, "This server does not offer an SSE stream."), 405); + } + + if (request.method !== "POST") { + return jsonResponse(rpcError(null, ERR_INVALID_REQUEST, "Method not allowed."), 405); + } + + const declared = request.headers.get("MCP-Protocol-Version"); + if (declared && !SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) { + return jsonResponse( + rpcError(null, ERR_INVALID_REQUEST, `Unsupported MCP-Protocol-Version: ${declared}`), + 400, + ); + } + + let payload: unknown; + try { + payload = await request.json(); + } catch { + return jsonResponse(rpcError(null, ERR_PARSE, "Invalid JSON."), 400); + } + + if (Array.isArray(payload)) { + // JSON-RPC batching was removed from MCP in the 2025-06-18 revision. + return jsonResponse(rpcError(null, ERR_INVALID_REQUEST, "Batched requests are not supported."), 400); + } + + const msg = payload as JsonRpcRequest; + if (!msg || typeof msg.method !== "string") { + return jsonResponse(rpcError(null, ERR_INVALID_REQUEST, "Not a JSON-RPC request."), 400); + } + + try { + const response = await handleRpc(msg, env); + // Notifications and responses get 202 Accepted with an empty body. + if (response === null) return new Response(null, { status: 202, headers: CORS_HEADERS }); + return jsonResponse(response); + } catch (err) { + return jsonResponse(rpcError(msg.id ?? null, ERR_INTERNAL, (err as Error).message), 500); + } + }, +}; diff --git a/mcp-server/test/smoke.sh b/mcp-server/test/smoke.sh new file mode 100755 index 00000000000..be3999ba709 --- /dev/null +++ b/mcp-server/test/smoke.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# +# Smoke test for the devdocs MCP server. Exercises the transport rules and every tool. +# +# ./bin/mcp-local.sh # in one terminal +# ./mcp-server/test/smoke.sh # in another +# +# Override the endpoint with MCP_URL to test a deployed Worker. +# +set -uo pipefail + +EP="${MCP_URL:-http://127.0.0.1:8787/mcp}" +PASS=0 +FAIL=0 + +green() { printf '\033[32m%s\033[0m' "$1"; } +red() { printf '\033[31m%s\033[0m' "$1"; } + +rpc() { + curl -s -X POST "$EP" \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H 'MCP-Protocol-Version: 2025-11-25' \ + -d "$1" +} + +# check +check() { + if printf '%s' "$3" | grep -qF -- "$2"; then + printf ' %s %s\n' "$(green '✓')" "$1"; PASS=$((PASS + 1)) + else + printf ' %s %s\n expected to contain: %s\n got: %s\n' \ + "$(red '✗')" "$1" "$2" "$(printf '%s' "$3" | head -c 300)"; FAIL=$((FAIL + 1)) + fi +} + +status() { curl -s -o /dev/null -w '%{http_code}' "$@"; } + +echo "Testing ${EP}" +echo +echo "Transport" +check "GET /mcp is 405 (no SSE stream offered)" "405" "$(status "$EP")" +check "DELETE /mcp is 405" "405" "$(status -X DELETE "$EP")" +check "OPTIONS /mcp is 204 (CORS preflight)" "204" "$(status -X OPTIONS "$EP")" +check "unsupported protocol version is 400" "400" "$(status -X POST -H 'MCP-Protocol-Version: 1999-01-01' "$EP")" +check "notification is 202 with no body" "202" \ + "$(status -X POST -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' "$EP")" +check "batched request is rejected" "Batched requests are not supported" \ + "$(rpc '[{"jsonrpc":"2.0","id":1,"method":"ping"}]')" +check "malformed JSON is a parse error" "-32700" \ + "$(curl -s -X POST "$EP" -H 'Content-Type: application/json' -d 'not json')" + +echo +echo "Lifecycle" +INIT=$(rpc '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"1"}}}') +check "initialize echoes the client protocol version" '"protocolVersion":"2025-06-18"' "$INIT" +check "initialize advertises the tools capability" '"tools"' "$INIT" +check "initialize carries usage instructions" '"instructions"' "$INIT" +check "unknown protocol version falls back to latest" '"protocolVersion":"2025-11-25"' \ + "$(rpc '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2030-01-01","capabilities":{}}}')" +check "ping works" '"result":{}' "$(rpc '{"jsonrpc":"2.0","id":2,"method":"ping"}')" +check "unknown method is -32601" '-32601' "$(rpc '{"jsonrpc":"2.0","id":3,"method":"nope/nope"}')" + +TOOLS=$(rpc '{"jsonrpc":"2.0","id":4,"method":"tools/list"}') +for t in search_docs get_doc list_sections; do + check "tools/list exposes ${t}" "\"name\":\"${t}\"" "$TOOLS" +done + +echo +echo "search_docs" +S=$(rpc '{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"search_docs","arguments":{"query":"register a hook from a module","limit":5}}}') +check "finds pages" "page(s) matching" "$S" +check "returns version 9 paths only" "/9/" "$S" +check "points at get_doc" "Call \`get_doc\`" "$S" +check "section filter narrows scope" "/9/webservice/" \ + "$(rpc '{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"search_docs","arguments":{"query":"authentication","section":"webservice","limit":3}}}')" +check "empty query is a tool error" '"isError":true' \ + "$(rpc '{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"search_docs","arguments":{"query":" "}}}')" +check "flags loose matches as approximate" "APPROXIMATE matches" \ + "$(rpc '{"jsonrpc":"2.0","id":13,"method":"tools/call","params":{"name":"search_docs","arguments":{"query":"zzzqqqxyzzy no such thing"}}}')" +check "reports nothing found when Algolia has nothing" "No results for" \ + "$(rpc '{"jsonrpc":"2.0","id":14,"method":"tools/call","params":{"name":"search_docs","arguments":{"query":"qqqzzzxyw vvvbbbnnn"}}}')" +check "exact matches are not flagged approximate" "page(s) matching" \ + "$(rpc '{"jsonrpc":"2.0","id":15,"method":"tools/call","params":{"name":"search_docs","arguments":{"query":"override a controller","limit":3}}}')" + +echo +echo "list_sections" +L=$(rpc '{"jsonrpc":"2.0","id":20,"method":"tools/call","params":{"name":"list_sections","arguments":{}}}') +check "lists top-level sections" "modules" "$L" +check "reports page counts" "pages) →" "$L" +check "expands a section" "/9/faq/upgrade/" \ + "$(rpc '{"jsonrpc":"2.0","id":21,"method":"tools/call","params":{"name":"list_sections","arguments":{"section":"faq"}}}')" +check "rejects unknown section" "Unknown section" \ + "$(rpc '{"jsonrpc":"2.0","id":22,"method":"tools/call","params":{"name":"list_sections","arguments":{"section":"nope"}}}')" + +echo +echo "get_doc" +D=$(rpc '{"jsonrpc":"2.0","id":30,"method":"tools/call","params":{"name":"get_doc","arguments":{"path":"/9/development/components/console/","max_chars":1000}}}') +check "returns markdown with front matter" 'title: ' "$D" +check "links back to the GitHub source" 'github.com/PrestaShop/docs/blob/9.x/' "$D" +check "explains how to page on truncation" 'offset=' "$D" +check "accepts a full URL" 'version: ' \ + "$(rpc '{"jsonrpc":"2.0","id":31,"method":"tools/call","params":{"name":"get_doc","arguments":{"path":"https://devdocs.prestashop-project.org/9/basics/installation/#anchor","max_chars":1000}}}')" +check "accepts an .md path" 'version: ' \ + "$(rpc '{"jsonrpc":"2.0","id":32,"method":"tools/call","params":{"name":"get_doc","arguments":{"path":"/9/faq/upgrade/index.md","max_chars":1000}}}')" +check "offset resumes mid-page" 'resuming at character 500' \ + "$(rpc '{"jsonrpc":"2.0","id":33,"method":"tools/call","params":{"name":"get_doc","arguments":{"path":"/9/basics/installation/","offset":500,"max_chars":1000}}}')" +check "refuses unsupported versions" '"isError":true' \ + "$(rpc '{"jsonrpc":"2.0","id":34,"method":"tools/call","params":{"name":"get_doc","arguments":{"path":"/8/basics/"}}}')" +check "refuses path traversal" '"isError":true' \ + "$(rpc '{"jsonrpc":"2.0","id":35,"method":"tools/call","params":{"name":"get_doc","arguments":{"path":"/9/../../etc/passwd"}}}')" +check "reports a missing page" 'No page at' \ + "$(rpc '{"jsonrpc":"2.0","id":36,"method":"tools/call","params":{"name":"get_doc","arguments":{"path":"/9/does/not/exist/"}}}')" +check "unknown tool is -32602" '-32602' \ + "$(rpc '{"jsonrpc":"2.0","id":37,"method":"tools/call","params":{"name":"nope","arguments":{}}}')" + +echo +if [ "$FAIL" -eq 0 ]; then + printf '%s %d checks passed\n' "$(green '✓')" "$PASS" +else + printf '%s %d passed, %d failed\n' "$(red '✗')" "$PASS" "$FAIL" +fi +exit $((FAIL > 0)) diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json new file mode 100644 index 00000000000..76f03404eac --- /dev/null +++ b/mcp-server/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "ES2022", + "moduleResolution": "Bundler", + "types": ["@cloudflare/workers-types"], + "strict": true, + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/mcp-server/wrangler.jsonc b/mcp-server/wrangler.jsonc new file mode 100644 index 00000000000..f7f3a260c7f --- /dev/null +++ b/mcp-server/wrangler.jsonc @@ -0,0 +1,43 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "prestashop-devdocs-mcp", + "main": "src/index.ts", + "compatibility_date": "2025-11-01", + "observability": { "enabled": true }, + + // All of these are public values, mirrored from src/config.yml of this repository. + // The Algolia key is the search-only DocSearch key already shipped in the site's + // JavaScript — it is not a secret. + "vars": { + "DOCS_BASE_URL": "https://devdocs.prestashop-project.org/", + "DOCS_VERSION": "9", + "ALGOLIA_APP_ID": "79DO4UR9Y5", + "ALGOLIA_API_KEY": "352524fc37137a45610faa44bcc1a47c", + "ALGOLIA_INDEX": "prestashop" + }, + + // `workers_dev` defaults to true, which would leave the Worker reachable at BOTH + // the custom domain and ..workers.dev. Two public URLs for one + // endpoint is a trap: whichever one people copy into their MCP client config is + // one you can never retire. Keep it true for the first deploy so there is + // something to test against, then flip it to false once the custom domain is + // verified, and publish only that. + "workers_dev": true, + + // Serves the MCP endpoint from a custom domain instead of *.workers.dev. + // prestashop-project.org is already an active zone on Cloudflare, so `wrangler + // deploy` creates the DNS record and provisions the certificate on its own — + // there is nothing to set up by hand. + // + // The hostname must stay a FIRST-LEVEL subdomain. Universal SSL on this zone + // covers `prestashop-project.org` and `*.prestashop-project.org`, and a wildcard + // matches exactly one label — so `devdocs-mcp.…` is covered but + // `mcp.devdocs.…` is not, and would fail TLS. Going deeper needs Advanced + // Certificate Manager (paid). + "routes": [ + { "pattern": "devdocs-mcp.prestashop-project.org", "custom_domain": true } + ] + // + // Note: the Cloudflare edge cache is bypassed on *.workers.dev, so a custom + // domain also gets you edge caching of the fetched documentation artifacts. +} diff --git a/package.json b/package.json index 8f681a18638..d9f4bd8c6f6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "scripts": { "dev": "cd src && hugo server --config config.yml,config-local.yml", "build": "cd src && hugo --config config.yml,config-local.yml", - "build:prod": "cd src && hugo --config config.yml" + "build:prod": "cd src && hugo --config config.yml", + "mcp:local": "./bin/mcp-local.sh", + "mcp:test": "./mcp-server/test/smoke.sh" }, "repository": { "type": "git", diff --git a/src/config.yml b/src/config.yml index 3dcb03e6c21..8d0f92d2de1 100644 --- a/src/config.yml +++ b/src/config.yml @@ -44,9 +44,36 @@ params: apiKey: "352524fc37137a45610faa44bcc1a47c" indexName: "prestashop" +# Machine-readable outputs consumed by the MCP server (see mcp-server/README.md) +outputFormats: + # Raw markdown source of every page, emitted next to its index.html + # e.g. /9/basics/introduction/index.md + markdown: + mediaType: "text/markdown" + baseName: "index" + isPlainText: true + permalinkable: false + notAlternative: true + # llms.txt convention: https://llmstxt.org/ -> /llms.txt + llms: + mediaType: "text/plain" + baseName: "llms" + isPlainText: true + permalinkable: false + notAlternative: true + # Compact page catalogue for the MCP server -> /mcp-index.json + mcpindex: + mediaType: "application/json" + baseName: "mcp-index" + isPlainText: true + permalinkable: false + notAlternative: true + # For search functionality outputs: - home: ["HTML", "RSS", "JSON"] + home: ["HTML", "RSS", "JSON", "LLMS", "MCPINDEX"] + section: ["HTML", "RSS", "MARKDOWN"] + page: ["HTML", "MARKDOWN"] markup: highlight: diff --git a/src/layouts/_default/list.md b/src/layouts/_default/list.md new file mode 100644 index 00000000000..25e4605f879 --- /dev/null +++ b/src/layouts/_default/list.md @@ -0,0 +1 @@ +{{- partial "llm-page.md" . -}} diff --git a/src/layouts/_default/single.md b/src/layouts/_default/single.md new file mode 100644 index 00000000000..25e4605f879 --- /dev/null +++ b/src/layouts/_default/single.md @@ -0,0 +1 @@ +{{- partial "llm-page.md" . -}} diff --git a/src/layouts/index.llms.txt b/src/layouts/index.llms.txt new file mode 100644 index 00000000000..36fc5231bee --- /dev/null +++ b/src/layouts/index.llms.txt @@ -0,0 +1,24 @@ +{{- $version := site.Params.versions.current -}} +{{- $root := site.GetPage (printf "/%s" $version) -}} +# {{ site.Title }} + +> {{ site.Params.description }} This file indexes the currently supported documentation (PrestaShop {{ $version }}.x). Every entry links to the raw markdown source of that page. + +Base URL: {{ site.BaseURL }} +Documentation version: {{ $version }}.x +Source repository: {{ site.Params.ghRepoURL }} (branch `{{ $version }}.x`) +Machine-readable page catalogue: {{ site.BaseURL }}mcp-index.json +{{ with $root }} +{{ range .RegularPages.ByWeight }} +- [{{ .Title }}]({{ with .OutputFormats.Get "markdown" }}{{ .Permalink }}{{ end }}){{ with .Description }}: {{ . }}{{ end }} +{{- end }} +{{ range .Sections.ByWeight }} +## {{ .Title }} +{{ with .Description }} +{{ . }} +{{ end }} +{{ range .RegularPagesRecursive.ByWeight }} +- [{{ .Title }}]({{ with .OutputFormats.Get "markdown" }}{{ .Permalink }}{{ end }}){{ with .Description }}: {{ . }}{{ end }} +{{- end }} +{{ end }} +{{- end -}} diff --git a/src/layouts/index.mcpindex.json b/src/layouts/index.mcpindex.json new file mode 100644 index 00000000000..3f6a915bd1d --- /dev/null +++ b/src/layouts/index.mcpindex.json @@ -0,0 +1,39 @@ +{{- /* + Compact catalogue of every page of the currently supported documentation + version. Consumed by the devdocs MCP server (mcp-server/) to power the + `list_sections` / `browse_docs` tools and to resolve `get_doc` paths. +*/ -}} +{{- $version := site.Params.versions.current -}} +{{- $root := site.GetPage (printf "/%s" $version) -}} +{{- $pages := slice -}} +{{- $sections := slice -}} +{{- with $root -}} + {{- range .Sections.ByWeight -}} + {{- $sections = $sections | append (dict + "slug" (path.Base .RelPermalink) + "title" .Title + "path" .RelPermalink + "description" .Description + "pageCount" (len .RegularPagesRecursive)) -}} + {{- end -}} +{{- end -}} +{{- range where site.RegularPages "Section" $version -}} + {{- $md := "" -}} + {{- with .OutputFormats.Get "markdown" -}}{{- $md = .RelPermalink -}}{{- end -}} + {{- $parts := split (strings.TrimPrefix "/" .RelPermalink) "/" -}} + {{- $sec := "" -}} + {{- if gt (len $parts) 2 -}}{{- $sec = index $parts 1 -}}{{- end -}} + {{- $pages = $pages | append (dict + "path" .RelPermalink + "markdown" $md + "title" .Title + "description" .Description + "section" $sec) -}} +{{- end -}} +{{- dict + "site" site.BaseURL + "docsVersion" $version + "generated" (now.Format "2006-01-02T15:04:05Z07:00") + "sections" $sections + "pages" $pages + | jsonify -}} diff --git a/src/layouts/partials/llm-page.md b/src/layouts/partials/llm-page.md new file mode 100644 index 00000000000..2a2a973f456 --- /dev/null +++ b/src/layouts/partials/llm-page.md @@ -0,0 +1,25 @@ +{{- /* + Renders a single documentation page as plain markdown with a YAML preamble. + Consumed by the `markdown` output format (//index.md) and, through it, + by the devdocs MCP server's `get_doc` tool. +*/ -}} +{{- $repoPath := "" -}} +{{- with .File -}} + {{- $repoPath = strings.TrimPrefix (printf "%s/" $.Section) (replace .Path "\\" "/") -}} +{{- end -}} +--- +title: {{ .Title | jsonify }} +url: {{ .Permalink | jsonify }} +version: {{ .Section | jsonify }} +{{ with .Description -}} +description: {{ . | jsonify }} +{{ end -}} +{{ if $repoPath -}} +source: {{ printf "%sblob/%s.x/%s" $.Site.Params.ghRepoURL $.Section $repoPath | jsonify }} +{{ end -}} +{{ if not .Lastmod.IsZero -}} +lastmod: {{ .Lastmod.Format "2006-01-02" | jsonify }} +{{ end -}} +--- + +{{ .RawContent }}