Skip to content

Commit 7ef2c04

Browse files
committed
fix: update changeset for graceful degradation, document boolean schema limitation
1 parent 55983bc commit 7ef2c04

3 files changed

Lines changed: 24 additions & 2 deletions

File tree

.changeset/inline-ref-in-tool-schema.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
'@modelcontextprotocol/core': patch
33
---
44

5-
Inline local `$ref` pointers in tool `inputSchema` so schemas are self-contained and LLM-consumable. LLMs cannot resolve JSON Schema `$ref` — they serialize referenced parameters as strings instead of objects. Recursive schemas now throw at `tools/list` time instead of silently degrading.
5+
Inline local `$ref` pointers in tool `inputSchema` so schemas are self-contained and LLM-consumable. LLMs cannot resolve JSON Schema `$ref` — they serialize referenced parameters as strings instead of objects. Recursive schemas are handled gracefully — cyclic `$ref` pointers are left in place with only their `$defs` entries preserved, while all non-cyclic refs are fully inlined.

packages/core/src/util/schema.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ export function dereferenceLocalRefs(schema: Record<string, unknown>): Record<st
101101
resolvedDefs.set(defName, resolved);
102102
}
103103

104-
// Merge sibling keywords onto the resolved definition
104+
// Merge sibling keywords onto the resolved definition.
105+
// Note: boolean JSON Schemas (true/false) skip this merge — siblings are dropped.
106+
// This is acceptable: the SDK's JsonSchemaType excludes boolean schemas by design,
107+
// and no schema library (Zod v4, ArkType, Valibot) produces boolean $defs entries.
105108
if (hasSiblings && resolved !== null && typeof resolved === 'object' && !Array.isArray(resolved)) {
106109
const resolvedSiblings = Object.fromEntries(Object.entries(siblings).map(([k, v]) => [k, inlineRefs(v, stack)]));
107110
return { ...(resolved as Record<string, unknown>), ...resolvedSiblings };

packages/core/test/schema.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,25 @@ describe('dereferenceLocalRefs', () => {
269269
});
270270
});
271271

272+
test('boolean $defs entry resolves without sibling merge (SDK excludes boolean schemas by design)', () => {
273+
// JSON Schema allows boolean schemas (true = accept all, false = reject all) in $defs.
274+
// When a $ref resolves to a boolean, sibling keywords (description, title, etc.) are
275+
// dropped because the merge guard requires an object. This test documents that behavior.
276+
// No schema library produces boolean $defs — Zod: z.any() → {}, z.never() → {not:{}},
277+
// and the SDK's JsonSchemaType explicitly excludes boolean schemas (validators/types.ts).
278+
const schema = {
279+
type: 'object',
280+
properties: {
281+
x: { $ref: '#/$defs/AlwaysValid', description: 'Any value' }
282+
},
283+
$defs: { AlwaysValid: true }
284+
};
285+
expect(dereferenceLocalRefs(schema)).toEqual({
286+
type: 'object',
287+
properties: { x: true }
288+
});
289+
});
290+
272291
test('$ref inlined from real $defs while properties named "$defs" and "definitions" are preserved', () => {
273292
const schema = {
274293
type: 'object',

0 commit comments

Comments
 (0)