[GH-ISSUE #1695] [Bug] inspect_analysis_view advertises selector combinations rejected by input validation #566

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

Originally created by @N0zoM1z0 on GitHub (Oct 11, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/1695

inspect_analysis_view advertises item selectors independently of the selected collection. Its exported draft-2020-12 input schema accepts combinations that the MCP server rejects immediately as invalid arguments. Agents and clients validating against tools/list cannot discover the collection-specific input constraints from the schema.

Reproduced on main 0ecf3f2438ec96a6b543942869808018e8d84452 (REA 6.4.0 source build), Linux x64, Node 24.18.0, the real stdio MCP transport with @modelcontextprotocol/client@2.3.1, and Ajv 8.20.0's draft-2020-12 validator. No native provider or model endpoint was involved; the JavaScript fixture was analyzed as text and never executed.

Reproduction

  1. Build REA with its pinned toolchain using npm ci and npm run build:cached.
  2. Connect a real MCP client to node scripts/rea.mjs mcp, then retrieve tools/list and compile inspect_analysis_view.inputSchema with a draft-2020-12 validator.
  3. Analyze a small JavaScript directory through analyze_javascript_application with detail: "summary". Keep its normalized_result.parent_evidence_id in the same session.
  4. Validate and send this request, replacing the placeholder with that retained Evidence ID:
{
  "name": "inspect_analysis_view",
  "arguments": {
    "source": {
      "kind": "retained-evidence",
      "evidence_id": "<parent_evidence_id>"
    },
    "view": {
      "kind": "item",
      "collection": "modules",
      "selector": { "index": 0 }
    }
  }
}

The real advertised schema validates successfully, and Ajv accepts the complete arguments. MCP then rejects them:

Input validation error: Invalid arguments for tool inspect_analysis_view:
view.selector: Module items are selected by exact path or node_id, not index or name.

The same contradiction was reproduced for all four representative combinations:

Collection Selector Advertised input schema Runtime input validation
modules index accepts rejects
modules name accepts rejects
sections path accepts rejects
symbols node_id accepts rejects

These failures happen during input validation, before interpreting whether the retained Evidence supports the requested collection. As a positive control, module paging succeeds, selecting a returned module by exact node_id succeeds with the same parent Evidence ID, and a subsequent ping succeeds. Owned client/server cleanup completes.

Cause and expected behavior

itemViewSchema in src/domain/analysisView/analysisView.ts uses one object with an independent collection enum and four-way selector union. A superRefine adds the collection-specific constraints at runtime; those constraints are absent after JSON Schema conversion.

Express the existing collection/selector alternatives in the owning contract so the converted schema preserves them: modules accept path/node_id, while sections and symbols accept index/name. Keep runtime validation and existing selection semantics. Verify the actual advertised schema rejects incompatible combinations and a real stdio workflow still pages/selects retained Evidence successfully.

Path ambiguity is a separate, expected runtime condition: multiple observed module identities can share a path, and the current error correctly directs callers to node_id. This report does not request index-based module selection or changes to Evidence identity.

Open/closed issues and PRs were searched. #1632 concerns Pi's upstream reference-budget handling, and #1364 concerned lost comparison refinements; neither tracks this collection-specific schema mismatch.

Originally created by @N0zoM1z0 on GitHub (Oct 11, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1695 `inspect_analysis_view` advertises item selectors independently of the selected collection. Its exported draft-2020-12 input schema accepts combinations that the MCP server rejects immediately as invalid arguments. Agents and clients validating against `tools/list` cannot discover the collection-specific input constraints from the schema. Reproduced on main `0ecf3f2438ec96a6b543942869808018e8d84452` (REA 6.4.0 source build), Linux x64, Node 24.18.0, the real stdio MCP transport with `@modelcontextprotocol/client@2.3.1`, and Ajv 8.20.0's draft-2020-12 validator. No native provider or model endpoint was involved; the JavaScript fixture was analyzed as text and never executed. ### Reproduction 1. Build REA with its pinned toolchain using `npm ci` and `npm run build:cached`. 2. Connect a real MCP client to `node scripts/rea.mjs mcp`, then retrieve `tools/list` and compile `inspect_analysis_view.inputSchema` with a draft-2020-12 validator. 3. Analyze a small JavaScript directory through `analyze_javascript_application` with `detail: "summary"`. Keep its `normalized_result.parent_evidence_id` in the same session. 4. Validate and send this request, replacing the placeholder with that retained Evidence ID: ```json { "name": "inspect_analysis_view", "arguments": { "source": { "kind": "retained-evidence", "evidence_id": "<parent_evidence_id>" }, "view": { "kind": "item", "collection": "modules", "selector": { "index": 0 } } } } ``` The real advertised schema validates successfully, and Ajv accepts the complete arguments. MCP then rejects them: ```text Input validation error: Invalid arguments for tool inspect_analysis_view: view.selector: Module items are selected by exact path or node_id, not index or name. ``` The same contradiction was reproduced for all four representative combinations: | Collection | Selector | Advertised input schema | Runtime input validation | | --- | --- | --- | --- | | `modules` | `index` | accepts | rejects | | `modules` | `name` | accepts | rejects | | `sections` | `path` | accepts | rejects | | `symbols` | `node_id` | accepts | rejects | These failures happen during input validation, before interpreting whether the retained Evidence supports the requested collection. As a positive control, module paging succeeds, selecting a returned module by exact `node_id` succeeds with the same parent Evidence ID, and a subsequent ping succeeds. Owned client/server cleanup completes. ### Cause and expected behavior [`itemViewSchema` in `src/domain/analysisView/analysisView.ts`](https://github.com/morluto/rea/blob/0ecf3f2438ec96a6b543942869808018e8d84452/src/domain/analysisView/analysisView.ts#L47) uses one object with an independent collection enum and four-way selector union. A `superRefine` adds the collection-specific constraints at runtime; those constraints are absent after JSON Schema conversion. Express the existing collection/selector alternatives in the owning contract so the converted schema preserves them: modules accept `path`/`node_id`, while sections and symbols accept `index`/`name`. Keep runtime validation and existing selection semantics. Verify the actual advertised schema rejects incompatible combinations and a real stdio workflow still pages/selects retained Evidence successfully. Path ambiguity is a separate, expected runtime condition: multiple observed module identities can share a path, and the current error correctly directs callers to `node_id`. This report does not request index-based module selection or changes to Evidence identity. Open/closed issues and PRs were searched. #1632 concerns Pi's upstream reference-budget handling, and #1364 concerned lost comparison refinements; neither tracks this collection-specific schema mismatch.
Author
Owner

@subratbarnwal commented on GitHub (Oct 11, 2026):

// ---------------------------------------------------------------
// DRAFT: assumes zod v4 (native z.toJSONSchema) and vitest.
// Adapt names/paths to src/domain/analysisView/analysisView.ts.
// ---------------------------------------------------------------
import { z } from "zod";

// ---- 1. Schema (src/domain/analysisView/analysisView.ts) ------

const nonEmpty = z.string().min(1);

// Modules: exact path or node_id only.
export const moduleSelectorSchema = z.union([
z.object({ path: nonEmpty }).strict(),
z.object({ node_id: nonEmpty }).strict(),
]);

// Sections and symbols: index or name only.
export const indexedSelectorSchema = z.union([
z.object({ index: z.number().int().nonnegative() }).strict(),
z.object({ name: nonEmpty }).strict(),
]);

// One branch per collection. collection is a literal in each branch,
// so the converted JSON Schema carries a const the validator can use.
export const itemViewSchema = z.discriminatedUnion("collection", [
z
.object({
kind: z.literal("item"),
collection: z.literal("modules"),
selector: moduleSelectorSchema,
})
.strict(),
z
.object({
kind: z.literal("item"),
collection: z.literal("sections"),
selector: indexedSelectorSchema,
})
.strict(),
z
.object({
kind: z.literal("item"),
collection: z.literal("symbols"),
selector: indexedSelectorSchema,
})
.strict(),
]);

// NOTE: if the top-level view is a discriminatedUnion on kind,
// itemViewSchema can no longer be one of its members (nested
// discriminated unions on different keys don't compose). Use
// z.union([...pageView, itemViewSchema]) there, or flatten.

// Keep runtime-only checks that JSON Schema can't express, e.g.
// path ambiguity ("multiple modules share this path; use node_id").
// Those stay in the handler, not the schema.

// ---- 2. Test (e.g. test/mcp/inspectAnalysisView.schema.test.ts) ---
/*
import Ajv2020 from "ajv/dist/2020";
import { describe, it, expect } from "vitest";
import { z } from "zod";
import { inspectAnalysisViewInputSchema } from "..."; // the tool's full input schema

const advertised = z.toJSONSchema(inspectAnalysisViewInputSchema, { target: "draft-2020-12" });
const validate = new Ajv2020({ strict: false }).compile(advertised);

const base = { source: { kind: "retained-evidence", evidence_id: "ev_123" } };
const item = (collection: string, selector: object) =>
({ ...base, view: { kind: "item", collection, selector } });

describe("inspect_analysis_view advertised schema", () => {
it.each([
["modules", { index: 0 }],
["modules", { name: "foo" }],
["sections", { path: "a/b.js" }],
["symbols", { node_id: "n1" }],
])("rejects %s + %j", (collection, selector) => {
expect(validate(item(collection, selector))).toBe(false);
});

it.each([
["modules", { path: "a/b.js" }],
["modules", { node_id: "n1" }],
["sections", { index: 0 }],
["sections", { name: "text" }],
["symbols", { index: 3 }],
["symbols", { name: "main" }],
])("accepts %s + %j", (collection, selector) => {
expect(validate(item(collection, selector))).toBe(true);
});

it("rejects mixed selector keys", () => {
expect(validate(item("modules", { path: "a", node_id: "n1" }))).toBe(false);
});

it("agrees with runtime validation on every case above", () => {
// For each case, assert safeParse().success === validate(...) so the
// advertised schema and runtime parser can't drift apart again.
});
});
*/

const moduleSelector = z.union([
z.object({ path: z.string() }).strict(),
z.object({ node_id: z.string() }).strict(),
]);
const indexedSelector = z.union([
z.object({ index: z.number().int().nonnegative() }).strict(),
z.object({ name: z.string() }).strict(),
]);

const itemViewSchema = z.discriminatedUnion("collection", [
z.object({ kind: z.literal("item"), collection: z.literal("modules"), selector: moduleSelector }),
z.object({ kind: z.literal("item"), collection: z.literal("sections"), selector: indexedSelector }),
z.object({ kind: z.literal("item"), collection: z.literal("symbols"), selector: indexedSelector }),
]);

<!-- gh-comment-id:6106327844 --> @subratbarnwal commented on GitHub (Oct 11, 2026): // --------------------------------------------------------------- // DRAFT: assumes zod v4 (native z.toJSONSchema) and vitest. // Adapt names/paths to src/domain/analysisView/analysisView.ts. // --------------------------------------------------------------- import { z } from "zod"; // ---- 1. Schema (src/domain/analysisView/analysisView.ts) ------ const nonEmpty = z.string().min(1); // Modules: exact path or node_id only. export const moduleSelectorSchema = z.union([ z.object({ path: nonEmpty }).strict(), z.object({ node_id: nonEmpty }).strict(), ]); // Sections and symbols: index or name only. export const indexedSelectorSchema = z.union([ z.object({ index: z.number().int().nonnegative() }).strict(), z.object({ name: nonEmpty }).strict(), ]); // One branch per collection. `collection` is a literal in each branch, // so the converted JSON Schema carries a `const` the validator can use. export const itemViewSchema = z.discriminatedUnion("collection", [ z .object({ kind: z.literal("item"), collection: z.literal("modules"), selector: moduleSelectorSchema, }) .strict(), z .object({ kind: z.literal("item"), collection: z.literal("sections"), selector: indexedSelectorSchema, }) .strict(), z .object({ kind: z.literal("item"), collection: z.literal("symbols"), selector: indexedSelectorSchema, }) .strict(), ]); // NOTE: if the top-level `view` is a discriminatedUnion on `kind`, // itemViewSchema can no longer be one of its members (nested // discriminated unions on different keys don't compose). Use // z.union([...pageView, itemViewSchema]) there, or flatten. // Keep runtime-only checks that JSON Schema can't express, e.g. // path ambiguity ("multiple modules share this path; use node_id"). // Those stay in the handler, not the schema. // ---- 2. Test (e.g. test/mcp/inspectAnalysisView.schema.test.ts) --- /* import Ajv2020 from "ajv/dist/2020"; import { describe, it, expect } from "vitest"; import { z } from "zod"; import { inspectAnalysisViewInputSchema } from "..."; // the tool's full input schema const advertised = z.toJSONSchema(inspectAnalysisViewInputSchema, { target: "draft-2020-12" }); const validate = new Ajv2020({ strict: false }).compile(advertised); const base = { source: { kind: "retained-evidence", evidence_id: "ev_123" } }; const item = (collection: string, selector: object) => ({ ...base, view: { kind: "item", collection, selector } }); describe("inspect_analysis_view advertised schema", () => { it.each([ ["modules", { index: 0 }], ["modules", { name: "foo" }], ["sections", { path: "a/b.js" }], ["symbols", { node_id: "n1" }], ])("rejects %s + %j", (collection, selector) => { expect(validate(item(collection, selector))).toBe(false); }); it.each([ ["modules", { path: "a/b.js" }], ["modules", { node_id: "n1" }], ["sections", { index: 0 }], ["sections", { name: "text" }], ["symbols", { index: 3 }], ["symbols", { name: "main" }], ])("accepts %s + %j", (collection, selector) => { expect(validate(item(collection, selector))).toBe(true); }); it("rejects mixed selector keys", () => { expect(validate(item("modules", { path: "a", node_id: "n1" }))).toBe(false); }); it("agrees with runtime validation on every case above", () => { // For each case, assert safeParse().success === validate(...) so the // advertised schema and runtime parser can't drift apart again. }); }); */ const moduleSelector = z.union([ z.object({ path: z.string() }).strict(), z.object({ node_id: z.string() }).strict(), ]); const indexedSelector = z.union([ z.object({ index: z.number().int().nonnegative() }).strict(), z.object({ name: z.string() }).strict(), ]); const itemViewSchema = z.discriminatedUnion("collection", [ z.object({ kind: z.literal("item"), collection: z.literal("modules"), selector: moduleSelector }), z.object({ kind: z.literal("item"), collection: z.literal("sections"), selector: indexedSelector }), z.object({ kind: z.literal("item"), collection: z.literal("symbols"), selector: indexedSelector }), ]);
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#566
No description provided.