Skip to content

Commit 00162c4

Browse files
ochafikclaude
andcommitted
fix: add applyDefaults to elicitation capability and fix preprocess
- Inject applyDefaults?: boolean into ClientCapabilities.elicitation.form - Update addElicitationPreprocess to handle z.looseObject pattern with nested structures - Use brace-counting instead of regex for robust schema extraction This restores backwards compatibility for empty elicitation capabilities ({} -> { form: {} }). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f6ffc37 commit 00162c4

File tree

3 files changed

+102
-18
lines changed

3 files changed

+102
-18
lines changed

scripts/generate-schemas.ts

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,14 @@ function preProcessTypes(content: string): string {
170170
// (types.ts will define these locally with proper schema unions)
171171
inlineJSONRPCResponse(sourceFile);
172172

173-
// Transform 6: Convert JSDoc comments to @description tags for .describe() generation
173+
// Transform 6: Inject SDK-specific extensions to spec types
174+
injectSdkExtensions(sourceFile);
175+
176+
// Transform 7: Convert JSDoc comments to @description tags for .describe() generation
174177
// (Must run before injectDerivedCapabilityTypes so inline types get @description)
175178
convertJsDocToDescription(sourceFile);
176179

177-
// Transform 7: Add derived capability types (extracts from parent interfaces)
180+
// Transform 8: Add derived capability types (extracts from parent interfaces)
178181
injectDerivedCapabilityTypes(sourceFile);
179182

180183
return sourceFile.getFullText();
@@ -475,6 +478,34 @@ function inlineJSONRPCResponse(sourceFile: SourceFile): void {
475478
}
476479
}
477480

