QUESTPIE

Limits and errors

Every tool call and resource read runs inside a budget for size, depth, time and concurrency. Every failure comes back as one of eight codes with a correlation id.

View markdown

An agent is a caller you do not control. So the boundary ships with limits already on, and execution in config/mcp.ts only narrows them.

LimitDefaultCeilingWhat it bounds
maxInputBytes64 KiB1 MiBThe decoded input of one call.
maxInputDepth1664Object and array nesting in that input.
maxOutputDepth6464Nesting in the result.
maxValueNodes10 000100 000Properties and entries visited per value.
maxOutputBytes1 MiB4 MiBThe serialized result of one call.
timeoutMs30 000300 000Authorization plus execution, per call.
maxConcurrency641 024In-flight calls for one app.
maxConcurrencyPerPrincipal81 024In-flight calls for one caller.
maxTools5122 048Tools in the released catalog.
maxResources5122 048Resources in the released catalog.
src/questpie/server/config/mcp.ts
import { mcpConfig } from "@questpie/mcp";

export default mcpConfig({
	execution: {
		timeoutMs: 10_000,
		maxConcurrencyPerPrincipal: 4,
		onDiagnostic(event) {
			logger.info(event);
		},
	},
});

A value above its ceiling throws Invalid MCP execution limit: <name> rather than being clamped. So does a maxConcurrencyPerPrincipal above maxConcurrency, and a value that is not a positive safe integer.

One budget per app, not per request

Every transport shares the same counters, including separate HTTP requests and separately built workload ports. A busy stdio session and a busy agent draw on the same maxConcurrency.

How saturation behaves

A call over the concurrency limit is rejected straight away. There is no queue and no wait, so a slow agent cannot pile up work behind itself.

A timed-out or cancelled call keeps its slot until the underlying work actually settles. So code that ignores signal cannot get around the limit. It only holds the slot for longer. Honour signal in custom tool handlers.

maxTools and maxResources are checked once, when the catalog is built. Cross either one and building it throws. A catalog the server cannot bound is never served at all.

Error codes

A failed call returns isError: true. The text says only MCP access denied or MCP operation failed. The real code sits in the result's _meta, under the key questpie/error, next to a correlation id.

CodeWhen
access_deniedA gate refused the call.
invalid_inputThe input failed its Zod schema.
input_too_largeThe input broke a byte, depth or node limit.
output_too_largeThe result broke one.
timeouttimeoutMs elapsed.
cancelledThe client aborted.
busyA concurrency limit was already full.
internalAnything else, including a thrown handler error.
{
	"isError": true,
	"content": [{ "type": "text", "text": "MCP operation failed" }],
	"_meta": {
		"questpie/error": { "code": "timeout", "correlationId": "b0f6…" }
	}
}

The caller never sees why

No database message, no stack, no input, no output, no credential and no authorization envelope crosses the boundary. Give the user the correlation id and match it in your own logs.

mcpPublicErrorCode(error) reads the code off an error thrown by the list and cancellation paths, where there is no result object to inspect.

import { mcpPublicErrorCode } from "@questpie/mcp";

try {
	await tools.listCustomTools({ signal });
} catch (error) {
	if (mcpPublicErrorCode(error) === "busy") await retryLater();
}

Diagnostics

execution.onDiagnostic is a server-side sink. It fires once per call, on both outcomes.

FieldNotes
correlationIdThe same id the caller received.
requestIdThe MCP request id, when there is one.
transport"http", "stdio" or "workload".
operationThe tool or resource name, truncated to 256 characters.
durationMsWall clock for the whole call.
outcome"completed" or "rejected".
codeThe public error code, on a rejection.
internalError{ kind: "Error" | "Unknown" }, on an internal code only.

The sink is deliberately narrow. Only one unresolved callback is kept at a time, and events that arrive while it is still pending are dropped rather than buffered. Do slow work elsewhere.

On this page