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

  1. Read err.violations — each entry names the exact offending field and the server's description; fix those params in the tools/call arguments
  2. Re-fetch the tool's inputSchema via tools/list and align client param construction with the current schema
  3. 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
  4. 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

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.

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/aa8d3ef08491a681. Report an issue: GitHub.