481+
/**
482+
* Inject SDK-specific extensions to spec types.
483+
* These are fields/types that the SDK adds beyond what the spec defines.
484+
*/
485+
function injectSdkExtensions(sourceFile: SourceFile): void {
486+
// Add applyDefaults to ClientCapabilities.elicitation.form
487+
// The SDK allows clients to request that servers apply schema defaults to elicitation responses
488+
const clientCaps = sourceFile.getInterface('ClientCapabilities');
489+
if (clientCaps) {
490+
const elicitationProp = clientCaps.getProperty('elicitation');
491+
if (elicitationProp) {
492+
const typeNode = elicitationProp.getTypeNode();
493+
if (typeNode) {
494+
const originalType = typeNode.getText();
495+
// Replace { form?: object; url?: object } with extended form type
496+
if (originalType.includes('form?: object')) {
497+
const newType = originalType.replace(
498+
'form?: object',
499+
'form?: { applyDefaults?: boolean; [key: string]: unknown }'
500+
);
501+
typeNode.replaceWithText(newType);
502+
console.log(' ✓ Added applyDefaults to ClientCapabilities.elicitation.form');
503+
}
504+
}
505+
}
506+
}
507+
}
508+
478509
/**
479510
* Add derived capability types by extracting nested properties from parent interfaces.
480511
* This creates concrete interface definitions that ts-to-zod can generate schemas for.
@@ -1086,16 +1117,65 @@ function addElicitationPreprocess(sourceFile: SourceFile): void {
10861117
let text = initializer.getText();
10871118

10881119
// Find the elicitation field and wrap with preprocess
1089-
// Pattern: elicitation: z.object({ form: ..., url: ... }).optional().describe(...)
1090-
// We need to capture everything up to and including the object's closing paren, then handle the trailing .optional().describe() separately
1091-
const elicitationPattern = /elicitation:\s*(z\s*\.\s*object\(\s*\{[^}]*form:[^}]*url:[^}]*\}\s*\))\s*\.optional\(\)(\s*\.describe\([^)]*\))?/;
1092-
1093-
const match = text.match(elicitationPattern);
1094-
if (match) {
1095-
const innerSchema = match[1]; // z.object({...}) without .optional()
1096-
const describeCall = match[2] || ''; // .describe(...) if present
1097-
const replacement = `elicitation: z.preprocess(
1098-
(value) => {
1120+
// Handle both z.object and z.looseObject patterns
1121+
// The inner schema structure may be complex (nested objects, etc.), so we use brace counting
1122+
const elicitationStart = text.indexOf('elicitation:');
1123+
if (elicitationStart === -1) return;
1124+
1125+
// Find the schema after 'elicitation:'
1126+
const afterElicitation = text.substring(elicitationStart + 'elicitation:'.length);
1127+
1128+
// Find where z.object or z.looseObject starts
1129+
const objectMatch = afterElicitation.match(/^\s*(z\s*\.\s*(?:looseObject|object)\s*\()/);
1130+
if (!objectMatch) return;
1131+
1132+
const schemaStart = elicitationStart + 'elicitation:'.length + objectMatch.index!;
1133+
1134+
// Count braces/parens to find the end of the schema (before .optional())
1135+
let depth = 0;
1136+
let inString = false;
1137+
let stringChar = '';
1138+
let schemaEnd = schemaStart;
1139+
const startPos = schemaStart;
1140+
1141+
for (let i = startPos; i < text.length; i++) {
1142+
const char = text[i];
1143+
const prevChar = i > 0 ? text[i - 1] : '';
1144+
1145+
if (inString) {
1146+
if (char === stringChar && prevChar !== '\\') {
1147+
inString = false;
1148+
}
1149+
} else {
1150+
if (char === '"' || char === "'") {
1151+
inString = true;
1152+
stringChar = char;
1153+
} else if (char === '(' || char === '{' || char === '[') {
1154+
depth++;
1155+
} else if (char === ')' || char === '}' || char === ']') {
1156+
depth--;
1157+
if (depth === 0) {
1158+
schemaEnd = i + 1;
1159+
break;
1160+
}
1161+
}
1162+
}
1163+
}
1164+
1165+
// Extract the inner schema (z.looseObject({...}) or z.object({...}))
1166+
const innerSchema = text.substring(schemaStart, schemaEnd).trim();
1167+
1168+
// Find what follows the schema (.optional().describe(...) etc)
1169+
const afterSchema = text.substring(schemaEnd);
1170+
const optionalMatch = afterSchema.match(/^\s*\.optional\(\)(\s*\.describe\([^)]*\))?/);
1171+
if (!optionalMatch) return;
1172+
1173+
const describeCall = optionalMatch[1] || '';
1174+
const fullMatchEnd = schemaEnd + optionalMatch[0].length;
1175+
1176+
// Build the replacement with preprocess
1177+
const replacement = `elicitation: z.preprocess(
1178+
value => {
10991179
if (value && typeof value === 'object' && !Array.isArray(value)) {
11001180
if (Object.keys(value as Record<string, unknown>).length === 0) {
11011181
return { form: {} };
@@ -1106,10 +1186,9 @@ function addElicitationPreprocess(sourceFile: SourceFile): void {
11061186
${innerSchema}${describeCall}
11071187
).optional()`;
11081188

1109-
text = text.replace(elicitationPattern, replacement);
1110-
varDecl.setInitializer(text);
1111-
console.log(' ✓ Added z.preprocess to ClientCapabilitiesSchema.elicitation');
1112-
}
1189+
text = text.substring(0, elicitationStart) + replacement + text.substring(fullMatchEnd);
1190+
varDecl.setInitializer(text);
1191+
console.log(' ✓ Added z.preprocess to ClientCapabilitiesSchema.elicitation');
11131192
}
11141193

11151194
// =============================================================================

src/generated/sdk.schemas.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,12 @@ export const ClientCapabilitiesSchema = z
265265
},
266266
z
267267
.looseObject({
268-
form: AssertObjectSchema.optional(),
268+
form: z
269+
.looseObject({
270+
applyDefaults: z.boolean().optional()
271+
})
272+
.passthrough()
273+
.optional(),
269274
url: AssertObjectSchema.optional()
270275
})
271276
.describe('Present if the client supports elicitation from the server.')

src/generated/sdk.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ export interface ClientCapabilities {
340340
tools?: object;
341341
};
342342
/** @description Present if the client supports elicitation from the server. */
343-
elicitation?: { form?: object; url?: object };
343+
elicitation?: { form?: { applyDefaults?: boolean; [key: string]: unknown }; url?: object };
344344

345345
/** @description Present if the client supports task-augmented requests. */
346346
tasks?: {

0 commit comments

Comments
 (0)