[GH-ISSUE #1052] [Bug] Three MCP tool input schemas use a root anyOf and are rejected by strict function-schema clients #387

Closed
opened 2026-10-15 18:24:33 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @LeekJay on GitHub (Oct 8, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/1052

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

MCP or agent integration

Observed behavior

Three tool input schemas are rejected by providers that enforce OpenAI-style function schema validation. The provider aborts the whole request, so no REA tool is usable.

Observed with Codex against rea-agents@5.0.0:

{"error":{"message":"Invalid schema for function 'mcp__rea::project_managed_application_graph': schema must be a JSON Schema of 'type: \"object\"', got 'type: null'.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}

tools/list returns 133 tools. Three of them carry a top-level anyOf next to type: "object":

project_managed_application_graph | type=object | hasProperties=false | hasAnyOf=true | keys=type,$schema,anyOf,$defs,examples
inspect_web_page                  | type=object | hasProperties=false | hasAnyOf=true | keys=type,$schema,anyOf,examples
inspect_electron_page             | type=object | hasProperties=false | hasAnyOf=true | keys=type,$schema,anyOf,examples

All three Zod contracts are unions:

  • inspectWebPageInputSchema = z.union([...]) in dist/domain/browserObservation.js
  • inspect_electron_page uses the same union shape
  • managedApplicationGraphReferenceInputSchema is a union of the same form

A single invalid tool fails the entire request, so one bad schema makes the whole server unusable rather than degrading that tool alone.

Reproduction

  1. Install rea-agents@5.0.0.
  2. Register the stdio server with a provider that enforces OpenAI function schema validation (Codex was used here).
  3. Start a session so the client calls tools/list and sends the tool schemas.
  4. Observe the request rejected with got 'type: null'.

Without a client, the same condition is visible directly:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
  | npx -y rea-agents@5.0.0 mcp

Inspect the inputSchema of the three tools named above.

Expected behavior

Every tool that is advertised over MCP should project to a schema the provider accepts: an object schema whose property constraints live under properties. Providers that reject a root anyOf should not be sent one, or the tool should be omitted with an explicit limitation rather than failing the entire tool set.

If a root union cannot be represented for a given provider, a typed, per-tool degradation would be preferable to an all-or-nothing request failure.

Impact and scope

This blocks REA entirely for clients that validate function schemas strictly, including Codex. It affects MCP integration only.

Not affected: CLI usage, the MCP stdio server itself, and server-side Zod validation of tool inputs. The agent never gets far enough to call anything, so no analysis behavior, Evidence integrity, or process isolation is involved.

REA version

5.0.0 (rea --version reports 5.0.0); also present on current main.

Runtime and platform

Node.js 24.21.0; Windows 11 x64; Codex client with OpenAI-style function schema validation; no analysis provider required to reproduce.

Target and artifact context

Not applicable. Reproduces at tools/list, before any target is opened.

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

Root-level shape returned by tools/list for project_managed_application_graph:

{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "anyOf": [
    {
      "type": "object",
      "properties": {
        "managed_artifact": { "...": "..." }
      }
    }
  ],
  "$defs": { "...": "..." },
  "examples": [ { "...": "..." } ]
}

Note that properties is absent at the root. A reader that requires object-with-properties at the root resolves the type as null.

The same shape appears for inspect_web_page and inspect_electron_page.

Typed outcome or recovery expected

Preferred: the projected input schema always carries type: "object" and root-level properties, with union branches flattened into that property set.

Acceptable alternative: the affected tools are withheld from tools/list with a recorded limitation, so the remaining tools still work and the client never receives an unrepresentable schema.

Analysis or suspected cause

The projection is produced in dist/contracts/toolSchemaMetadata.js:

Object.defineProperty(schema, "~standard", {
    value: {
        ...standard,
        jsonSchema: {
            ...standard.jsonSchema,
            input: (options) => ({
                ...describeProperties(inputJsonSchema(options)),
                examples: contract.examples.map(({ input }) => input),
            }),
        },
    },
});

describeProperties only walks properties. It does not resolve a root anyOf, so the union and its $defs/examples are passed through unchanged. dist/catalogIdentity.js then adds type: "object" in front of the same schema for the catalog, which is how a schema with anyOf and no root properties acquires type: "object".

Everything below is a hypothesis, not a verified cause: providers appear to resolve a schema that has a root anyOf without root properties as having no object type, and they reject the request instead of dropping the unrepresentable tool.

Additional context

A local patch that flattens the root union in the projection resolves it. Server-side Zod validation is untouched; the parser still enforces the union. Added at the end of dist/contracts/toolSchemaMetadata.js and applied inside the input projection:

// Providers reject a top-level `anyOf` alongside `type: "object"`.
// Zod emits `anyOf` for union inputs, so flatten the branches into a single
// object schema. Server-side Zod validation is unchanged.
const flattenRootUnion = (value) => {
    if (!isObject(value) || !Array.isArray(value.anyOf))
        return value;
    const properties = isObject(value.properties) ? { ...value.properties } : {};
    for (const branch of value.anyOf) {
        if (!isObject(branch) || !isObject(branch.properties))
            continue;
        for (const [property, propertySchema] of Object.entries(branch.properties)) {
            if (!(property in properties))
                properties[property] = propertySchema;
        }
    }
    const { anyOf, ...rest } = value;
    void anyOf;
    return { ...rest, type: "object", properties };
};
input: (options) => flattenRootUnion({
    ...describeProperties(inputJsonSchema(options)),
    examples: contract.examples.map(({ input }) => input),
}),

Measured before and after, with npx -y rea-agents@5.0.0 mcp:

before: tools=133, invalid top-level schemas=3
after:  tools=133, invalid top-level schemas=0

Resulting property sets:

project_managed_application_graph  type=object  properties=managed_artifact,managed_members,managed_native_boundaries
inspect_web_page                   type=object  properties=cdp_endpoint,allowed_origins,target_id,observation_ms,include_accessibility_text,...
inspect_electron_page              type=object  properties=cdp_endpoint,target_id,observation_ms,include_script_sources

This patch merges branch properties at the top level, so it does not preserve "exactly one branch" semantics for a reader that only sees the schema. A schema-level fix that survives provider constraints while keeping the union constraint, for example oneOf with a resolved discriminator or a single merged object with additionalProperties: false and per-branch required guidance, would be better if the provider accepts it.

A regression test that asserts type === "object" and non-empty root properties for every entry in tools/list would catch this class of defect for any future union input.

Originally created by @LeekJay on GitHub (Oct 8, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1052 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area MCP or agent integration ### Observed behavior Three tool input schemas are rejected by providers that enforce OpenAI-style function schema validation. The provider aborts the whole request, so no REA tool is usable. Observed with Codex against `rea-agents@5.0.0`: ```text {"error":{"message":"Invalid schema for function 'mcp__rea::project_managed_application_graph': schema must be a JSON Schema of 'type: \"object\"', got 'type: null'.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}} ``` `tools/list` returns 133 tools. Three of them carry a top-level `anyOf` next to `type: "object"`: ```text project_managed_application_graph | type=object | hasProperties=false | hasAnyOf=true | keys=type,$schema,anyOf,$defs,examples inspect_web_page | type=object | hasProperties=false | hasAnyOf=true | keys=type,$schema,anyOf,examples inspect_electron_page | type=object | hasProperties=false | hasAnyOf=true | keys=type,$schema,anyOf,examples ``` All three Zod contracts are unions: - `inspectWebPageInputSchema = z.union([...])` in `dist/domain/browserObservation.js` - `inspect_electron_page` uses the same union shape - `managedApplicationGraphReferenceInputSchema` is a union of the same form A single invalid tool fails the entire request, so one bad schema makes the whole server unusable rather than degrading that tool alone. ### Reproduction 1. Install `rea-agents@5.0.0`. 2. Register the stdio server with a provider that enforces OpenAI function schema validation (Codex was used here). 3. Start a session so the client calls `tools/list` and sends the tool schemas. 4. Observe the request rejected with `got 'type: null'`. Without a client, the same condition is visible directly: ```bash printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \ | npx -y rea-agents@5.0.0 mcp ``` Inspect the `inputSchema` of the three tools named above. ### Expected behavior Every tool that is advertised over MCP should project to a schema the provider accepts: an object schema whose property constraints live under `properties`. Providers that reject a root `anyOf` should not be sent one, or the tool should be omitted with an explicit limitation rather than failing the entire tool set. If a root union cannot be represented for a given provider, a typed, per-tool degradation would be preferable to an all-or-nothing request failure. ### Impact and scope This blocks REA entirely for clients that validate function schemas strictly, including Codex. It affects MCP integration only. Not affected: CLI usage, the MCP stdio server itself, and server-side Zod validation of tool inputs. The agent never gets far enough to call anything, so no analysis behavior, Evidence integrity, or process isolation is involved. ### REA version 5.0.0 (`rea --version` reports `5.0.0`); also present on current `main`. ### Runtime and platform Node.js 24.21.0; Windows 11 x64; Codex client with OpenAI-style function schema validation; no analysis provider required to reproduce. ### Target and artifact context Not applicable. Reproduces at `tools/list`, before any target is opened. ### Interface MCP ### Earliest failure stage Transport or result projection ### Relevant output or evidence Root-level shape returned by `tools/list` for `project_managed_application_graph`: ```json { "type": "object", "$schema": "https://json-schema.org/draft/2020-12/schema", "anyOf": [ { "type": "object", "properties": { "managed_artifact": { "...": "..." } } } ], "$defs": { "...": "..." }, "examples": [ { "...": "..." } ] } ``` Note that `properties` is absent at the root. A reader that requires object-with-properties at the root resolves the type as null. The same shape appears for `inspect_web_page` and `inspect_electron_page`. ### Typed outcome or recovery expected Preferred: the projected input schema always carries `type: "object"` and root-level `properties`, with union branches flattened into that property set. Acceptable alternative: the affected tools are withheld from `tools/list` with a recorded limitation, so the remaining tools still work and the client never receives an unrepresentable schema. ### Analysis or suspected cause The projection is produced in `dist/contracts/toolSchemaMetadata.js`: ```js Object.defineProperty(schema, "~standard", { value: { ...standard, jsonSchema: { ...standard.jsonSchema, input: (options) => ({ ...describeProperties(inputJsonSchema(options)), examples: contract.examples.map(({ input }) => input), }), }, }, }); ``` `describeProperties` only walks `properties`. It does not resolve a root `anyOf`, so the union and its `$defs`/`examples` are passed through unchanged. `dist/catalogIdentity.js` then adds `type: "object"` in front of the same schema for the catalog, which is how a schema with `anyOf` and no root `properties` acquires `type: "object"`. Everything below is a hypothesis, not a verified cause: providers appear to resolve a schema that has a root `anyOf` without root `properties` as having no object type, and they reject the request instead of dropping the unrepresentable tool. ### Additional context A local patch that flattens the root union in the projection resolves it. Server-side Zod validation is untouched; the parser still enforces the union. Added at the end of `dist/contracts/toolSchemaMetadata.js` and applied inside the `input` projection: ```js // Providers reject a top-level `anyOf` alongside `type: "object"`. // Zod emits `anyOf` for union inputs, so flatten the branches into a single // object schema. Server-side Zod validation is unchanged. const flattenRootUnion = (value) => { if (!isObject(value) || !Array.isArray(value.anyOf)) return value; const properties = isObject(value.properties) ? { ...value.properties } : {}; for (const branch of value.anyOf) { if (!isObject(branch) || !isObject(branch.properties)) continue; for (const [property, propertySchema] of Object.entries(branch.properties)) { if (!(property in properties)) properties[property] = propertySchema; } } const { anyOf, ...rest } = value; void anyOf; return { ...rest, type: "object", properties }; }; ``` ```js input: (options) => flattenRootUnion({ ...describeProperties(inputJsonSchema(options)), examples: contract.examples.map(({ input }) => input), }), ``` Measured before and after, with `npx -y rea-agents@5.0.0 mcp`: ```text before: tools=133, invalid top-level schemas=3 after: tools=133, invalid top-level schemas=0 ``` Resulting property sets: ```text project_managed_application_graph type=object properties=managed_artifact,managed_members,managed_native_boundaries inspect_web_page type=object properties=cdp_endpoint,allowed_origins,target_id,observation_ms,include_accessibility_text,... inspect_electron_page type=object properties=cdp_endpoint,target_id,observation_ms,include_script_sources ``` This patch merges branch properties at the top level, so it does not preserve "exactly one branch" semantics for a reader that only sees the schema. A schema-level fix that survives provider constraints while keeping the union constraint, for example `oneOf` with a resolved discriminator or a single merged object with `additionalProperties: false` and per-branch `required` guidance, would be better if the provider accepts it. A regression test that asserts `type === "object"` and non-empty root `properties` for every entry in `tools/list` would catch this class of defect for any future union input.
bsakaguchi 2026-10-15 18:24:33 +00:00
Author
Owner

@cevieiras commented on GitHub (Oct 8, 2026):

I opened PR #1062 to address this.

The MCP projection now flattens the affected root unions into root-level object properties, so strict clients receive an object schema. The canonical Zod parser remains unchanged and still enforces the full input rules when a tool is invoked. For project_managed_application_graph, the advertised schema cannot express “at least one source” without a root union; the server-side parser continues to enforce it.

Verification: npm run build:cached, the focused MCP schema test (7 passed), and npm run check:fast all passed.

<!-- gh-comment-id:6057104065 --> @cevieiras commented on GitHub (Oct 8, 2026): I opened [PR #1062](https://github.com/morluto/rea/pull/1062) to address this. The MCP projection now flattens the affected root unions into root-level object properties, so strict clients receive an object schema. The canonical Zod parser remains unchanged and still enforces the full input rules when a tool is invoked. For `project_managed_application_graph`, the advertised schema cannot express “at least one source” without a root union; the server-side parser continues to enforce it. Verification: `npm run build:cached`, the focused MCP schema test (7 passed), and `npm run check:fast` all passed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#387
No description provided.