From 97a38ce5bd8eca3dd2b3f96ec8142795077c708e Mon Sep 17 00:00:00 2001 From: rpalekar Date: Tue, 16 Jun 2026 11:31:43 +0100 Subject: [PATCH] PIGS-864: Add render_pdf_page tool for inline page image rendering New MCP tool that renders a single PDF page (or clipped region) as an inline base64 PNG content block, with no disk writes. Enables agent workflows (e.g. PII redaction verification) where the model needs visual access to a specific page. Implementation composes existing platform endpoints: splitPdf isolates the target page, convertFile rasterises it to PNG at the platform's fixed 300 DPI, then fflate/jimp are used in-memory to unpack the ZIPs, crop to clipBox (in 72-dpi PDF points, top-left origin to match extract_pii/search_text_in_pdf/redact_pdf), and downscale to the requested DPI (50-300, default 150). Co-Authored-By: Claude Code --- CLAUDE.md | 1 + README.md | 5 + node-version/package-lock.json | 781 +++++++++++++++++++++++++-- node-version/package.json | 4 +- node-version/src/tools/index.ts | 2 + node-version/src/tools/rendering.ts | 162 ++++++ node-version/tests/rendering.test.ts | 238 ++++++++ 7 files changed, 1158 insertions(+), 35 deletions(-) create mode 100644 node-version/src/tools/rendering.ts create mode 100644 node-version/tests/rendering.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 7bacaca..6aec97f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,6 +57,7 @@ The codebase is **fully typed**. Treat any type error as a build failure. - **JSON-returning tools must use `outputTarget`** — any tool that produces JSON takes the shared `outputTargetSchema` param (`inline` | `file` | `both`, default `inline`) and returns via the `jsonResult` helper in `node-version/src/tools/jsonOutput.ts`. Inline returns parsed data under `data` so Claude can chain steps; `file`/`both` write to disk and return `outputFilename`. Default inline so transient flows don't litter the working folder. (Binary outputs like Excel/PDF remain file-only and are orthogonal to `outputTarget`.) +- **Inline image tools** (e.g. `render_pdf_page`) return an MCP `image` content block (`type: 'image'`, base64 `data`, `mimeType`) and never write to disk — `outputTarget` does not apply. ## Sub-Agents diff --git a/README.md b/README.md index 45b978f..b347216 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,11 @@ Split contract.pdf into two files: pages 1-5 and pages 6-10 using Nitro Add password protection to confidential.pdf in Desktop using Nitro ``` +**Render a PDF page as an image to verify content visually:** +``` +Using Nitro, render page 1 of contract.pdf in Downloads at 150 DPI so I can see it +``` + ## Data Handling When you invoke a Nitro PDF Services tool, the file you specify is uploaded to Nitro's hosted Platform API at `api.gonitro.com` for processing. Results are written back to your local disk. Your OAuth refresh token is stored only on your local device at `~/.nitro-mcp/session.json`; access tokens are kept in memory and never stored server-side. diff --git a/node-version/package-lock.json b/node-version/package-lock.json index e4fe477..928d2cf 100644 --- a/node-version/package-lock.json +++ b/node-version/package-lock.json @@ -12,10 +12,12 @@ "@modelcontextprotocol/sdk": "^1.12.0", "dotenv": "^17.4.2", "exceljs": "^4.4.0", + "fflate": "^0.8.2", + "jimp": "^1.6.0", "zod": "^3.24.3" }, "devDependencies": { - "@types/node": "^22.15.3", + "@types/node": "^22.19.21", "@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/parser": "^8.32.0", "@vitest/coverage-v8": "^4.1.8", @@ -92,6 +94,16 @@ "node": ">=18" } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -914,6 +926,418 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jimp/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.1.tgz", + "integrity": "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A==", + "license": "MIT", + "dependencies": { + "@jimp/file-ops": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "await-to-js": "^3.0.0", + "exif-parser": "^0.1.12", + "file-type": "^21.3.3", + "mime": "3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/diff": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.1.tgz", + "integrity": "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ==", + "license": "MIT", + "dependencies": { + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "pixelmatch": "^5.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/file-ops": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.1.tgz", + "integrity": "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-bmp": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.1.tgz", + "integrity": "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "bmp-ts": "^1.0.9" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-gif": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.1.tgz", + "integrity": "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "gifwrap": "^0.10.1", + "omggif": "^1.0.10" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-jpeg": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.1.tgz", + "integrity": "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "jpeg-js": "^0.4.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-png": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.1.tgz", + "integrity": "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "pngjs": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-tiff": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.1.tgz", + "integrity": "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "utif2": "^4.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-blit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.1.tgz", + "integrity": "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-blur": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.1.tgz", + "integrity": "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/utils": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-circle": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.1.tgz", + "integrity": "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-color": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.1.tgz", + "integrity": "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "tinycolor2": "^1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.1.tgz", + "integrity": "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.1.tgz", + "integrity": "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.1.tgz", + "integrity": "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.1.tgz", + "integrity": "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.1.tgz", + "integrity": "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.1.tgz", + "integrity": "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.1.tgz", + "integrity": "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-hash": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.1.tgz", + "integrity": "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "any-base": "^1.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.1.tgz", + "integrity": "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.1.tgz", + "integrity": "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/types": "1.6.1", + "parse-bmfont-ascii": "^1.0.6", + "parse-bmfont-binary": "^1.0.6", + "parse-bmfont-xml": "^1.1.6", + "simple-xml-to-json": "^1.2.2", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-quantize": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.1.tgz", + "integrity": "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww==", + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.1.tgz", + "integrity": "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.1.tgz", + "integrity": "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.1.tgz", + "integrity": "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/types": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.1.tgz", + "integrity": "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w==", + "license": "MIT", + "dependencies": { + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.1", + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1104,9 +1528,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1124,9 +1545,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1144,9 +1562,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1164,9 +1579,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1184,9 +1596,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1204,9 +1613,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1300,6 +1706,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1344,9 +1773,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "version": "22.19.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz", + "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==", "dev": true, "license": "MIT", "dependencies": { @@ -1815,6 +2244,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "license": "MIT" + }, "node_modules/archiver": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", @@ -1968,6 +2403,15 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2037,6 +2481,12 @@ "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", "license": "MIT" }, + "node_modules/bmp-ts": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz", + "integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -2902,6 +3352,11 @@ "node": ">=8.3.0" } }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -3040,6 +3495,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3053,6 +3514,24 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -3232,6 +3711,16 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/gifwrap": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", + "integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==", + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3392,6 +3881,21 @@ "node": ">= 4" } }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "license": "MIT", + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "license": "MIT" + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -3540,6 +4044,44 @@ "node": ">=8" } }, + "node_modules/jimp": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.1.tgz", + "integrity": "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/diff": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-gif": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-blur": "1.6.1", + "@jimp/plugin-circle": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-contain": "1.6.1", + "@jimp/plugin-cover": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-displace": "1.6.1", + "@jimp/plugin-dither": "1.6.1", + "@jimp/plugin-fisheye": "1.6.1", + "@jimp/plugin-flip": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/plugin-mask": "1.6.1", + "@jimp/plugin-print": "1.6.1", + "@jimp/plugin-quantize": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/plugin-rotate": "1.6.1", + "@jimp/plugin-threshold": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -3559,6 +4101,12 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -3865,9 +4413,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3889,9 +4434,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3913,9 +4455,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3937,9 +4476,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4171,6 +4707,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -4315,6 +4863,12 @@ ], "license": "MIT" }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4405,6 +4959,28 @@ "node": ">=6" } }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -4479,6 +5055,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pixelmatch": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", + "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", + "license": "ISC", + "dependencies": { + "pngjs": "^6.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", @@ -4488,6 +5085,15 @@ "node": ">=16.20.0" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -4828,6 +5434,15 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/saxes": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", @@ -5010,6 +5625,15 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-xml-to-json": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.7.tgz", + "integrity": "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==", + "license": "MIT", + "engines": { + "node": ">=20.12.2" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5065,6 +5689,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5101,6 +5741,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", @@ -5156,6 +5802,24 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", @@ -5755,6 +6419,18 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -5829,6 +6505,15 @@ "punycode": "^2.1.0" } }, + "node_modules/utif2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", + "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.11" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6073,6 +6758,34 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/node-version/package.json b/node-version/package.json index 0894835..43f1e00 100644 --- a/node-version/package.json +++ b/node-version/package.json @@ -23,13 +23,15 @@ "@modelcontextprotocol/sdk": "^1.12.0", "dotenv": "^17.4.2", "exceljs": "^4.4.0", + "fflate": "^0.8.2", + "jimp": "^1.6.0", "zod": "^3.24.3" }, "overrides": { "uuid": "^14" }, "devDependencies": { - "@types/node": "^22.15.3", + "@types/node": "^22.19.21", "@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/parser": "^8.32.0", "@vitest/coverage-v8": "^4.1.8", diff --git a/node-version/src/tools/index.ts b/node-version/src/tools/index.ts index aba3063..72c2bd4 100644 --- a/node-version/src/tools/index.ts +++ b/node-version/src/tools/index.ts @@ -5,6 +5,7 @@ import { register as registerExtractions } from './extractions.js'; import { register as registerFileManagement } from './fileManagement.js'; import { register as registerGenerations } from './generations.js'; import { register as registerPii } from './pii.js'; +import { register as registerRendering } from './rendering.js'; import { register as registerTransformations } from './transformations.js'; export function registerAll(server: McpServer, context: AppContext): void { @@ -13,5 +14,6 @@ export function registerAll(server: McpServer, context: AppContext): void { registerExtractions(server, context); registerGenerations(server, context); registerPii(server, context); + registerRendering(server, context); registerTransformations(server, context); } diff --git a/node-version/src/tools/rendering.ts b/node-version/src/tools/rendering.ts new file mode 100644 index 0000000..e292e8e --- /dev/null +++ b/node-version/src/tools/rendering.ts @@ -0,0 +1,162 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { unzipSync } from 'fflate'; +import { Jimp } from 'jimp'; +import { z } from 'zod'; +import { READ_ONLY } from './annotations.js'; +import type { AppContext } from '../context.js'; +import { getDep } from '../context.js'; +import { FileFormat } from '../client/enums.js'; +import { handleToolError, UserFacingError } from '../errors.js'; +import { singleFileInputSchema } from '../models.js'; + +const _PLATFORM_RENDER_DPI = 300; +const _MAX_DPI = 300; +const _MIN_DPI = 50; +const _DEFAULT_DPI = 150; +const _POINTS_PER_INCH = 72; + +function _unzipSingleEntry(zipBytes: Buffer): Buffer { + const entries = unzipSync(new Uint8Array(zipBytes)); + const names = Object.keys(entries); + if (names.length === 0) { + throw new UserFacingError('Platform returned an empty archive while rendering the page.'); + } + if (names.length > 1) { + throw new UserFacingError( + `Expected single-entry archive, got ${String(names.length)} entries: ${names.join(', ')}.`, + ); + } + const [name] = names as [string]; + return Buffer.from(entries[name] as Uint8Array); +} + +interface _ClipBoxPx { + readonly x: number; + readonly y: number; + readonly w: number; + readonly h: number; +} + +function _clipBoxToPixels(clipBox: number[], renderDpi: number): _ClipBoxPx { + const factor = renderDpi / _POINTS_PER_INCH; + const [x, y, w, h] = clipBox as [number, number, number, number]; + return { + x: Math.max(0, Math.round(x * factor)), + y: Math.max(0, Math.round(y * factor)), + w: Math.max(1, Math.round(w * factor)), + h: Math.max(1, Math.round(h * factor)), + }; +} + +async function _processImage( + pngBytes: Buffer, + targetDpi: number, + clipBox: number[] | undefined, +): Promise<{ bytes: Buffer; width: number; height: number }> { + const image = await Jimp.read(pngBytes); + + if (clipBox !== undefined) { + const px = _clipBoxToPixels(clipBox, _PLATFORM_RENDER_DPI); + const clampedW = Math.min(px.w, image.width - px.x); + const clampedH = Math.min(px.h, image.height - px.y); + if (clampedW <= 0 || clampedH <= 0) { + throw new UserFacingError( + 'clipBox lies outside the rendered page bounds; check pageIndex and coordinates.', + ); + } + image.crop({ x: px.x, y: px.y, w: clampedW, h: clampedH }); + } + + if (targetDpi !== _PLATFORM_RENDER_DPI) { + const scale = targetDpi / _PLATFORM_RENDER_DPI; + const newW = Math.max(1, Math.round(image.width * scale)); + const newH = Math.max(1, Math.round(image.height * scale)); + image.resize({ w: newW, h: newH }); + } + + const bytes = await image.getBuffer('image/png'); + return { bytes, width: image.width, height: image.height }; +} + +export function register(server: McpServer, context: AppContext): void { + server.registerTool( + 'render_pdf_page', + { + description: + 'Render a single PDF page (or a clipped region of it) as an inline PNG image, ' + + 'returned directly in the tool result with no file written to disk. ' + + 'Useful for visually verifying redactions, inspecting layout near a detected PII box, ' + + 'or any case where the model needs to see what a page actually looks like. ' + + 'Use selectively (one page or region per call) — typically on pages flagged by ' + + 'extract_pii or search_text_in_pdf.', + inputSchema: { + ...singleFileInputSchema.shape, + pageIndex: z.number().int().min(0).describe('Zero-based index of the page to render.'), + dpi: z + .number() + .int() + .min(_MIN_DPI) + .max(_MAX_DPI) + .default(_DEFAULT_DPI) + .describe( + `Target rendering DPI (${String(_MIN_DPI)}–${String(_MAX_DPI)}, default ${String(_DEFAULT_DPI)}). ` + + 'Higher DPI yields a sharper image at the cost of token usage; use lower values ' + + '(100–150) for full pages and higher values (up to 300) when clipBox is provided.', + ), + clipBox: z + .array(z.number()) + .length(4) + .optional() + .describe( + 'Optional clip region in PDF points (1/72"), top-left origin, ' + + '[x, y, width, height]. Matches the coordinate space used by extract_pii, ' + + 'search_text_in_pdf, and redact_pdf so detection boxes can be passed through directly.', + ), + }, + annotations: READ_ONLY('Render PDF Page'), + }, + async (args) => { + try { + const filesHandler = getDep(context, 'filesHandler'); + const platformHandler = getDep(context, 'platformHandler'); + + const pdfBytes = filesHandler.read(args.inputPath); + + const splitZipBytes = await platformHandler.splitPdf(pdfBytes, [[args.pageIndex]]); + const singlePagePdf = _unzipSingleEntry(splitZipBytes); + + const pngZipBytes = await platformHandler.convertFile( + singlePagePdf, + FileFormat.PDF, + FileFormat.PNG, + ); + const renderedPng = _unzipSingleEntry(pngZipBytes); + + const { bytes, width, height } = await _processImage(renderedPng, args.dpi, args.clipBox); + const base64 = bytes.toString('base64'); + + const structured = { + pageIndex: args.pageIndex, + dpi: args.dpi, + widthPx: width, + heightPx: height, + ...(args.clipBox !== undefined && { clipBox: args.clipBox }), + }; + + const summary = + `Rendered page ${String(args.pageIndex)} at ${String(args.dpi)} DPI ` + + `(${String(width)}×${String(height)} px${args.clipBox !== undefined ? ', clipped' : ''}).`; + + return { + structuredContent: structured, + content: [ + { type: 'image' as const, data: base64, mimeType: 'image/png' as const }, + { type: 'text' as const, text: summary }, + ], + }; + } catch (err) { + return handleToolError('render_pdf_page', err); + } + }, + ); +} diff --git a/node-version/tests/rendering.test.ts b/node-version/tests/rendering.test.ts new file mode 100644 index 0000000..51cca6a --- /dev/null +++ b/node-version/tests/rendering.test.ts @@ -0,0 +1,238 @@ +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { zipSync } from 'fflate'; +import { Jimp } from 'jimp'; +import { register } from '../src/tools/rendering.js'; +import { FileFormat } from '../src/client/enums.js'; +import { + createAppContext, + type MockFilesHandler, + type MockPlatformHandler, +} from './helpers/fixtures.js'; +import { ToolCaller } from './helpers/toolCaller.js'; +import type { FilesHandler } from '../src/handlers/filesHandler.js'; +import type { PlatformHandler } from '../src/handlers/platformHandler.js'; + +async function _createToolCaller( + context: ReturnType, +): Promise<{ caller: ToolCaller; cleanup: () => Promise }> { + const server = new McpServer({ name: 'test', version: '0.0.0' }); + register(server, context); + + const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair(); + const client = new Client({ name: 'test-client', version: '0.0.0' }); + + await server.connect(serverTransport); + await client.connect(clientTransport); + + return { + caller: new ToolCaller(client), + cleanup: async (): Promise => { + await client.close(); + }, + }; +} + +async function _makePng(width: number, height: number): Promise { + const image = new Jimp({ width, height, color: 0xff0000ff }); + return image.getBuffer('image/png'); +} + +function _zipOne(name: string, bytes: Buffer): Buffer { + return Buffer.from(zipSync({ [name]: new Uint8Array(bytes) })); +} + +describe('render_pdf_page tool', () => { + let filesHandlerMock: MockFilesHandler; + let platformHandlerMock: MockPlatformHandler; + let caller: ToolCaller; + let cleanup: () => Promise; + const tmpDir = '/tmp/test-dir'; + const inputPath = path.join(tmpDir, 'doc.pdf'); + + beforeEach(async () => { + filesHandlerMock = { + read: vi.fn(), + write: vi.fn(), + listFiles: vi.fn(), + }; + platformHandlerMock = { + convertFile: vi.fn(), + getPdfMetadata: vi.fn(), + extractPdfData: vi.fn(), + extractTextBoundingBoxes: vi.fn(), + mergePdfs: vi.fn(), + compressPdf: vi.fn(), + splitPdf: vi.fn(), + rotatePdf: vi.fn(), + protectPdf: vi.fn(), + unprotectPdf: vi.fn(), + deletePdfPages: vi.fn(), + setPdfMetadata: vi.fn(), + flattenPdf: vi.fn(), + extractPiiBoundingBoxes: vi.fn(), + redactPdf: vi.fn(), + watermarkPdf: vi.fn(), + ocrPdf: vi.fn(), + optimizePdf: vi.fn(), + fillForms: vi.fn(), + extractExpenseData: vi.fn(), + }; + const context = createAppContext({ + filesHandler: filesHandlerMock as unknown as FilesHandler, + platformHandler: platformHandlerMock as unknown as PlatformHandler, + }); + ({ caller, cleanup } = await _createToolCaller(context)); + }); + + afterEach(async () => { + await cleanup(); + vi.restoreAllMocks(); + }); + + it('renders a full page, returning an inline image and structured metadata', async () => { + filesHandlerMock.read.mockReturnValue(Buffer.from('pdf-bytes')); + platformHandlerMock.splitPdf.mockResolvedValue( + _zipOne('page-0.pdf', Buffer.from('single-page-pdf')), + ); + // platform always renders at 300 DPI; use a small image as a stand-in + const renderedPng = await _makePng(600, 800); + platformHandlerMock.convertFile.mockResolvedValue(_zipOne('page-0.png', renderedPng)); + + const result = await caller.call('render_pdf_page', { + inputPath, + pageIndex: 2, + dpi: 150, + }); + + expect(filesHandlerMock.read).toHaveBeenCalledWith(inputPath); + expect(platformHandlerMock.splitPdf).toHaveBeenCalledWith(Buffer.from('pdf-bytes'), [[2]]); + expect(platformHandlerMock.convertFile).toHaveBeenCalledWith( + Buffer.from('single-page-pdf'), + FileFormat.PDF, + FileFormat.PNG, + ); + // No disk writes for intermediates + expect(filesHandlerMock.write).not.toHaveBeenCalled(); + + expect(result.structuredContent).toEqual({ + pageIndex: 2, + dpi: 150, + widthPx: 300, + heightPx: 400, + }); + const content = ( + result as unknown as { content: { type: string; mimeType?: string; text?: string }[] } + ).content; + expect(content).toHaveLength(2); + const [imageBlock, textBlock] = content as [ + { type: string; mimeType?: string }, + { type: string; text?: string }, + ]; + expect(imageBlock.type).toBe('image'); + expect(imageBlock.mimeType).toBe('image/png'); + expect(textBlock.type).toBe('text'); + expect(textBlock.text).toContain('150 DPI'); + expect(textBlock.text).toContain('300×400 px'); + }); + + it('renders at the platform DPI without resizing when dpi=300', async () => { + filesHandlerMock.read.mockReturnValue(Buffer.from('pdf-bytes')); + platformHandlerMock.splitPdf.mockResolvedValue(_zipOne('page-0.pdf', Buffer.from('pdf'))); + const renderedPng = await _makePng(400, 200); + platformHandlerMock.convertFile.mockResolvedValue(_zipOne('page-0.png', renderedPng)); + + const result = await caller.call('render_pdf_page', { + inputPath, + pageIndex: 0, + dpi: 300, + }); + + expect(result.structuredContent).toMatchObject({ widthPx: 400, heightPx: 200, dpi: 300 }); + }); + + it('crops to clipBox (in points) and reports cropped pixel dimensions', async () => { + filesHandlerMock.read.mockReturnValue(Buffer.from('pdf-bytes')); + platformHandlerMock.splitPdf.mockResolvedValue(_zipOne('page-0.pdf', Buffer.from('pdf'))); + const renderedPng = await _makePng(600, 800); + platformHandlerMock.convertFile.mockResolvedValue(_zipOne('page-0.png', renderedPng)); + + // 72 pt × 144 pt clip at top-left → at 300 DPI: 300×600 px, then dpi=300 stays as-is + const result = await caller.call('render_pdf_page', { + inputPath, + pageIndex: 0, + dpi: 300, + clipBox: [0, 0, 72, 144], + }); + + expect(result.structuredContent).toEqual({ + pageIndex: 0, + dpi: 300, + widthPx: 300, + heightPx: 600, + clipBox: [0, 0, 72, 144], + }); + }); + + it('rejects a clipBox that lies outside the rendered page bounds', async () => { + filesHandlerMock.read.mockReturnValue(Buffer.from('pdf-bytes')); + platformHandlerMock.splitPdf.mockResolvedValue(_zipOne('page-0.pdf', Buffer.from('pdf'))); + const renderedPng = await _makePng(100, 100); + platformHandlerMock.convertFile.mockResolvedValue(_zipOne('page-0.png', renderedPng)); + + await caller.call( + 'render_pdf_page', + { + inputPath, + pageIndex: 0, + dpi: 150, + clipBox: [10000, 10000, 50, 50], + }, + { expectError: true }, + ); + }); + + it('errors when the split step returns an empty archive', async () => { + filesHandlerMock.read.mockReturnValue(Buffer.from('pdf-bytes')); + platformHandlerMock.splitPdf.mockResolvedValue(Buffer.from(zipSync({}))); + + await caller.call( + 'render_pdf_page', + { inputPath, pageIndex: 0, dpi: 150 }, + { expectError: true }, + ); + expect(platformHandlerMock.convertFile).not.toHaveBeenCalled(); + }); + + it('errors when the convert step returns a multi-entry archive', async () => { + filesHandlerMock.read.mockReturnValue(Buffer.from('pdf-bytes')); + platformHandlerMock.splitPdf.mockResolvedValue(_zipOne('page-0.pdf', Buffer.from('pdf'))); + const png = await _makePng(10, 10); + platformHandlerMock.convertFile.mockResolvedValue( + Buffer.from( + zipSync({ + 'page-0.png': new Uint8Array(png), + 'page-1.png': new Uint8Array(png), + }), + ), + ); + + await caller.call( + 'render_pdf_page', + { inputPath, pageIndex: 0, dpi: 150 }, + { expectError: true }, + ); + }); + + it('rejects dpi above the cap', async () => { + await caller.call( + 'render_pdf_page', + { inputPath, pageIndex: 0, dpi: 600 }, + { expectError: true }, + ); + expect(platformHandlerMock.splitPdf).not.toHaveBeenCalled(); + }); +});