Skip to content

Humanize jsonschema validation errors; validate tool inputs at tool boundary #28

@mgoldsborough

Description

@mgoldsborough

Problem

Auto-generated entity CRUD tools use a raw Tool subclass (_make_entity_tool in upjack/server.py) that bypasses FastMCP's Pydantic-based input validation. When an LLM passes bad arguments to create_<entity>, the call:

  1. Reaches the handler with no up-front validation
  2. The handler invokes app.create_entity, which calls validate_entity
  3. validate_entity raises jsonschema.ValidationError
  4. FastMCP's default stringification dumps the entire failing schema — including the inlined base entity, every nested property, every description — as the error message body

Observed in production (conv_30f049cdb75d464f on ws_mat, line 27): an LLM called create_board({"title": "Work"}) (using title instead of the required name). The error text came back as a ~5KB blob with the actual problem ("missing required field 'name'") buried at the top under the full schema dump. The agent recovered on retry, but it cost:

  • One wasted tool-call round-trip
  • ~15K tokens of re-sent conversation context
  • Noticeable user-visible latency

Per-call cost is small. Aggregate cost across every agent workflow that makes a type error is not.

Two-part fix

A. Validate inputs at the tool boundary

Before the handler runs in _make_entity_tool.run(), validate arguments against the tool's own declared JSON Schema. On failure, return a concise error:

create_board: missing required field 'name'. You passed: {title}.
Valid fields: name, columns, description, default_column.

No schema dump. ~100 bytes instead of 5KB. The LLM recovers on the retry in most cases.

B. Humanize downstream jsonschema.ValidationError

Direct callers of app.create_entity / app.update_entity (and any custom tools that hit those paths) still encounter the raw ValidationError. Wrap the stringification: extract err.validator, err.validator_value, err.path, err.instance, produce a one-line reason, keep the full blob as detail for debugging.

Expected impact

  • Error text drops from ~5KB → ~100-200 bytes
  • LLM retry success rate on field-typo errors should approach 100% (they were already ~95% on 5KB dumps, but at much higher token/latency cost)
  • Debug-ability preserved: detail field or structured error object retains the full context

Effort

~30 LOC in upjack/server.py::_make_entity_tool + a wrapper helper near validate_entity. Test coverage: add field-typo cases to the existing test_server.py::TestToolInputSchemas.

Optional follow-up

"Did you mean X?" suggestion on unknown-key errors (use difflib.get_close_matches). ~10 LOC extra. Nice polish, not required for the core fix.

Scope

Python SDK only for now. TypeScript SDK uses the MCP SDK directly (not FastMCP), and its validation story is different — worth a separate look but not blocking this.

Relation to other issues

Severity

Medium. Not blocking — LLMs recover — but the cost is measurable and paid on every field-typo error across every tool. Low-effort fix with broad upside.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions