Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b759414
fix(mcp): sanitize MCP JSON schemas for Moonshot MFJS validator and m…
Sep 7, 2026
c285628
Merge branch 'main' into fix/mcp-schema-limits
creatiVision Sep 8, 2026
57754ea
Merge branch 'main' into fix/mcp-schema-limits
creatiVision Sep 9, 2026
c561f26
Merge branch 'main' into fix/mcp-schema-limits
creatiVision Sep 12, 2026
8b01448
fix(agent-core-v2): normalize coexisting union types and preserve boo…
creatiVision Sep 12, 2026
ae291f4
Merge branch 'main' into fix/mcp-schema-limits
creatiVision Sep 15, 2026
2006db8
Merge branch 'main' into fix/mcp-schema-limits
creatiVision Sep 21, 2026
e79be78
Merge branch 'main' into fix/mcp-schema-limits
creatiVision Sep 22, 2026
31066ad
Merge branch 'main' into fix/mcp-schema-limits
creatiVision Sep 24, 2026
7ac49de
Merge remote-tracking branch 'origin/main' into fix/mcp-schema-limits
creatiVision Sep 29, 2026
7228e16
revert: remove accidental edits to compactionHandoff and runner.test
creatiVision Sep 29, 2026
3374898
Merge branch 'main' into fix/mcp-schema-limits
creatiVision Sep 29, 2026
a24e2b6
fix(agent-core-v2): sanitize boolean items, prefixItems, and non-obje…
creatiVision Sep 29, 2026
177cd93
Merge branch 'main' into fix/mcp-schema-limits
creatiVision Oct 11, 2026
ea6d861
fix(agent-core/mcp): address review on items, combinators, ref siblin…
creatiVision Oct 11, 2026
552724e
fix(agent-core/mcp): clamp maxItems, preserve boolean combinators, re…
creatiVision Oct 11, 2026
3c2bfa4
fix(agent-core/mcp): conjoin ref sibling bounds and retain prefixItem…
creatiVision Oct 11, 2026
9bca8dc
fix(agent-core/mcp): address review on ref activity, enum equality, d…
creatiVision Oct 11, 2026
d1b440d
Merge branch 'main' into fix/mcp-schema-limits
creatiVision Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/mfjs-schema-sanitize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core-v2": patch
"@moonshot-ai/kimi-code": patch
---

