Skip to content

fix(tools): add Zod discriminated union overrides for object creation and update tools - #145

Open
Hatgor wants to merge 1 commit into
anyproto:mainfrom
Hatgor:fix/object-tools-discriminated-union
Open

fix(tools): add Zod discriminated union overrides for object creation and update tools#145
Hatgor wants to merge 1 commit into
anyproto:mainfrom
Hatgor:fix/object-tools-discriminated-union

Conversation

@Hatgor

@Hatgor Hatgor commented Aug 21, 2026

Copy link
Copy Markdown

Description

This PR introduces handcrafted Zod schema overrides (ToolOverrides) for API-create-object and API-update-object to resolve polymorphic schema inference issues when generating MCP tools.

Problem

The Anytype OpenAPI specification for object properties (PropertyLinkWithValue) defines 11 polymorphic property formats (text, number, select, multi_select, date, files, checkbox, url, email, phone, objects). The automatic OpenAPI-to-MCP converter produced flattened/unconstrained schemas that caused validation issues and deserialization failures for LLM agents.


Real-World Reproduction: Why LLMs Fail Without Discriminated Unions

1. What the LLM Generates (Using Flat / Unconstrained Schema)

When the schema presents all 11 property fields as optional properties of a single object, LLM agents (such as ZeroClaw or other tool-calling agents) routinely populate all fields with default/empty placeholder values (text: "", number: 0, checkbox: false, date: "", files: [], multi_select: []):

{
  "space_id": "bafyreieofefmnne6lgxjgpu4k2jt4f2dzgriev7jnqkqsbayrt2gax67fu.12orhxm981fr7",
  "object_id": "bafyreic7bzn5iwqhjvh7qv43hp5uglid6icjlmbmqy62jd6uq3ibdz4bom",
  "type_key": "task",
  "name": "AnyType - add MCP",
  "markdown": "## Details...",
  "icon": {
    "emoji": "",
    "format": "emoji"
  },
  "properties": [
    {
      "key": "status",
      "select": "63454af7c493f68e301890dd",
      "checkbox": false,
      "date": "",
      "email": "",
      "files": [],
      "multi_select": [],
      "number": 0,
      "objects": [],
      "phone": "",
      "text": "",
      "url": ""
    }
  ]
}

2. Anytype API Error Response

The Anytype backend (anytype-heart) Go deserializer processes the empty string fields (like text: "" or empty select: "") alongside the target field, causing deserialization conflicts and failing with 400 Bad Request:

{
  "status": 400,
  "object": "error",
  "code": "bad_request",
  "message": "bad input: invalid select option for \"status\": "
}

3. Why Prompt Engineering Fails

Even when system prompts or explicit instructions ("never pass empty dummy fields, only pass the key and target field") are provided, LLMs repeatedly fall back to sending dummy values on subsequent turns because the model is guided primarily by the tool's JSON Schema. If the schema defines flat optional fields, the model's tool-calling tokenizer will continue to generate default values.


Solution

  1. Zod Discriminated Union: Handcrafted explicit schemas for all 11 Anytype property formats using z.discriminatedUnion("format", [...]) in src/tools/object-tools.ts. Each branch exposes only { key, format, <target_field> }.
  2. Tool Overrides Registry: Added a ToolOverrides mechanism injected into OpenAPIToMCPConverter and validated/sanitized in MCPProxy.
  3. Full Icon Support: Updated IconSchema to support emoji, file, named icon formats, and nullable (allowing clients to clear icons by passing null).
  4. LLM Compatibility: Stripped root $schema metadata from generated tool input schemas to ensure compatibility across MCP and function-calling clients.
  5. MCP Spec Compliance: Added isError: true flag to HttpClientError handling in MCPProxy so HTTP 4xx/5xx responses are properly flagged as tool call errors.
  6. Tool Name Normalization: Normalized lookup for tool names exceeding 64 characters so openApiLookup lookups succeed.
  7. Comprehensive Tests: Added unit tests covering all 11 property types, icon variations, update payloads, and error handling (113/113 tests passing).

Upstream OpenAPI Note

Ideally, this polymorphic union should be modeled with an explicit discriminator in the upstream Anytype OpenAPI specification itself (PropertyLinkWithValue). Once the OpenAPI specification is updated upstream with proper discriminated unions, these manual ToolOverrides can simply be retired and deleted without any breaking changes.

… and update tools

Add handcrafted Zod schema overrides (`ToolOverrides`) for `API-create-object`
and `API-update-object` to replace broken OpenAPI polymorphic schema inference
with an explicit `format`-discriminated union for object properties.

Key changes:
- Handcrafted Zod schemas with a discriminated union on `format` for all 11 Anytype
  property types (text, number, select, multi_select, date, files, checkbox, url, email, phone, objects)
- Registered `ToolOverrides` injected via `OpenAPIToMCPConverter` and validated in `MCPProxy`

Supporting improvements:
- Full `IconSchema` support (emoji, file, named icon, and nullable to clear icon)
- Strip root `$schema` metadata from tool input schemas for OpenAI / LLM compatibility
- Set `isError: true` on `HttpClientError` responses per MCP specification
- Normalize tool lookup map to handle >64 character name truncation
- Added comprehensive unit tests and regenerated `scripts/tools.json`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant