Repository navigation
Conversation
collectBody() buffered the whole request before JSON.parse and schema
validation, so a single request could make the HTTP server hold an
arbitrarily large body in memory. The collections arrays on the MCP
query and metadata tools and on REST /query, /search and /metadata had
no length limit either.
Cap request bodies at 1 MiB on /mcp, /query, /search and /metadata.
The limit is counted per chunk while the body streams in, so a chunked
upload with no Content-Length is bounded too; an over-limit
Content-Length is rejected before reading. Oversized bodies get a 413:
plain JSON on REST, a JSON-RPC parse error (-32700) on /mcp.
Read with req.iterator({ destroyOnReturn: false }) and drain the rest
of an oversized body. Throwing out of the default iterator destroys the
request; in a standalone probe on Node 24 and 26 a client still
uploading then got EPIPE instead of the 413 in roughly a fifth to two
fifths of 1-8 MB uploads, while the drained form delivered the 413 in
every run on Node 24, Node 26 and Bun 1.4.4. The race does not
reproduce under vitest, so the chunked test guards the per-chunk limit,
not the race.
Allow at most 1000 collections per request. Over-limit lists get a 400
on REST and an input validation error on MCP, and a non-array REST
/query collections value now gets a 400 instead of silently falling
back to the default collections, matching /metadata.
rymalia
marked this pull request as ready for review
October 26, 2026 01:59
This was referenced Oct 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
collectBody()insrc/mcp/server.tsbuffers the entire request body beforeJSON.parseand schema validation, with no size limit:As a result, one request to
qmd mcp --httpcan make the server hold an arbitrarily large body in memory, on every route that reads a body (/mcp,/query,/searchand/metadata). Thecollectionsarrays on the MCPqueryandmetadatatools and on the REST endpoints have no length limit either.This is also where the memory advisories that
npm auditreports for the pinned dependencies would be triggered:@modelcontextprotocol/server2.0.0 (memory exhaustion during request validation) andzod4.2.1 (memory exhaustion on oversized constrained collections). #1085 bumps the SDK, and zod is pinned on purpose (#382). Bounding the input in qmd itself limits the impact of both, whatever the dependency versions.As far as I can tell, the SDK's other advisory, one HTTP client's messages delivered to another, doesn't apply:
createMcpHandler(() => createMcpServer(store))builds a fresh server and transport for each request.Changes
collectBody()call site.Content-Lengthis bounded too.Content-Lengthearly-out: a declared length over the limit is rejected before anything is read.{"error": "Request body too large"}on REST, and a JSON-RPC parse error on/mcp(-32700,id: null), the same code as for unparseable bodies.collectBody()now reads withreq.iterator({ destroyOnReturn: false })and drains the rest of the body after responding.EPIPEinstead of the 413 in roughly 20–40% of 1–8 MB uploads, on Node 24.22.1 and 26.12.1.Connection: closeinstead of draining: producedEPIPE/ECONNRESETat every size..max()on thequeryandmetadataschemas, so over-limit lists get an input validation error./query,/searchand/metadata, which return a 400{"error": "Invalid field: collections (maximum 1000 items)"}./querynow returns a 400 for a non-arraycollectionsvalue, where it used to fall back to the default collections silently. This matches what/metadataalready does.[Unreleased]→ Fixed.REST
searchesis deliberately left alone, because #1002 already validates it withsubSearchSchema(min 1, max 10).Tests
All in
test/mcp.test.ts:/query,/metadataand/mcp, with the exact response bodies.Content-Length: gets the 413 on/queryand/mcp. This is the only test that reaches the per-chunk counting, sincefetch()always sendsContent-Lengthfor a string body./queryand/metadata, and an input validation error on MCPqueryandmetadata.I checked that the tests fail without the change: removing the per-chunk check fails the chunked test, and removing the
/metadata413 handling fails the fixed-length test.Known limits:
Verification
npx tsc --noEmit -p tsconfig.json: cleannpx oxlint: 0 diagnosticsnpx vitest run test/mcp.test.ts(Node 24.22.1): 76 passedbun test --timeout 60000 --preload ./src/test-preload.ts test/mcp.test.ts(Bun 1.4.4): 76 passednpx vitest run test/, Node 24.22.1): 1617 passed, 1 skippedchunkStrategyon MCP and REST). The two merge cleanly in either order, and with both mergedtest/mcp.test.tspasses 78/78.src/mcp/server.tsortest/mcp.test.ts(fix(mcp): reject malformed REST /query searches with 400 (#998) #1002, fix(mcp): stream SSE responses from the HTTP adapter #1036, feat(mcp): opt-in compact response mode (QMD_MCP_COMPACT=1 / --compact) #1047, Export QMD MCP server factory #1057, feat(mcp): report document and embedding counts in HTTP health #1059); checked withgit merge-tree. I placed the new code and tests in spots none of them change, so either merge order works.doctor vector sampling … 32 MiB SQLite budgetandlegacy fingerprint adoption … 16 MiB SQLite budget, both assertinglimitBindsunder Bun on Linux).mainpassed that job on Bun 1.4.3; both PRs ran on Bun 1.4.4, because the workflow installsbun-version: latest. Neither PR touches the store or doctor code.