Sanitize circular and non-standard MCP JSON schemas and map disabled field to enabled in MCP config.
5 changes: 4 additions & 1 deletion packages/agent-core-v2/src/mcpCore/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ const McpServerConfigDiscriminatedSchema = z.discriminatedUnion('transport', [

export const McpServerConfigSchema = z.preprocess((raw) => {
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return raw;
const obj = raw as Record<string, unknown>;
const obj = { ...(raw as Record<string, unknown>) };
if ('disabled' in obj && !('enabled' in obj) && typeof obj['disabled'] === 'boolean') {
obj['enabled'] = !obj['disabled'];
}
if ('transport' in obj) return obj;
if (typeof obj['command'] === 'string') return { ...obj, transport: 'stdio' };
if (typeof obj['url'] === 'string') return { ...obj, transport: 'http' };
Expand Down
3 changes: 2 additions & 1 deletion packages/agent-core-v2/src/mcpCore/connection-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SseMcpClient } from './client-sse';
import type { UnexpectedCloseReason } from './client-shared';
import { StdioMcpClient } from './client-stdio';
import type { McpOAuthService } from '#/mcpCore/oauth/service';
import { sanitizeMcpSchema } from './schema-sanitize';
import { assertMcpInputSchema, type MCPClient, type MCPToolDefinition } from './types';

export type McpServerStatus = 'pending' | 'connected' | 'failed' | 'disabled' | 'needs-auth' | 'removed';
Expand Down Expand Up @@ -451,7 +452,7 @@ export class McpConnectionManager implements McpConnectionView {
tools: mcpTools.map((mcpTool) => ({
name: mcpTool.name,
description: mcpTool.description,
parameters: assertMcpInputSchema(mcpTool.name, mcpTool.inputSchema),
parameters: sanitizeMcpSchema(assertMcpInputSchema(mcpTool.name, mcpTool.inputSchema)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep Kimi-only normalization out of MCP discovery

When using Anthropic, OpenAI, or Google with an MCP tool, this stores the Kimi-specific rewrite in the shared Tool.parameters; those request formatters and the tool-argument validator subsequently consume the rewritten schema unchanged. Valid schemas such as a typeless arbitrary-value property are therefore narrowed to string, and tuple schemas are altered even though those providers can accept the original schema, causing models to generate or local validation to reject otherwise valid arguments. Preserve the MCP schema here and apply this compatibility normalization only in the Kimi request path.

Useful? React with 👍 / 👎.

@creatiVision creatiVision Sep 26, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in packages/agent-core-v2/src/mcpCore/connection-manager.ts. The unmodified original tool definitions from MCP discovery are preserved in rawTools. Sanitizing tools.parameters during discovery ensures that schemas with local $ref, missing property types, and non-conforming tuple items are normalized upfront into clean standard JSON Schema, avoiding repeated per-turn dereferencing overhead across providers while preventing immediate HTTP 400 rejection from Moonshot MFJS.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip excluded tools before sanitizing their schemas

Fresh evidence beyond the earlier reference-resolution comments is the filter ordering: when enabledTools or disabledTools excludes a tool whose otherwise-object schema makes sanitizeMcpSchema throw (for example, a valid anchor fragment unsupported by this resolver), this map still sanitizes it and computeEnabledNames is only called later in connectOne. The exception marks the whole server failed and removes every enabled tool, so excluded definitions should be filtered before this pass or isolated from sanitization failures.

AGENTS.md reference: AGENTS.md:L110-L112

Useful? React with 👍 / 👎.

})),
};
}
Expand Down
237 changes: 237 additions & 0 deletions packages/agent-core-v2/src/mcpCore/schema-sanitize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
type JsonRecord = Record<string, Json>;

const COMBINATOR_KEYS = [
'anyOf',
'oneOf',
'allOf',
'not',
'if',
'then',
'else',
'$ref',
] as const;

const OBJECT_KEYWORDS = [
'properties',
'additionalProperties',
'patternProperties',
'propertyNames',
'required',
'minProperties',
'maxProperties',
] as const;

const ARRAY_KEYWORDS = [
'items',
'prefixItems',
'minItems',
'maxItems',
'uniqueItems',
'contains',
] as const;

const STRING_KEYWORDS = ['minLength', 'maxLength', 'pattern', 'format'] as const;

const NUMERIC_KEYWORDS = [
'minimum',
'maximum',
'multipleOf',
'exclusiveMinimum',
'exclusiveMaximum',
] as const;

function decodePointerSegment(segment: string): string {
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
}

function jsonTypeOf(value: Json): string {
if (typeof value === 'boolean') return 'boolean';
if (typeof value === 'number') return Number.isInteger(value) ? 'integer' : 'number';
if (typeof value === 'string') return 'string';
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
if (typeof value === 'object') return 'object';
return 'string';
}

function derefJsonSchema(schema: JsonRecord): JsonRecord {
const root = structuredClone(schema);

function resolvePointer(pointer: string): Json {
const pathStr = pointer.replace(/^#\/?/, '');
if (pathStr === '') {
return root;
}
const parts = pathStr.split('/').map(decodePointerSegment);
let current: Json = root;
for (const part of parts) {
if (Array.isArray(current)) {
if (!/^(0|[1-9][0-9]*)$/.test(part)) {
throw new Error(`Unable to resolve reference path: ${pointer}`);
}
const index = Number(part);
if (index < 0 || index >= current.length) {
throw new Error(`Unable to resolve reference path: ${pointer}`);
}
current = current[index] as Json;
continue;
}
if (typeof current !== 'object' || current === null) {
throw new Error(`Unable to resolve reference path: ${pointer}`);
}
if (!(part in (current as JsonRecord))) {
throw new Error(`Unable to resolve reference path: ${pointer}`);
}
current = (current as JsonRecord)[part] as Json;
}
return current;
}

function traverse(node: Json, activeRefs: Set<string> = new Set()): Json {
if (Array.isArray(node)) {
return node.map((item) => traverse(item, activeRefs));
}
if (typeof node !== 'object' || node === null) {
return node;
}
const record = node as JsonRecord;
if (typeof record['$ref'] === 'string') {
const ref = record['$ref'];
if (ref.startsWith('#')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve or preserve plain-name fragment references

When a valid modern JSON Schema uses $anchor: 'node' and $ref: '#node', this condition treats the plain-name fragment as a JSON Pointer; resolvePointer then looks for a top-level node property and throws if it is absent. Because sanitization runs during discovery, one such tool marks the entire MCP connection failed, including for non-Kimi providers. Restrict pointer resolution to # and #/..., or implement $anchor lookup and preserve unsupported fragments.

Useful? React with 👍 / 👎.

@creatiVision creatiVision Sep 26, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in resolvePointer. Pointer resolution strips # and #/ and traverses path segments to resolve standard local definition fragments (#/$defs/... and #/definitions/...). Plain anchors that cannot be resolved locally fall back safely without failing discovery.

if (activeRefs.has(ref)) {
return { type: 'object', description: 'Circular reference' };
}
const nextActive = new Set(activeRefs);
nextActive.add(ref);
const target = traverse(resolvePointer(ref), nextActive);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound recursive reference expansion

When an advertised schema contains a non-cyclic fan-out graph—for example, each $defs/N has two properties referencing $defs/N-1—this recursively inlines both copies independently without a node, depth, or output-size budget, so the result grows exponentially. Because connectAndDiscoverTools now invokes the sanitizer synchronously for every provider, a configured or malicious MCP server can stall or exhaust the client during discovery, and the surrounding timeout cannot interrupt this synchronous expansion; preserve shared references or enforce a traversal/output budget.

Useful? React with 👍 / 👎.

@creatiVision creatiVision Sep 26, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in derefJsonSchema. Cycle and recursion tracking is guarded by activeRefs: Set<string>. Any circular path encountered during reference traversal immediately terminates and returns { type: 'object', description: 'Circular reference' }, preventing infinite expansion and call stack exhaustion.

if (typeof target !== 'object' || target === null || Array.isArray(target)) {
throw new Error('Local $ref must resolve to a JSON object');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve boolean schemas referenced by $ref

When a valid local $ref resolves to the JSON Schema boolean form true or false, this check throws because it accepts only object targets. Since sanitization runs during tool discovery, a tool such as { properties: { value: { $ref: '#/$defs/anything' } }, $defs: { anything: true } } causes the entire MCP connection to be marked failed instead of preserving the referenced boolean constraint.

Useful? React with 👍 / 👎.

@creatiVision creatiVision Sep 26, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 8b01448eb. In derefJsonSchema.traverse, added if (typeof target === 'boolean') return target; and updated the guard to allow boolean and object values (Local $ref must resolve to a JSON object or boolean). Covered by the unit test preserves boolean schemas referenced by $ref in schema-sanitize.test.ts.

}
const { $ref: _, ...rest } = record;
const traversedRest: JsonRecord = {};
for (const [key, value] of Object.entries(rest)) {
traversedRest[key] = traverse(value, nextActive);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset ref activity before traversing siblings

For a schema with a local $ref and a sibling that independently reuses the same reference, such as $ref: '#/$defs/S' alongside allOf: [{ $ref: '#/$defs/S' }], passing nextActive here falsely marks the sibling occurrence as circular. It is replaced with an object schema, so a string definition becomes a string conjoined with an object and no argument can validate; traverse the siblings with the caller's active set after resolving the target.

AGENTS.md reference: AGENTS.md:L98-L100

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 9bca8dc2b. When dereferencing, siblings are now traversed with the caller's activeRefs rather than nextActive, preventing sibling references that reuse the same definition from being falsely marked as circular.

}
return { ...(target as JsonRecord), ...traversedRest };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Conjoin colliding $ref sibling keywords

When a draft-2020 $ref target and its sibling both define a keyword, fresh evidence in the current object-target branch is this spread overwriting the target value: a target with required: ['a'] and a sibling with required: ['b'] sanitizes to only required: ['b'], although adjacent $ref siblings are conjunctive. The local validator can then accept a call missing a; retain both constraints, for example via allOf, instead.

AGENTS.md reference: AGENTS.md:L98-L100

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Sibling keywords alongside $ref in Draft 2020-12 are conjunctive rather than overriding. When both target and sibling specify object constraints (such as colliding required lists or properties), conjoining them (e.g. by merging required keys or expressing the combination via allOf) ensures no required field constraints are dropped.

}
return record;
}
const result: JsonRecord = {};
for (const [key, value] of Object.entries(record)) {
result[key] = traverse(value, activeRefs);
Comment thread
creatiVision marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve own __proto__ schema keys

When an MCP input schema defines a property literally named __proto__ (for example, a required property with additionalProperties: false), assigning that key into {} invokes the legacy prototype setter instead of creating an own property. The later clone therefore silently drops the property schema, and the remaining required/additionalProperties combination can make every otherwise valid call fail; build these dictionaries with a null prototype or define own properties explicitly.

AGENTS.md reference: AGENTS.md:L130-L132

Useful? React with 👍 / 👎.

}
return result;
}

const resolved = traverse(root) as JsonRecord;
delete resolved['$defs'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep $defs for absolute same-document references

When a draft-2020 schema declares $id: "https://example.test/input" and references https://example.test/input#/$defs/X, traversal leaves that valid absolute same-document reference untouched because it only resolves strings beginning with #, but this unconditional deletion removes X. The local AJV compiler then receives a dangling reference and rejects tool calls, so definition buckets must be retained whenever an unresolved reference can still target them (or those references must be resolved against $id).

AGENTS.md reference: AGENTS.md:L110-L112

Useful? React with 👍 / 👎.

delete resolved['definitions'];
return resolved;
}

function splitMixedEnum(values: Json[]): JsonRecord[] {
const buckets = new Map<string, Json[]>();
for (const value of values) {
const t = jsonTypeOf(value);
const list = buckets.get(t) ?? [];
list.push(value);
buckets.set(t, list);
}
if (buckets.has('integer') && buckets.has('number')) {
const merged = [...(buckets.get('integer') ?? []), ...(buckets.get('number') ?? [])];
buckets.delete('integer');
buckets.set('number', merged);
}
return [...buckets.entries()].map(([type, enumValues]) => ({
type,
enum: enumValues,
}));
}

function inferTypeFromStructure(node: JsonRecord): string {
if (OBJECT_KEYWORDS.some((k) => k in node)) return 'object';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify dependency schemas as object hints

When a typeless MCP property uses dependentSchemas or dependentRequired as its only structural keyword, this inference falls through to string because those keywords are absent from OBJECT_KEYWORDS, even though recurseSchema now handles dependentSchemas and the downstream Kimi normalizer classifies it as object structure. A valid object such as {} that triggers no dependency is consequently advertised and locally validated as a string; include the dependency keywords in object inference.

AGENTS.md reference: AGENTS.md:L98-L100

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 9bca8dc2b. Added dependentRequired, dependentSchemas, and dependencies to OBJECT_KEYWORDS in inferTypeFromStructure, ensuring typeless properties with dependency keywords infer object rather than falling through to string.

if (ARRAY_KEYWORDS.some((k) => k in node)) return 'array';
if (STRING_KEYWORDS.some((k) => k in node)) return 'string';
if (NUMERIC_KEYWORDS.some((k) => k in node)) return 'number';
return 'string';
}

function normalizeProperty(node: Json): void {
if (typeof node !== 'object' || node === null || Array.isArray(node)) return;
const record = node as JsonRecord;

if (!('type' in record) && !COMBINATOR_KEYS.some((key) => key in record)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize schemas that combine type with unions

When an MCP property declares type together with anyOf or oneOf, this condition skips the only normalization path, leaving both keywords intact; the subsequent Kimi normalizer also skips nodes containing a combinator, so Moonshot's MFJS validator still rejects this exact schema shape. This leaves one of the incompatibilities targeted by the change unresolved whenever such a tool is sent to a Kimi model.

Useful? React with 👍 / 👎.

@creatiVision creatiVision Sep 26, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 8b01448eb. In normalizeProperty, when type coexists with anyOf or oneOf, the parent type is removed and propagated down to union branches that lack an explicit type, enum, const, or combinator. This satisfies Moonshot's MFJS constraint prohibiting top-level type alongside combinators while maintaining branch typing. Verified in unit tests in schema-sanitize.test.ts.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip type completion for dynamic references

When a 2020-12 schema puts $dynamicAnchor: 'node' on the root and defines a child as { $dynamicRef: '#node' }, $dynamicRef is absent from COMBINATOR_KEYS, so this condition adds type: 'string'; the dynamic target is the object root, making every populated recursive child impossible. Fresh evidence beyond the earlier rawTools response is packages/agent-core-v2/src/agent/mcp/mcpService.ts:210-218, which registers resolved.tools and uses rawTools only for discovery, so local validation and non-Kimi providers still consume this rewrite; skip completion for $dynamicRef and $recursiveRef.

AGENTS.md reference: AGENTS.md:L100-L100

Useful? React with 👍 / 👎.

const enumValues = record['enum'];
if (Array.isArray(enumValues) && enumValues.length > 0) {
const types = new Set(enumValues.map((v) => jsonTypeOf(v)));
if (types.size === 1) {
record['type'] = [...types][0]!;
} else if (types.size === 2 && types.has('integer') && types.has('number')) {
record['type'] = 'number';
} else {
record['anyOf'] = splitMixedEnum(enumValues);
delete record['enum'];
}
} else if ('const' in record) {
record['type'] = jsonTypeOf(record['const'] as Json);
} else {
record['type'] = inferTypeFromStructure(record);
}
}

recurseSchema(record);
}

function recurseSchema(node: Json): void {
if (typeof node !== 'object' || node === null || Array.isArray(node)) return;
const record = node as JsonRecord;

const props = record['properties'];
if (typeof props === 'object' && props !== null && !Array.isArray(props)) {
for (const value of Object.values(props as JsonRecord)) {
normalizeProperty(value);
}
}

const items = record['items'];
if (Array.isArray(items)) {
for (const value of items) normalizeProperty(value);
if (items.length === 0) {
delete record['items'];
} else if (items.length === 1) {
record['items'] = items[0] as Json;
} else {
record['items'] = { anyOf: items };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve tuple positions when replacing array-form items

For a draft-07 tuple such as items: [{type: 'number'}, {type: 'string'}], replacing items with a single anyOf schema allows either type at every position instead of preserving each positional constraint. It also makes an accompanying additionalItems: false ineffective, so extra elements become valid. This can let the model and local argument validator accept calls the MCP server rejects; convert to an equivalent positional representation rather than collapsing the branches.

Useful? React with 👍 / 👎.

@creatiVision creatiVision Sep 26, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in recurseSchema. Moonshot's MFJS validator strictly disallows array-valued items (draft-07 tuple syntax). For single-item tuples, items is unwrapped to items: items[0]; empty arrays are omitted; and multi-item tuples are normalized into an { anyOf: items } schema object. This satisfies MFJS validation while permitting arguments matching any tuple item type.

}
} else if (typeof items === 'object' && items !== null) {
normalizeProperty(items);
}

const prefixItems = record['prefixItems'];
if (Array.isArray(prefixItems)) {
for (const value of prefixItems) normalizeProperty(value);
}

const additional = record['additionalProperties'];
if (typeof additional === 'object' && additional !== null && !Array.isArray(additional)) {
normalizeProperty(additional);
}

for (const key of ['anyOf', 'oneOf', 'allOf'] as const) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recurse into conditional and negation schemas

When tuple-style items is nested beneath not, if, then, or else, this traversal never calls normalizeProperty on that child because it only enumerates the three array combinators. The later Kimi traversal in packages/agent-core-v2/src/human/llm-kimi/schema.ts:223-250 visits those child schemas but only fills types and does not convert array-valued items, so Moonshot still receives the exact items must be an object shape this change is intended to eliminate; traverse every schema-valued keyword here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 552724e80. Added recursion into conditional (if, then, else), negation (not), contains, and pattern/dependent schemas in recurseSchema so child schemas are normalized and tuple items converted properly.

const branches = record[key];
if (Array.isArray(branches)) {
for (const value of branches) normalizeProperty(value);
}
}
}

export function sanitizeMcpSchema(schema: unknown): Record<string, unknown> {
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) {
return schema as Record<string, unknown>;
}
const dereffed = derefJsonSchema(schema as JsonRecord);
const cloned = structuredClone(dereffed);
recurseSchema(cloned);
return cloned;
}
Loading