🐛 Raise and make configurable the MCP request body size limit (#12450)

The MCP endpoint parsed request bodies with express.json() and its
default 100kb limit, so large tool calls such as execute_code failed
with HTTP 413 (PayloadTooLargeError). Read the limit from the new
PENPOT_MCP_MAX_BODY_SIZE env var, defaulting to 10mb, and document it.

Closes #12080

Signed-off-by: Nimesh Kulkarni <167794819+GitNimay@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Nimesh 2026-11-03 20:51:28 +05:30 committed by GitHub
parent 029d9c0af7
commit 88cd0933bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 18 additions and 1 deletions

View File

@ -298,6 +298,7 @@ The Penpot MCP server can be configured using environment variables.
| `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 in local single-user mode. Set to `true` to enable. | `false` |
| `PENPOT_MCP_TOOL_TIMEOUT_S` | Timeout, in seconds, for tool calls dispatched to the Penpot plugin | `120` |
| `PENPOT_MCP_MAX_BODY_SIZE` | Maximum size of a JSON request body accepted by the MCP endpoint, in any format supported by Express (e.g. `10mb`) | `10mb` |
| `PENPOT_MCP_EXPORT_SHAPE_MAX_PARALLEL_REQUESTS` | Maximum number of parallel export shape requests (multi-user mode only). | `0` (no limit) |
| `PENPOT_MCP_AUTH_URI` | Public base URL of the MCP authorization service, e.g. `https://penpot.example/api/mcp/oauth`. Set to enable OAuth; requires `--multi-user`. Use HTTPS (HTTP is allowed on loopback for development), without credentials, query parameters, or a fragment. | (unset; OAuth disabled) |
| `PENPOT_MCP_REDIS_URI` | Redis connection URI (e.g. `redis://host:6379`) enabling multi-instance horizontal scaling via Redis pub/sub task routing (multi-user mode only). When unset, the server runs in single-instance mode, requiring the plugin and MCP client to connect to the same instance. | (unset) |

View File

@ -126,6 +126,13 @@ test("passes an explicit Penpot session ID from the tool call to plugin dispatch
assert.match(JSON.stringify(body), /chosen-tab/);
});
test("accepts tool calls with request bodies larger than 100 KiB", async (t) => {
t.mock.method(server.pluginBridge, "executePluginTask", async () => ({ data: "ok" }));
const code = `return "${"x".repeat(200_000)}".length;`;
const response = await modernRequest("tools/call", { name: "execute_code", arguments: { code } });
assert.equal(response.status, 200);
});
test("supports older Streamable HTTP clients without allocating a session", async () => {
const client = new Client({ name: "legacy-test", version: "1" });
const transport = new StreamableHTTPClientTransport(new URL(`${baseUrl}/mcp`));

View File

@ -147,6 +147,14 @@ export class PenpotMcpServer {
*/
private readonly tenant: string;
/**
* Maximum size of a JSON request body, read from the `PENPOT_MCP_MAX_BODY_SIZE` environment variable.
*
* Accepts any value supported by Express' `json` body parser (e.g. `"10mb"`). Defaults to `"10mb"`,
* since tool calls such as `execute_code` can carry large payloads.
*/
private readonly maxBodySize: string;
constructor(private isMultiUser: boolean = false) {
this.authentication = new McpAuthentication(process.env.PENPOT_MCP_AUTH_URI, isMultiUser);
// read port configuration from environment variables
@ -156,6 +164,7 @@ export class PenpotMcpServer {
this.replHost = process.env.PENPOT_MCP_REPL_HOST ?? "localhost";
this.replPort = parseInt(process.env.PENPOT_MCP_REPL_PORT ?? "4403", 10);
this.tenant = process.env.PENPOT_TENANT ?? "default";
this.maxBodySize = process.env.PENPOT_MCP_MAX_BODY_SIZE ?? "10mb";
const toolTimeoutSecs = parseInt(process.env.PENPOT_MCP_TOOL_TIMEOUT_S ?? "120", 10);
this.configLoader = new ConfigurationLoader(process.cwd());
@ -323,7 +332,7 @@ export class PenpotMcpServer {
async start(): Promise<void> {
const { default: express } = await import("express");
this.app = express();
this.app.use(express.json());
this.app.use(express.json({ limit: this.maxBodySize }));
this.setupHttpEndpoints();