From ee6098bf1fd923ad8677fc0e47f163e4c4855f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Hanu=C5=A1?= Date: Fri, 16 Oct 2026 11:41:31 +0200 Subject: [PATCH] fix: Accept request bodies up to 9 MB in the dev server express.json() used its 100 KB default, so a larger MCP request got HTTP 413 before reaching any tool. Raise the limit to the Apify API's own payload limit (MAX_PAYLOAD_SIZE_BYTES in @apify/consts, 9 MiB). A larger body could not reach the API anyway. Fixes #1610. --- src/const.ts | 3 +++ src/dev_server.ts | 4 +++- tests/unit/dev_server.test.ts | 43 +++++++++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/const.ts b/src/const.ts index 7c23eba19..c291f8a53 100644 --- a/src/const.ts +++ b/src/const.ts @@ -43,6 +43,9 @@ export const TIP_MESSAGE_LIMIT = 300; /** When `false`, `resolveServerMode('auto', ...)` forces {@link SERVER_MODE.DEFAULT} regardless of client capabilities. */ export const SERVER_MODE_AUTO_DETECTION_ENABLED = true; +/** Matches the Apify API's payload limit (`MAX_PAYLOAD_SIZE_BYTES` in `@apify/consts`): no larger body can reach it. */ +export const MAX_REQUEST_BODY_BYTES = 9 * 1024 * 1024; + export const SERVER_NAME = 'apify-mcp-server'; export const SERVER_TITLE = 'Apify MCP Server'; export const HELPER_TOOLS = { diff --git a/src/dev_server.ts b/src/dev_server.ts index dd5e62904..f188444f1 100644 --- a/src/dev_server.ts +++ b/src/dev_server.ts @@ -32,6 +32,7 @@ import log from '@apify/log'; import { parseBooleanOrNull } from '@apify/utilities'; import { ApifyClient } from './apify_client.js'; +import { MAX_REQUEST_BODY_BYTES } from './const.js'; import { buildMcpClientContext } from './mcp/client_context.js'; import { ActorsMcpServer } from './mcp/server.js'; import { createStatelessServer } from './mcp/stateless_server.js'; @@ -249,7 +250,8 @@ export function createExpressApp(): express.Express { if (validateHost(req, res) && validateOrigin(req, res)) next(); }); - app.use(express.json()); + // Express's 100 KB default is too small for tool calls that carry files. See apify/apify-mcp-server#1610. + app.use(express.json({ limit: MAX_REQUEST_BODY_BYTES })); app.post('/', async (req: Request, res: Response) => { log.info('Received MCP request:', req.body); try { diff --git a/tests/unit/dev_server.test.ts b/tests/unit/dev_server.test.ts index 34a905fe4..a7cb8988c 100644 --- a/tests/unit/dev_server.test.ts +++ b/tests/unit/dev_server.test.ts @@ -11,7 +11,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } import log from '@apify/log'; -import { HELPER_TOOLS } from '../../src/const.js'; +import { HELPER_TOOLS, MAX_REQUEST_BODY_BYTES } from '../../src/const.js'; import { createExpressApp, extractInitializeMessage, @@ -20,7 +20,12 @@ import { } from '../../src/dev_server.js'; import type * as ToolsLoaderModule from '../../src/utils/tools_loader.js'; import { getActors } from '../../src/utils/tools_loader.js'; -import { makeArgsRecorderTool, readJsonRpcPayload, STATELESS_PROTOCOL_VERSION } from './helpers/mcp_server.js'; +import { + makeArgsRecorderTool, + makeRecorderTool, + readJsonRpcPayload, + STATELESS_PROTOCOL_VERSION, +} from './helpers/mcp_server.js'; vi.mock('../../src/utils/tools_loader.js', async (importOriginal) => { const actual = await importOriginal(); @@ -435,4 +440,38 @@ describe('createExpressApp() era routing', () => { expect(getActorsMock).not.toHaveBeenCalled(); }); }); + + it('accepts a 4 MiB tool call body, the size of a 3 MiB version sent as base64', async () => { + const { tool, received } = makeRecorderTool('source-tool'); + getActorsMock.mockResolvedValue([tool]); + + await withDevServer(async (post) => { + const { body, headers } = statelessRequest('tools/call', { + name: tool.name, + arguments: { content: 'x'.repeat(4 * 1024 * 1024) }, + }); + + const response = await post(DEV_URL, body, { ...headers, authorization: 'Bearer dev-token' }); + + expect(response.status).toBe(200); + expect(received.called).toBe(true); + }); + }); + + it('rejects a body over MAX_REQUEST_BODY_BYTES with 413', async () => { + const { tool, received } = makeRecorderTool('source-tool'); + getActorsMock.mockResolvedValue([tool]); + + await withDevServer(async (post) => { + const { body, headers } = statelessRequest('tools/call', { + name: tool.name, + arguments: { content: 'x'.repeat(MAX_REQUEST_BODY_BYTES) }, + }); + + const response = await post(DEV_URL, body, { ...headers, authorization: 'Bearer dev-token' }); + + expect(response.status).toBe(413); + expect(received.called).toBe(false); + }); + }); });