Bound MCP memory consumption by limiting parallel exports & response size (#9748)

*  Bound the size of plugin task responses

When using the integrated remote MCP server, bound response size.
All responses are passed to LLMs, which themselves impose bounds.
This is a measure to bound memory usage in the centrally provided
MCP server.

GitHub #9493

*  Bound parallelism in ExportShapeTool

Use an integer semaphore to bound parallel requests to this
memory-intensive tool, thus bounding memory usage.

GitHub #9493

*  Add (manual) integration test script for ExportShapeTool parallelism

Add dependency tsx to facilitate executions.

GitHub #9493

*  Make number of parallel export requests configurable in ExportShapeTool

Use env var PENPOT_MCP_EXPORT_SHAPE_MAX_PARALLEL_REQUESTS to configure
the maximum number of requests in multi-user mode (default 0, no limit).
This commit is contained in:
Dr. Dominik Jain
2026-05-19 19:37:29 +02:00
committed by GitHub
parent 6be4f157d6
commit 14b53ecfec
9 changed files with 591 additions and 19 deletions

View File

@@ -264,14 +264,15 @@ The Penpot MCP server can be configured using environment variables.
### Server Configuration
| Environment Variable | Description | Default |
|------------------------------------|----------------------------------------------------------------------------|--------------|
| `PENPOT_MCP_SERVER_HOST` | Address on which the MCP server listens (binds to) | `localhost` |
| `PENPOT_MCP_SERVER_PORT` | Port for the HTTP/SSE server | `4401` |
| `PENPOT_MCP_WEBSOCKET_PORT` | Port for the WebSocket server (plugin connection) | `4402` |
| `PENPOT_MCP_REPL_PORT` | Port for the REPL server (development/debugging) | `4403` |
| `PENPOT_MCP_REMOTE_MODE` | Enable remote mode (disables file system access). Set to `true` to enable. | `false` |
| `PENPOT_MCP_DEVENV` | Enable Penpot development environment tools. Set to `true` to enable. | `false` |
| Environment Variable | Description | Default |
|--------------------------------------------------|----------------------------------------------------------------------------|----------------|
| `PENPOT_MCP_SERVER_HOST` | Address on which the MCP server listens (binds to) | `localhost` |
| `PENPOT_MCP_SERVER_PORT` | Port for the HTTP/SSE server | `4401` |
| `PENPOT_MCP_WEBSOCKET_PORT` | Port for the WebSocket server (plugin connection) | `4402` |
| `PENPOT_MCP_REPL_PORT` | Port for the REPL server (development/debugging) | `4403` |
| `PENPOT_MCP_REMOTE_MODE` | Enable remote mode (disables file system access). Set to `true` to enable. | `false` |
| `PENPOT_MCP_DEVENV` | Enable Penpot development environment tools. Set to `true` to enable. | `false` |
| `PENPOT_MCP_EXPORT_SHAPE_MAX_PARALLEL_REQUESTS` | Maximum number of parallel export shape requests (multi-user mode only). | `0` (no limit) |
### Logging Configuration

View File

@@ -24,8 +24,10 @@ export class Task<TParams = any> {
return;
}
// create response object
const response = {
type: "task-response",
// NOTE: This inner response schema also constructed in main.ts/sendTaskResponse.
response: {
id: this.requestId,
success: success,

View File

@@ -1,12 +1,29 @@
import "./style.css";
/**
* the maximum allowed size for task responses sent back to the MCP server in the integrated remote MCP mode.
* This bounds the JSON response size.
* Note that in the remote MCP case, responses are transferred to LLMs (not the file system) and LLMs have
* size limitations. This serves to bound the size of returned images in particular.
* Too many overly large simultaneous responses can cause OOM issues in the MCP server, so this contributes
* to bounding memory usage in the centrally provided MCP server.
*/
const MAX_TASK_RESPONSE_SIZE_REMOTE_MCP = 15_000_000;
// get the current theme from the URL
const searchParams = new URLSearchParams(window.location.hash.split("?")[1]);
document.body.dataset.theme = searchParams.get("theme") ?? "light";
// WebSocket connection management
// WebSocket connection to the MCP server
let ws: WebSocket | null = null;
/**
* indicates whether the plugin is running with the Penpot-integrated remote MCP server enabled
* (as opposed to a local server used with the explicitly loaded plugin);
* set via the "mcp-mode" message sent by plugin.ts on initialization
*/
let isIntegratedRemoteMcp = false;
const statusPill = document.getElementById("connection-status") as HTMLElement;
const statusText = document.getElementById("status-text") as HTMLElement;
const currentTaskEl = document.getElementById("current-task") as HTMLElement;
@@ -79,7 +96,22 @@ function updateExecutedCode(code: string | null): void {
*/
function sendTaskResponse(response: any): void {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(response));
let responseString = JSON.stringify(response);
if (isIntegratedRemoteMcp && responseString.length > MAX_TASK_RESPONSE_SIZE_REMOTE_MCP) {
const errorMessage = `Serialised response size (${responseString.length}) exceeds maximum of ${MAX_TASK_RESPONSE_SIZE_REMOTE_MCP}.`;
console.warn(
errorMessage +
" [integrated remote MCP mode restriction]; sending error response instead; original response:",
response
);
response = {
id: response.id,
success: false,
error: errorMessage,
};
responseString = JSON.stringify(response);
}
ws.send(responseString);
console.log("Sent response to MCP server:", response);
} else {
console.error("WebSocket not connected, cannot send response");
@@ -176,6 +208,9 @@ disconnectBtn?.addEventListener("click", () => {
// Listen plugin.ts messages
window.addEventListener("message", (event) => {
if (event.data.type === "mcp-mode") {
isIntegratedRemoteMcp = event.data.integratedRemoteMcp;
}
if (event.data.type === "start-server") {
connectToMcpServer(event.data.url, event.data.token);
}

View File

@@ -1,6 +1,12 @@
import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler";
import { Task, TaskHandler } from "./TaskHandler";
/**
* indicates whether the plugin is running in an environment with the Penpot-integrated remote MCP server
* enabled (as opposed to a local server used with the explicitly loaded plugin)
*/
const isIntegratedRemoteMcp = !!mcp;
/**
* Extracts the major.minor.patch prefix from a version string.
*
@@ -23,12 +29,17 @@ const taskHandlers: TaskHandler[] = [new ExecuteCodeTaskHandler()];
penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}`, {
width: 236,
height: 210,
hidden: !!mcp,
hidden: isIntegratedRemoteMcp,
} as any);
// Register message handlers
penpot.ui.onMessage<string | { id: string; type?: string; status?: string; task: string; params: any }>((message) => {
if (typeof message === "object" && message.type === "ui-initialized") {
// Inform the UI about the operating mode
penpot.ui.sendMessage({
type: "mcp-mode",
integratedRemoteMcp: isIntegratedRemoteMcp,
});
// Check Penpot version compatibility
const penpotVersionPrefix = penpot.version ? extractVersionPrefix(penpot.version) : "<2.15"; // pre-2.15 versions don't have version info
const mcpVersionPrefix = extractVersionPrefix(PENPOT_MCP_VERSION);
@@ -42,7 +53,7 @@ penpot.ui.onMessage<string | { id: string; type?: string; status?: string; task:
});
}
// Initiate connection to remote MCP server (if enabled)
if (mcp) {
if (isIntegratedRemoteMcp) {
penpot.ui.sendMessage({
type: "start-server",
url: mcp?.getServerUrl(),

View File

@@ -12,6 +12,7 @@
"start:multi-user": "node dist/index.js --multi-user",
"start:dev": "node --import ts-node/register src/index.ts",
"start:dev:multi-user": "node --loader ts-node/esm src/index.ts --multi-user",
"test:integration:export-sema": "tsx scripts/integration-test-export-image-semaphore.ts",
"types:check": "tsc --noEmit",
"clean": "rm -rf dist/"
},
@@ -40,14 +41,15 @@
"zod": "^4.3.6"
},
"devDependencies": {
"cross-env": "^7.0.3",
"@penpot/mcp-common": "workspace:../common",
"@types/express": "^4.17.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.0.0",
"@types/ws": "^8.5.10",
"cross-env": "^7.0.3",
"esbuild": "^0.25.0",
"ts-node": "^10.9.2",
"tsx": "^4.22.3",
"typescript": "^5.0.0"
},
"ts-node": {

View File

@@ -0,0 +1,116 @@
/**
* One-off integration test for the parallelism bound around image exports.
*
* Setup:
* - Stubs ExportShapeTool.exportImage to sleep SLEEP_MS instead of doing real work,
* so no actual plugin connection is needed.
* - Replaces the static parallelism semaphore with one of size N.
* - Starts a PenpotMcpServer in multi-user mode on three random free ports.
* - Fires M > N parallel MCP clients that each call the export_shape tool.
*
* Expectations (observed manually from the server's console output):
* - "Semaphore 'ExportShapeTool' saturated; request queued (k waiting)" lines appear
* at INFO level (at least M - N of them).
* - All M tool calls return successfully.
* - Total elapsed wall-clock time is approximately ceil(M / N) * SLEEP_MS.
*
* Invoke from packages/server with:
* pnpm run test:integration:export
*/
import * as net from "node:net";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { PenpotMcpServer } from "../src/PenpotMcpServer";
import { ExportShapeTool } from "../src/tools/ExportShapeTool";
import { TextResponse } from "../src/ToolResponse";
import { Semaphore } from "../src/utils/Semaphore";
// === parameters ===
const N = 3;
const M = 6;
const SLEEP_MS = 5_000;
// === helpers ===
async function findFreePort(): Promise<number> {
return new Promise((resolve, reject) => {
const srv = net.createServer();
srv.unref();
srv.on("error", reject);
srv.listen(0, "127.0.0.1", () => {
const port = (srv.address() as net.AddressInfo).port;
srv.close(() => resolve(port));
});
});
}
async function callExportShape(url: URL, idx: number): Promise<unknown> {
const client = new Client({ name: `integration-test-client-${idx}`, version: "0.0.0" });
const transport = new StreamableHTTPClientTransport(url);
await client.connect(transport);
try {
return await client.callTool({
name: "export_shape",
arguments: { shapeId: "selection" },
});
} finally {
await client.close();
}
}
// === main ===
async function main(): Promise<void> {
// dynamic ports must be set before PenpotMcpServer is constructed
const httpPort = await findFreePort();
process.env.PENPOT_MCP_SERVER_HOST = "127.0.0.1";
process.env.PENPOT_MCP_SERVER_PORT = String(httpPort);
process.env.PENPOT_MCP_WEBSOCKET_PORT = String(await findFreePort());
process.env.PENPOT_MCP_REPL_PORT = String(await findFreePort());
// shrink the gate and stub the worker
(ExportShapeTool as any).parallelismSemaphore = new Semaphore("ExportShapeTool", N);
(ExportShapeTool.prototype as any).exportImage = async (): Promise<TextResponse> => {
await new Promise((r) => setTimeout(r, SLEEP_MS));
return new TextResponse("stubbed export");
};
const server = new PenpotMcpServer(/* isMultiUser */ true);
await server.start();
console.log(`\n=== integration test: N=${N} permits, M=${M} clients, sleep=${SLEEP_MS}ms ===\n`);
// fire M parallel tool calls, each via its own MCP session with a distinct userToken
const start = Date.now();
const results = await Promise.allSettled(
Array.from({ length: M }, (_, i) =>
callExportShape(new URL(`http://127.0.0.1:${httpPort}/mcp?userToken=test-token-${i}`), i)
)
);
const elapsed = Date.now() - start;
await server.stop();
// report
const failures = results.map((r, i) => ({ r, i })).filter(({ r }) => r.status === "rejected");
console.log(`\n=== results ===`);
console.log(` elapsed: ${elapsed}ms (expected ~${Math.ceil(M / N) * SLEEP_MS}ms)`);
console.log(` succeeded: ${M - failures.length}/${M}`);
if (failures.length > 0) {
console.log(` failures:`);
for (const { r, i } of failures) {
console.log(` [${i}] ${(r as PromiseRejectedResult).reason}`);
}
process.exit(1);
}
console.log(`\nAll ${M} tool calls succeeded. Scroll up to verify saturation log lines.\n`);
process.exit(0);
}
main().catch((err) => {
console.error("Integration test crashed:", err);
process.exit(1);
});

View File

@@ -1,10 +1,12 @@
import { z } from "zod";
import { Tool } from "../Tool";
import { ImageContent, PNGImageContent, PNGResponse, TextContent, TextResponse, ToolResponse } from "../ToolResponse";
import { ImageContent, PNGResponse, TextContent, TextResponse, ToolResponse } from "../ToolResponse";
import "reflect-metadata";
import { PenpotMcpServer } from "../PenpotMcpServer";
import { ExecuteCodePluginTask } from "../tasks/ExecuteCodePluginTask";
import { createLogger } from "../logger";
import { FileUtils } from "../utils/FileUtils";
import { Semaphore } from "../utils/Semaphore";
import sharp from "sharp";
/**
@@ -49,6 +51,38 @@ export class ExportShapeArgs {
* Tool for executing JavaScript code in the Penpot plugin context
*/
export class ExportShapeTool extends Tool<ExportShapeArgs> {
/**
* Maximum number of image-export operations that may run concurrently in multi-user mode.
* Configurable via the PENPOT_MCP_EXPORT_SHAPE_MAX_PARALLEL_REQUESTS environment variable;
* defaults to 0, meaning no limit.
*
* When set to a positive value (and combined with the plugin-side per-response cap
* MAX_TASK_RESPONSE_SIZE_REMOTE_MCP, ~15 MB JSON), this caps the in-flight memory
* footprint of image exports at roughly N x cap on the centrally hosted MCP server.
*/
private static readonly MAX_PARALLEL_EXPORTS = parseInt(
process.env.PENPOT_MCP_EXPORT_SHAPE_MAX_PARALLEL_REQUESTS ?? "0",
10
);
/**
* Gates concurrent export operations across all tool instances (one per session in
* multi-user mode). Static because instances are per-session, but the bound has to
* apply across the whole process. Permits beyond the maximum queue in FIFO order.
* Undefined when MAX_PARALLEL_EXPORTS is non-positive, indicating no limit.
*/
private static readonly parallelismSemaphore: Semaphore | undefined =
ExportShapeTool.MAX_PARALLEL_EXPORTS > 0
? new Semaphore("ExportShapeTool", ExportShapeTool.MAX_PARALLEL_EXPORTS)
: undefined;
static {
createLogger("ExportShapeTool").info(
"Max parallel exports (multi-user mode): %d (0 = unbounded)",
ExportShapeTool.MAX_PARALLEL_EXPORTS
);
}
/**
* Creates a new ExecuteCode tool instance.
*
@@ -79,6 +113,25 @@ export class ExportShapeTool extends Tool<ExportShapeArgs> {
}
protected async executeCore(args: ExportShapeArgs): Promise<ToolResponse> {
// bound concurrent exports in multi-user mode to keep peak server memory under control;
// in single-user mode (or when no limit is configured) the gate is irrelevant
// and the export runs directly
if (this.mcpServer.isMultiUserMode() && ExportShapeTool.parallelismSemaphore) {
return ExportShapeTool.parallelismSemaphore.withPermit(() => this.exportImage(args));
} else {
return this.exportImage(args);
}
}
/**
* Performs the actual image export: requests the image via the plugin and either
* returns it as a tool response or saves it to the requested file path. The bulk
* of the memory pressure (parsed plugin response, decoded image buffer, optional
* re-encoding via sharp) lives here, which is why executeCore gates the call.
*
* @param args - the validated tool arguments
*/
private async exportImage(args: ExportShapeArgs): Promise<ToolResponse> {
// check arguments
if (args.filePath) {
FileUtils.checkPathIsAbsolute(args.filePath);

View File

@@ -0,0 +1,69 @@
import { createLogger } from "../logger";
/**
* Counting semaphore for bounding the parallelism of asynchronous operations.
*
* Calls in excess of the configured maximum are queued in FIFO order and
* proceed once an earlier holder has released its permit.
*/
export class Semaphore {
private static readonly logger = createLogger("Semaphore");
private available: number;
private readonly waiters: Array<() => void> = [];
/**
* @param name - identifier used in log messages so the source of contention is recognisable
* @param maxConcurrent - the maximum number of permits that may be held simultaneously
*/
constructor(
private readonly name: string,
maxConcurrent: number
) {
if (maxConcurrent < 1) {
throw new Error(`maxConcurrent must be at least 1; got ${maxConcurrent}`);
}
this.available = maxConcurrent;
}
/**
* Acquires a permit, runs the given function, and releases the permit when it
* settles - including on rejection. Queues if no permit is currently available.
*
* @param fn - the function to run while holding the permit
* @returns the value returned by fn
*/
public async withPermit<T>(fn: () => Promise<T>): Promise<T> {
await this.acquire();
try {
return await fn();
} finally {
this.release();
}
}
private acquire(): Promise<void> {
if (this.available > 0) {
this.available--;
return Promise.resolve();
}
return new Promise<void>((resolve) => {
this.waiters.push(resolve);
Semaphore.logger.info(
"Semaphore '%s' saturated; request queued (%d waiting)",
this.name,
this.waiters.length
);
});
}
private release(): void {
const next = this.waiters.shift();
if (next !== undefined) {
// pass the permit directly to the next waiter without touching `available`
next();
} else {
this.available++;
}
}
}

293
mcp/pnpm-lock.yaml generated
View File

@@ -38,10 +38,10 @@ importers:
version: 5.9.3
vite:
specifier: ^7.0.8
version: 7.3.1(@types/node@20.19.30)
version: 7.3.1(@types/node@20.19.30)(tsx@4.22.3)
vite-live-preview:
specifier: ^0.3.2
version: 0.3.2(vite@7.3.1(@types/node@20.19.30))
version: 0.3.2(vite@7.3.1(@types/node@20.19.30)(tsx@4.22.3))
packages/server:
dependencies:
@@ -112,6 +112,9 @@ importers:
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@20.19.30)(typescript@5.9.3)
tsx:
specifier: ^4.22.3
version: 4.22.3
typescript:
specifier: ^5.0.0
version: 5.9.3
@@ -142,6 +145,12 @@ packages:
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.28.0':
resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.12':
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
engines: {node: '>=18'}
@@ -154,6 +163,12 @@ packages:
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.28.0':
resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.12':
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
engines: {node: '>=18'}
@@ -166,6 +181,12 @@ packages:
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.28.0':
resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.12':
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
engines: {node: '>=18'}
@@ -178,6 +199,12 @@ packages:
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.28.0':
resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.12':
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
engines: {node: '>=18'}
@@ -190,6 +217,12 @@ packages:
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.28.0':
resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.12':
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
engines: {node: '>=18'}
@@ -202,6 +235,12 @@ packages:
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.28.0':
resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.12':
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
engines: {node: '>=18'}
@@ -214,6 +253,12 @@ packages:
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.28.0':
resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.12':
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
engines: {node: '>=18'}
@@ -226,6 +271,12 @@ packages:
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.28.0':
resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.12':
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
engines: {node: '>=18'}
@@ -238,6 +289,12 @@ packages:
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.28.0':
resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.12':
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
engines: {node: '>=18'}
@@ -250,6 +307,12 @@ packages:
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.28.0':
resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.12':
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
engines: {node: '>=18'}
@@ -262,6 +325,12 @@ packages:
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.28.0':
resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.12':
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
engines: {node: '>=18'}
@@ -274,6 +343,12 @@ packages:
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.28.0':
resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.12':
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
engines: {node: '>=18'}
@@ -286,6 +361,12 @@ packages:
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.28.0':
resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.12':
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
engines: {node: '>=18'}
@@ -298,6 +379,12 @@ packages:
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.28.0':
resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.12':
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
engines: {node: '>=18'}
@@ -310,6 +397,12 @@ packages:
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.28.0':
resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.12':
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
engines: {node: '>=18'}
@@ -322,6 +415,12 @@ packages:
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.28.0':
resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.12':
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
engines: {node: '>=18'}
@@ -334,6 +433,12 @@ packages:
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.28.0':
resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.12':
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
engines: {node: '>=18'}
@@ -346,6 +451,12 @@ packages:
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-arm64@0.28.0':
resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.12':
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
engines: {node: '>=18'}
@@ -358,6 +469,12 @@ packages:
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.28.0':
resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.12':
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
engines: {node: '>=18'}
@@ -370,6 +487,12 @@ packages:
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-arm64@0.28.0':
resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.12':
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
engines: {node: '>=18'}
@@ -382,6 +505,12 @@ packages:
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.28.0':
resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.25.12':
resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
engines: {node: '>=18'}
@@ -394,6 +523,12 @@ packages:
cpu: [arm64]
os: [openharmony]
'@esbuild/openharmony-arm64@0.28.0':
resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.12':
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
engines: {node: '>=18'}
@@ -406,6 +541,12 @@ packages:
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.28.0':
resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.12':
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
engines: {node: '>=18'}
@@ -418,6 +559,12 @@ packages:
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.28.0':
resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.12':
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
engines: {node: '>=18'}
@@ -430,6 +577,12 @@ packages:
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.28.0':
resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.12':
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
engines: {node: '>=18'}
@@ -442,6 +595,12 @@ packages:
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.28.0':
resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@hono/node-server@1.19.9':
resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
engines: {node: '>=18.14.1'}
@@ -1035,6 +1194,11 @@ packages:
engines: {node: '>=18'}
hasBin: true
esbuild@0.28.0:
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -1485,6 +1649,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsx@4.22.3:
resolution: {integrity: sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==}
engines: {node: '>=18.0.0'}
hasBin: true
type-is@2.0.1:
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'}
@@ -1627,156 +1796,234 @@ snapshots:
'@esbuild/aix-ppc64@0.27.2':
optional: true
'@esbuild/aix-ppc64@0.28.0':
optional: true
'@esbuild/android-arm64@0.25.12':
optional: true
'@esbuild/android-arm64@0.27.2':
optional: true
'@esbuild/android-arm64@0.28.0':
optional: true
'@esbuild/android-arm@0.25.12':
optional: true
'@esbuild/android-arm@0.27.2':
optional: true
'@esbuild/android-arm@0.28.0':
optional: true
'@esbuild/android-x64@0.25.12':
optional: true
'@esbuild/android-x64@0.27.2':
optional: true
'@esbuild/android-x64@0.28.0':
optional: true
'@esbuild/darwin-arm64@0.25.12':
optional: true
'@esbuild/darwin-arm64@0.27.2':
optional: true
'@esbuild/darwin-arm64@0.28.0':
optional: true
'@esbuild/darwin-x64@0.25.12':
optional: true
'@esbuild/darwin-x64@0.27.2':
optional: true
'@esbuild/darwin-x64@0.28.0':
optional: true
'@esbuild/freebsd-arm64@0.25.12':
optional: true
'@esbuild/freebsd-arm64@0.27.2':
optional: true
'@esbuild/freebsd-arm64@0.28.0':
optional: true
'@esbuild/freebsd-x64@0.25.12':
optional: true
'@esbuild/freebsd-x64@0.27.2':
optional: true
'@esbuild/freebsd-x64@0.28.0':
optional: true
'@esbuild/linux-arm64@0.25.12':
optional: true
'@esbuild/linux-arm64@0.27.2':
optional: true
'@esbuild/linux-arm64@0.28.0':
optional: true
'@esbuild/linux-arm@0.25.12':
optional: true
'@esbuild/linux-arm@0.27.2':
optional: true
'@esbuild/linux-arm@0.28.0':
optional: true
'@esbuild/linux-ia32@0.25.12':
optional: true
'@esbuild/linux-ia32@0.27.2':
optional: true
'@esbuild/linux-ia32@0.28.0':
optional: true
'@esbuild/linux-loong64@0.25.12':
optional: true
'@esbuild/linux-loong64@0.27.2':
optional: true
'@esbuild/linux-loong64@0.28.0':
optional: true
'@esbuild/linux-mips64el@0.25.12':
optional: true
'@esbuild/linux-mips64el@0.27.2':
optional: true
'@esbuild/linux-mips64el@0.28.0':
optional: true
'@esbuild/linux-ppc64@0.25.12':
optional: true
'@esbuild/linux-ppc64@0.27.2':
optional: true
'@esbuild/linux-ppc64@0.28.0':
optional: true
'@esbuild/linux-riscv64@0.25.12':
optional: true
'@esbuild/linux-riscv64@0.27.2':
optional: true
'@esbuild/linux-riscv64@0.28.0':
optional: true
'@esbuild/linux-s390x@0.25.12':
optional: true
'@esbuild/linux-s390x@0.27.2':
optional: true
'@esbuild/linux-s390x@0.28.0':
optional: true
'@esbuild/linux-x64@0.25.12':
optional: true
'@esbuild/linux-x64@0.27.2':
optional: true
'@esbuild/linux-x64@0.28.0':
optional: true
'@esbuild/netbsd-arm64@0.25.12':
optional: true
'@esbuild/netbsd-arm64@0.27.2':
optional: true
'@esbuild/netbsd-arm64@0.28.0':
optional: true
'@esbuild/netbsd-x64@0.25.12':
optional: true
'@esbuild/netbsd-x64@0.27.2':
optional: true
'@esbuild/netbsd-x64@0.28.0':
optional: true
'@esbuild/openbsd-arm64@0.25.12':
optional: true
'@esbuild/openbsd-arm64@0.27.2':
optional: true
'@esbuild/openbsd-arm64@0.28.0':
optional: true
'@esbuild/openbsd-x64@0.25.12':
optional: true
'@esbuild/openbsd-x64@0.27.2':
optional: true
'@esbuild/openbsd-x64@0.28.0':
optional: true
'@esbuild/openharmony-arm64@0.25.12':
optional: true
'@esbuild/openharmony-arm64@0.27.2':
optional: true
'@esbuild/openharmony-arm64@0.28.0':
optional: true
'@esbuild/sunos-x64@0.25.12':
optional: true
'@esbuild/sunos-x64@0.27.2':
optional: true
'@esbuild/sunos-x64@0.28.0':
optional: true
'@esbuild/win32-arm64@0.25.12':
optional: true
'@esbuild/win32-arm64@0.27.2':
optional: true
'@esbuild/win32-arm64@0.28.0':
optional: true
'@esbuild/win32-ia32@0.25.12':
optional: true
'@esbuild/win32-ia32@0.27.2':
optional: true
'@esbuild/win32-ia32@0.28.0':
optional: true
'@esbuild/win32-x64@0.25.12':
optional: true
'@esbuild/win32-x64@0.27.2':
optional: true
'@esbuild/win32-x64@0.28.0':
optional: true
'@hono/node-server@1.19.9(hono@4.11.7)':
dependencies:
hono: 4.11.7
@@ -2288,6 +2535,35 @@ snapshots:
'@esbuild/win32-ia32': 0.27.2
'@esbuild/win32-x64': 0.27.2
esbuild@0.28.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.28.0
'@esbuild/android-arm': 0.28.0
'@esbuild/android-arm64': 0.28.0
'@esbuild/android-x64': 0.28.0
'@esbuild/darwin-arm64': 0.28.0
'@esbuild/darwin-x64': 0.28.0
'@esbuild/freebsd-arm64': 0.28.0
'@esbuild/freebsd-x64': 0.28.0
'@esbuild/linux-arm': 0.28.0
'@esbuild/linux-arm64': 0.28.0
'@esbuild/linux-ia32': 0.28.0
'@esbuild/linux-loong64': 0.28.0
'@esbuild/linux-mips64el': 0.28.0
'@esbuild/linux-ppc64': 0.28.0
'@esbuild/linux-riscv64': 0.28.0
'@esbuild/linux-s390x': 0.28.0
'@esbuild/linux-x64': 0.28.0
'@esbuild/netbsd-arm64': 0.28.0
'@esbuild/netbsd-x64': 0.28.0
'@esbuild/openbsd-arm64': 0.28.0
'@esbuild/openbsd-x64': 0.28.0
'@esbuild/openharmony-arm64': 0.28.0
'@esbuild/sunos-x64': 0.28.0
'@esbuild/win32-arm64': 0.28.0
'@esbuild/win32-ia32': 0.28.0
'@esbuild/win32-x64': 0.28.0
escalade@3.2.0: {}
escape-goat@4.0.0: {}
@@ -2796,6 +3072,12 @@ snapshots:
tslib@2.8.1: {}
tsx@4.22.3:
dependencies:
esbuild: 0.28.0
optionalDependencies:
fsevents: 2.3.3
type-is@2.0.1:
dependencies:
content-type: 1.0.5
@@ -2814,7 +3096,7 @@ snapshots:
vary@1.1.2: {}
vite-live-preview@0.3.2(vite@7.3.1(@types/node@20.19.30)):
vite-live-preview@0.3.2(vite@7.3.1(@types/node@20.19.30)(tsx@4.22.3)):
dependencies:
'@commander-js/extra-typings': 12.1.0(commander@12.1.0)
'@types/ansi-html': 0.0.0
@@ -2826,14 +3108,14 @@ snapshots:
debug: 4.4.3
escape-goat: 4.0.0
p-defer: 4.0.1
vite: 7.3.1(@types/node@20.19.30)
vite: 7.3.1(@types/node@20.19.30)(tsx@4.22.3)
ws: 8.19.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
vite@7.3.1(@types/node@20.19.30):
vite@7.3.1(@types/node@20.19.30)(tsx@4.22.3):
dependencies:
esbuild: 0.27.2
fdir: 6.5.0(picomatch@4.0.3)
@@ -2844,6 +3126,7 @@ snapshots:
optionalDependencies:
'@types/node': 20.19.30
fsevents: 2.3.3
tsx: 4.22.3
which@2.0.2:
dependencies: