UNPKG

@h1deya/langchain-mcp-tools

Version:
172 lines (171 loc) 7.75 kB
/** * Transforms a JSON Schema to be compatible with OpenAI's function calling requirements * Used for converting MCP tool schemas to OpenAI function declarations * * OpenAI requires that optional fields must be nullable (.optional() + .nullable()) * for function calling to avoid API errors (based on Structured Outputs API requirements, * strict enforcement coming in future SDK versions). * * This adapter specifically fixes the Zod-related error: * "Zod field uses `.optional()` without `.nullable()` which is not supported by the API" * * This function processes the raw JSON schema before converting it to Zod * to ensure OpenAI compatibility by making all non-required fields nullable. * * The official OpenAI documentation states that in Structured Outputs, * all fields must be required OR if optional, they must also be nullable. * * For OpenAI function calling requirements: * see: https://platform.openai.com/docs/guides/function-calling * For OpenAI structured outputs (where this constraint is documented): * see: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required */ export function makeJsonSchemaOpenAICompatible(schema) { const tracker = { nullableFieldsAdded: [], anyOfNullsAdded: [], oneOfNullsAdded: [], nestedSchemasProcessed: 0, }; const result = transformSchemaInternal(schema, tracker, ''); return { schema: result, wasTransformed: getTotalChanges(tracker) > 0, changesSummary: generateChangesSummary(tracker), }; } function transformSchemaInternal(schema, tracker, path = '') { if (typeof schema !== "object" || schema === null) { return schema; } const result = { ...schema }; // Handle object properties if (result.properties) { const processedProperties = {}; const required = new Set(Array.isArray(result.required) ? result.required : []); for (const [key, propSchema] of Object.entries(result.properties)) { const propPath = path ? `${path}.${key}` : key; const processedProp = transformSchemaInternal(propSchema, tracker, propPath); // If the field is not required, make it nullable if (!required.has(key)) { if (processedProp.type && !processedProp.nullable) { processedProp.nullable = true; tracker.nullableFieldsAdded.push(propPath); } else if (Array.isArray(processedProp.anyOf) && !processedProp.anyOf.some((s) => s.type === "null")) { processedProp.anyOf = [...processedProp.anyOf, { type: "null" }]; tracker.anyOfNullsAdded.push(propPath); } else if (Array.isArray(processedProp.oneOf) && !processedProp.oneOf.some((s) => s.type === "null")) { processedProp.oneOf = [...processedProp.oneOf, { type: "null" }]; tracker.oneOfNullsAdded.push(propPath); } } processedProperties[key] = processedProp; } result.properties = processedProperties; } // Handle anyOf/oneOf/allOf recursively if (Array.isArray(result.anyOf)) { if (result.anyOf) { result.anyOf = result.anyOf.map((subSchema, index) => { tracker.nestedSchemasProcessed++; return transformSchemaInternal(subSchema, tracker, `${path}.anyOf[${index}]`); }); } } if (Array.isArray(result.oneOf)) { if (result.oneOf) { result.oneOf = result.oneOf.map((subSchema, index) => { tracker.nestedSchemasProcessed++; return transformSchemaInternal(subSchema, tracker, `${path}.oneOf[${index}]`); }); } } if (Array.isArray(result.allOf)) { if (result.allOf) { result.allOf = result.allOf.map((subSchema, index) => { tracker.nestedSchemasProcessed++; return transformSchemaInternal(subSchema, tracker, `${path}.allOf[${index}]`); }); } } // Handle array items if (result.items) { if (Array.isArray(result.items)) { // Items is an array of schemas (tuple validation) result.items = result.items.map((item, index) => { tracker.nestedSchemasProcessed++; return transformSchemaInternal(item, tracker, `${path}.items[${index}]`); }); } else { // Items is a single schema (applies to all items) tracker.nestedSchemasProcessed++; result.items = transformSchemaInternal(result.items, tracker, `${path}.items`); } } // Handle additionalProperties if (result.additionalProperties && typeof result.additionalProperties === "object" && result.additionalProperties !== null) { tracker.nestedSchemasProcessed++; result.additionalProperties = transformSchemaInternal(result.additionalProperties, tracker, `${path}.additionalProperties`); } // Handle patternProperties (important for complex schemas) if (result.patternProperties) { const processedPatternProps = {}; for (const [pattern, patternSchema] of Object.entries(result.patternProperties)) { tracker.nestedSchemasProcessed++; processedPatternProps[pattern] = transformSchemaInternal(patternSchema, tracker, `${path}.patternProperties["${pattern}"]`); } result.patternProperties = processedPatternProps; } // Handle definitions (common in complex schemas) if (result.definitions || result.$defs) { const defsKey = result.definitions ? 'definitions' : '$defs'; const defsValue = result[defsKey]; // Type guard: ensure it's an object we can iterate over if (defsValue && typeof defsValue === "object" && defsValue !== null && !Array.isArray(defsValue)) { const processedDefs = {}; for (const [key, defSchema] of Object.entries(defsValue)) { tracker.nestedSchemasProcessed++; processedDefs[key] = transformSchemaInternal(defSchema, tracker, `${path}.${defsKey}.${key}`); } result[defsKey] = processedDefs; } } // Handle 'not' schemas if (result.not) { tracker.nestedSchemasProcessed++; result.not = transformSchemaInternal(result.not, tracker, `${path}.not`); } return result; } function getTotalChanges(tracker) { return tracker.nullableFieldsAdded.length + tracker.anyOfNullsAdded.length + tracker.oneOfNullsAdded.length; } function generateChangesSummary(tracker) { const changes = []; if (tracker.nullableFieldsAdded.length > 0) { const examples = tracker.nullableFieldsAdded.slice(0, 3); const exampleText = examples.join(', '); const moreText = tracker.nullableFieldsAdded.length > 3 ? `, +${tracker.nullableFieldsAdded.length - 3} more` : ''; changes.push(`${tracker.nullableFieldsAdded.length} field(s) made nullable (${exampleText}${moreText})`); } if (tracker.anyOfNullsAdded.length > 0) { changes.push(`${tracker.anyOfNullsAdded.length} anyOf schema(s) extended with null type`); } if (tracker.oneOfNullsAdded.length > 0) { changes.push(`${tracker.oneOfNullsAdded.length} oneOf schema(s) extended with null type`); } if (tracker.nestedSchemasProcessed > 0) { changes.push(`${tracker.nestedSchemasProcessed} nested schema(s) processed`); } if (changes.length === 0) { return ''; } return changes.join(', '); }