fix(mcp): express inspect_analysis_view selector constraints in the contract schema (#1705)

The item view advertised its four selector alternatives independently of the
selected collection, while collection-specific constraints lived only in a
superRefine that does not survive JSON Schema conversion. Clients validating
against tools/list could not discover that modules accept only path/node_id
and sections/symbols accept only index/name.

Replace the refined object with a discriminated union on collection so the
converted draft-2020-12 schema rejects the same combinations the MCP server
rejects. Runtime selection semantics are unchanged.

Fixes #1695

Co-authored-by: Saim Shafique <sx4imm@gmail.com>
This commit is contained in:
Saim ShafiqueandSaim Shafique authored and GitHub committed 2026-10-11 19:55:16 +08:00
1 parent cdd9fe7167
commit a8a9c4d817
2 files changed
+124 -37

No files matched your search

+35 -37
View File
@@ -30,44 +30,42 @@ const facetViewSchema = z.strictObject({
kind: z.literal("facet"),
facet: z.enum(["mitigations", "linkage"]),
});
const itemSelectorSchema = z.union([
z.strictObject({ index: z.number().int().nonnegative() }),
z.strictObject({ name: z.string() }),
z.strictObject({
path: z
.string()
.describe(
"Exact observed module path or original source-map reference from a module page; an empty reference is valid.",
),
}),
z.strictObject({ node_id: prefixedDigestSchema("jag_node") }),
const indexSelectorSchema = z.strictObject({
index: z.number().int().nonnegative(),
});
const nameSelectorSchema = z.strictObject({ name: z.string() });
const pathSelectorSchema = z.strictObject({
path: z
.string()
.describe(
"Exact observed module path or original source-map reference from a module page; an empty reference is valid.",
),
});
const nodeIdSelectorSchema = z.strictObject({
node_id: prefixedDigestSchema("jag_node"),
});
const moduleItemViewSchema = z.strictObject({
kind: z.literal("item"),
collection: z.literal("modules"),
selector: z.union([pathSelectorSchema, nodeIdSelectorSchema]),
});
const sectionOrSymbolItemViewSchema = z.strictObject({
kind: z.literal("item"),
collection: z.enum(["sections", "symbols"]),
selector: z.union([indexSelectorSchema, nameSelectorSchema]),
});
/**
* The collection-specific selector alternatives are expressed in the owning
* contract so the advertised JSON Schema preserves them: modules accept
* path/node_id, sections and symbols accept index/name. Refinement-only
* constraints do not survive JSON Schema conversion, so the advertised
* schema previously accepted combinations that runtime input validation
* rejects.
*/
const itemViewSchema = z.discriminatedUnion("collection", [
moduleItemViewSchema,
sectionOrSymbolItemViewSchema,
]);
const itemViewSchema = z
.strictObject({
kind: z.literal("item"),
collection: z.enum(["sections", "symbols", "modules"]),
selector: itemSelectorSchema,
})
.superRefine((value, context) => {
const selector = value.selector;
if (value.collection === "modules") {
if (!("path" in selector) && !("node_id" in selector))
context.addIssue({
code: "custom",
path: ["selector"],
message:
"Module items are selected by exact path or node_id, not index or name.",
});
return;
}
if (!("index" in selector) && !("name" in selector))
context.addIssue({
code: "custom",
path: ["selector"],
message:
"Section and symbol items are selected by index or exact name, not path or node_id.",
});
});
const pageViewSchema = z.strictObject({
kind: z.literal("page"),
collection: z.enum(["sections", "symbols", "modules"]),