Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 (${enumValues.length} values)`,
);
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 value`,
);
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