Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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/fix-xcode-mcp-schema-type-mismatch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/kosong": patch
"@moonshot-ai/kimi-code": patch
---

Repair mismatched JSON Schema types emitted by Xcode 26.5 MCP server for Moonshot compatibility.
55 changes: 55 additions & 0 deletions packages/kosong/src/providers/kimi-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,11 +309,66 @@ function normalizeProperty(node: unknown): void {
} else {
node['type'] = inferTypeFromStructure(node);
}
} else if (!hasAnyKey(node, TYPE_COMPLETION_SKIP_KEYS) && typeof node['type'] === 'string') {
// Some MCP servers emit schemas where a $ref merge or a generator bug
// leaves an explicit type that contradicts the enum/const values (e.g.
// type: 'object' alongside string enum values). Moonshot rejects these
// as invalid, so repair the type when it disagrees with the values.
//
// Known trigger: Xcode MCP (xcrun mcpbridge) starting with
// Version 26.5 (17F42) generates this bug for String-backed Swift enums.
const enumValues = node['enum'];
if (Array.isArray(enumValues) && enumValues.length > 0) {
try {
const inferred = inferTypeFromValues(enumValues);
if (node['type'] !== inferred) {
// eslint-disable-next-line no-console
console.warn(
`[kimi-schema] repaired mismatched type: changed "${String(node['type'])}" to "${inferred}" for enum ${JSON.stringify(enumValues.slice(0, 3))}${enumValues.length > 3 ? '...' : ''}`,
);
Comment thread
youngxhui marked this conversation as resolved.
node['type'] = inferred;
removeIrrelevantStructureKeys(node, inferred);
}
} catch {
// Mixed or uninferable enum types — leave the explicit type as-is
// and let the provider validator surface the error.
}
} else if (hasOwn(node, 'const')) {
try {
const inferred = inferTypeFromValues([node['const']]);
if (node['type'] !== inferred) {
// eslint-disable-next-line no-console
console.warn(
`[kimi-schema] repaired mismatched type: changed "${String(node['type'])}" to "${inferred}" for const ${JSON.stringify(node['const'])}`,
);
node['type'] = inferred;
removeIrrelevantStructureKeys(node, inferred);
}
} catch {
// Same as above.
}
}
}

recurseSchema(node);
}

function removeIrrelevantStructureKeys(
node: Record<string, unknown>,
newType: JsonSchemaType,
): void {
if (newType !== 'object') {
for (const key of OBJECT_STRUCTURE_KEYS) {
delete node[key];
}
}
if (newType !== 'array') {
for (const key of ARRAY_STRUCTURE_KEYS) {
delete node[key];
}
}
}

function inferTypeFromStructure(schema: Record<string, unknown>): JsonSchemaType {
if (hasAnyKey(schema, OBJECT_STRUCTURE_KEYS)) {
return 'object';
Expand Down
15 changes: 15 additions & 0 deletions packages/kosong/src/providers/kimi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,21 @@ export class KimiChatProvider implements ChatProvider {
)) as unknown as OpenAI.Chat.ChatCompletion | AsyncIterable<OpenAI.Chat.ChatCompletionChunk>;
return new KimiStreamedMessage(response, this._stream);
} catch (error: unknown) {
const apiError = error as { status?: number; message?: string };
if (apiError.status === 400 && typeof apiError.message === 'string') {
if (
apiError.message.includes('tools.function.parameters') ||
apiError.message.includes('json schema')
) {
const toolNames = (createParams['tools'] as Array<{ function?: { name?: string } }>)
?.map((t) => t.function?.name)
.filter((n): n is string => typeof n === 'string');
// eslint-disable-next-line no-console
console.error(
`[KimiChatProvider] 400 error with tools schema. tools: [${toolNames?.join(', ') ?? 'unknown'}]`,
);
}
}
throw convertOpenAIError(error);
}
}
Expand Down
70 changes: 70 additions & 0 deletions packages/kosong/test/providers/kimi-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,76 @@ describe('normalizeKimiToolSchema', () => {
});
});

it('repairs mismatched explicit type when enum values contradict it', () => {
// Regression: Xcode MCP (xcrun mcpbridge) Version 26.5 (17F42) and later
// generates schemas where String-backed Swift enums incorrectly carry
// type: 'object' alongside string enum values. We overwrite the contradictory
// type and strip object/array structure keys that are no longer relevant.
const schema = {
type: 'object',
properties: {
operation: {
type: 'object',
enum: ['move', 'copy'],
properties: {
rawValue: { type: 'string' },
},
required: ['rawValue'],
},
},
};

const result = normalizeKimiToolSchema(schema);

expect(result).toEqual({
type: 'object',
properties: {
operation: {
type: 'string',
enum: ['move', 'copy'],
},
},
});
});

it('repairs mismatched explicit type when const value contradicts it', () => {
const schema = {
type: 'object',
properties: {
mode: { type: 'object', const: 'fast' },
},
};

const result = normalizeKimiToolSchema(schema);

expect(result).toEqual({
type: 'object',
properties: {
mode: { type: 'string', const: 'fast' },
},
});
});

it('leaves mixed enum types with explicit type untouched to surface provider error', () => {
const schema = {
type: 'object',
properties: {
bad: { type: 'object', enum: ['move', 1] },
},
};

// inferTypeFromValues throws for mixed types; we should not overwrite the
// explicit type so the downstream provider validator can report the issue.
expect(() => normalizeKimiToolSchema(schema)).not.toThrow();
const result = normalizeKimiToolSchema(schema);
expect(result).toEqual({
type: 'object',
properties: {
bad: { type: 'object', enum: ['move', 1] },
},
});
});

it('infers object and array property types from container enum/const values', () => {
const schema = {
type: 'object',
Expand Down