koala73/worldmonitor · error · RpcValidationError
${label} HTTP 400
Error message
${label} HTTP 400 What it means
RpcValidationError is thrown by assertToolFetchOk in api/mcp/billing-denial.ts when a tool _execute gateway fetch returns HTTP 400 AND the response body parses as JSON containing a 'violations' array of safe {field, description} pairs. These violations are proto/sebuf ValidationError output: the MCP tool call's arguments failed server-side schema validation. The message keeps the '<label> HTTP 400' shape so dispatch's log-severity downgrade and mcpErrorFingerprint grouping still match, while the typed 'violations' array carries the actionable detail (max 8 entries, field regex ^[A-Za-z_][A-Za-z0-9_.]{0,63}$, description capped at 200 chars, HTML/credential-like text dropped).
Source
Thrown at api/mcp/billing-denial.ts:182
}
/**
* Standard non-ok handling for tool `_execute` gateway fetches: billing
* denials become typed errors dispatch can re-emit faithfully; proto 400
* bodies with safe field violations become RpcValidationError; everything
* else keeps the existing `<label> HTTP <status>` Error contract.
*
* HTTP 400 response bodies are consumed only to classify violations. Callers
* must await this helper — a forgotten await would let execution continue
* and treat the 400 as success.
*/
export async function assertToolFetchOk(response: ToolFetchResponse, label: string): Promise<void> {
if (response.ok) return;
throwIfBillingDenial(response, label);
if (response.status === 400) {
const violations = await extractSafeRpcViolations(response);
if (violations.length > 0) {
throw new RpcValidationError(label, violations);
}
}
throw new Error(`${label} HTTP ${response.status}`);
}
View on GitHub (pinned to eeab0a219f)
Solutions
- Read err.violations — each entry names the exact offending field and the server's description; fix those params in the tools/call arguments
- Re-fetch the tool's inputSchema via tools/list and align client param construction with the current schema
- If the schema looks correct on both sides, curl the underlying /api endpoint directly to inspect the raw 400 body and confirm the violations list
- If violations reference fields your client never sends, suspect proto drift: run make generate and redeploy/rebuild so registry and gateway agree
Example fix
// before
const res = await client.callTool('list-feed-digest', {
category: 'energy',
limit: 'ten', // wrong type: proto expects number
});
// throws RpcValidationError: [ { field: 'limit', description: 'expected int32' } ]
// after
const res = await client.callTool('list-feed-digest', {
category: 'energy',
limit: 10,
}); Defensive patterns
Strategy: validation
Validate before calling
// Validate params against the tool's declared inputSchema before tools/call
function validateAgainstSchema(params, schema) {
const errs = [];
for (const [name, prop] of Object.entries(schema.properties ?? {})) {
const v = params[name];
if (schema.required?.includes(name) && (v === undefined || v === null)) {
errs.push(`${name}: required`); continue;
}
if (v === undefined) continue;
if (prop.type === 'number' && (typeof v !== 'number' || !Number.isFinite(v))) errs.push(`${name}: expected number`);
if (prop.type === 'string' && typeof v !== 'string') errs.push(`${name}: expected string`);
if (prop.enum && !prop.enum.includes(v)) errs.push(`${name}: must be one of ${prop.enum.join('|')}`);
if (prop.type === 'integer' && !Number.isInteger(v)) errs.push(`${name}: expected integer`);
}
return errs;
}
const errs = validateAgainstSchema(args, toolSchema);
if (errs.length) throw new Error('Bad params: ' + errs.join('; ')); Type guard
function isRpcValidationError(e) {
return typeof e === 'object' && e !== null
&& (e.name === 'RpcValidationError' || Array.isArray(e.violations));
} Try / catch
try {
await client.callTool('list-feed-digest', args);
} catch (e) {
if (isRpcValidationError(e)) {
// e.violations: [{field, description}] — map to user-facing field errors
for (const v of e.violations) console.error(`${v.field}: ${v.description}`);
} else throw e;
} Prevention
- Fetch tools/list and generate/validate params from the live inputSchema instead of hardcoded shapes
- Re-fetch the schema after gateway deploys — proto changes are the usual source of new 400 violations
- Treat e.violations as the source of truth; never string-parse the message for field names
When it happens
Trigger: Calling any MCP tool whose params violate the generated proto schema: wrong type for a field (string where number expected), out-of-range limit, unknown enum value, or a missing required property. The gateway returns 400 with a JSON body {violations: [{field, description}...]} and extractSafeRpcViolations finds at least one sanitizable pair. Triggered from tools/call on paths that route through assertToolFetchOk (e.g. nlp-tools list-feed-digest with an invalid category/variant parameter shape).
Common situations: Client generated from an older tool inputSchema after the server proto evolved (new required field, retyped property). Hand-written tool calls with ad-hoc params. Local dev against a deployed gateway with a newer proto than the local registry. Bodies that are HTML or contain unsafe text fall through to the generic HTTP 400 error instead, so seeing RpcValidationError specifically means a real proto validation reply arrived.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
Related errors
- get_intel_timeline requires at least one of domain ("conflic
- get-china-decision-signals returned no canonical payload
- get-china-decision-signals returned an invalid canonical pay
- Seeded world brief unavailable (${result.reason})
- get-country-intel-brief HTTP ${res.status}${code ? `: ${code
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/aa8d3ef08491a681.
Report an issue: GitHub.