Skip to content

Commit 9189270

Browse files
ochafikclaude
andcommitted
feat: remove standalone index signatures from generated sdk.types.ts
Preparation for potential future bulk type re-exports. Changes: 1. Added removeStandaloneIndexSignatures() to clean sdk.types.ts - Removes standalone index signatures from Result, RequestParams, NotificationParams - These break TypeScript union narrowing - Keeps _meta field's internal { [key: string]: unknown } for extensibility 2. Updated test post-processing for passthrough schemas - Extended schemasWithIndexSignatures list to include all Result-based types - Comments out spec → schema direction type checks for these Note: Bulk type re-export (export type * from generated) was attempted but doesn't work because generated interfaces extend base types with passthrough index signatures. The Infer<typeof> approach remains necessary for clean types. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3ffad49 commit 9189270

File tree

3 files changed

+142
-42
lines changed

3 files changed

+142
-42
lines changed

scripts/generate-schemas.ts

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,50 @@ function preProcessTypes(content: string): string {
170170
return sourceFile.getFullText();
171171
}
172172

173+
/**
174+
* Interfaces that should have their standalone index signatures removed.
175+
* These are extensible types in the spec, but index signatures break TypeScript union narrowing.
176+
* The schemas use .passthrough() for runtime extensibility, so types don't need index sigs.
177+
*/
178+
const REMOVE_INDEX_SIGNATURE_INTERFACES = [
179+
'Result',
180+
'RequestParams',
181+
'NotificationParams',
182+
// GetTaskPayloadResult intentionally keeps index signature - it's the payload container
183+
];
184+
185+
/**
186+
* Remove standalone index signatures from interfaces to enable TypeScript union narrowing.
187+
*
188+
* The MCP spec defines extensible types like `Result = { _meta?: {...}; [key: string]: unknown }`.
189+
* Index signatures break TypeScript's ability to narrow unions (can't use `'id' in obj`).
190+
*
191+
* We remove them from the generated types because:
192+
* - Schemas use .passthrough() for runtime extensibility (accepts extra properties)
193+
* - Types without index signatures allow proper TypeScript narrowing
194+
* - The _meta field still has `{ [key: string]: unknown }` for its own extensibility
195+
*/
196+
function removeStandaloneIndexSignatures(content: string): string {
197+
const project = new Project({ useInMemoryFileSystem: true });
198+
const sourceFile = project.createSourceFile('types.ts', content);
199+
200+
console.log(' 🔧 Cleaning up index signatures for type exports...');
201+
202+
for (const ifaceName of REMOVE_INDEX_SIGNATURE_INTERFACES) {
203+
const iface = sourceFile.getInterface(ifaceName);
204+
if (!iface) continue;
205+
206+
// Find and remove index signatures from the interface body
207+
const indexSigs = iface.getIndexSignatures();
208+
for (const sig of indexSigs) {
209+
sig.remove();
210+
console.log(` ✓ Removed index signature from ${ifaceName}`);
211+
}
212+
}
213+
214+
return sourceFile.getFullText();
215+
}
216+
173217
/**
174218
* Transform extends clauses from one type to another.
175219
*/
@@ -943,6 +987,9 @@ async function main() {
943987
const rawSourceText = readFileSync(SPEC_TYPES_FILE, 'utf-8');
944988
const sdkTypesContent = preProcessTypes(rawSourceText);
945989

990+
// Clean up types for SDK export - remove index signatures that break union narrowing
991+
const cleanedTypesContent = removeStandaloneIndexSignatures(sdkTypesContent);
992+
946993
// Write pre-processed types to sdk.types.ts
947994
const sdkTypesWithHeader = `/**
948995
* SDK-compatible types generated from spec.types.ts
@@ -953,11 +1000,12 @@ async function main() {
9531000
* Transformations applied:
9541001
* - \`extends JSONRPCRequest\` → \`extends Request\`
9551002
* - \`extends JSONRPCNotification\` → \`extends Notification\`
1003+
* - Standalone index signatures removed (enables TypeScript union narrowing)
9561004
*
9571005
* This allows SDK types to omit jsonrpc/id fields, which are
9581006
* handled at the transport layer.
9591007
*/
960-
${sdkTypesContent.replace(/^\/\*\*[\s\S]*?\*\/\n/, '')}`;
1008+
${cleanedTypesContent.replace(/^\/\*\*[\s\S]*?\*\/\n/, '')}`;
9611009
writeFileSync(SDK_TYPES_FILE, sdkTypesWithHeader, 'utf-8');
9621010
console.log(`✅ Written: ${SDK_TYPES_FILE}`);
9631011

@@ -1018,11 +1066,12 @@ function postProcessTests(content: string): string {
10181066
// Run: npm run generate:schemas`,
10191067
);
10201068

1021-
// Comment out bidirectional type checks for schemas that use looseObject.
1022-
// looseObject adds an index signature [x: string]: unknown which makes
1023-
// spec types (without index signature) not assignable to schema-inferred types.
1069+
// Comment out bidirectional type checks for schemas that use looseObject or passthrough.
1070+
// These add index signatures [x: string]: unknown to schema-inferred types, but
1071+
// we've removed index signatures from spec types (for union narrowing).
10241072
// The one-way check (schema-inferred → spec) is kept to ensure compatibility.
1025-
const looseObjectSchemas = [
1073+
const schemasWithIndexSignatures = [
1074+
// Capability schemas use looseObject
10261075
'ClientCapabilities',
10271076
'ServerCapabilities',
10281077
'ClientTasksCapability',
@@ -1031,25 +1080,52 @@ function postProcessTests(content: string): string {
10311080
'InitializeRequestParams',
10321081
'InitializeRequest',
10331082
'ClientRequest', // Contains InitializeRequest
1083+
// Result-based schemas use passthrough (Result extends removed index sig)
1084+
'Result',
1085+
'EmptyResult',
1086+
'PaginatedResult',
1087+
'JSONRPCResultResponse',
1088+
'CreateTaskResult',
1089+
'GetTaskResult',
1090+
'CancelTaskResult',
1091+
'ListTasksResult',
1092+
'CompleteResult',
1093+
'ElicitResult',
1094+
'ListRootsResult',
1095+
'ReadResourceResult',
1096+
'ListToolsResult',
1097+
'ListPromptsResult',
1098+
'ListResourceTemplatesResult',
1099+
'ListResourcesResult',
1100+
'CallToolResult',
1101+
'GetPromptResult',
1102+
'CreateMessageResult',
1103+
// Request/Notification based schemas also use passthrough
1104+
'Request',
1105+
'Notification',
1106+
'RequestParams',
1107+
'NotificationParams',
1108+
// Union types that include passthrough schemas
1109+
'JSONRPCMessage',
10341110
];
10351111

10361112
let commentedCount = 0;
1037-
for (const schemaName of looseObjectSchemas) {
1038-
// Comment out spec → schema-inferred checks (these fail with looseObject)
1113+
for (const schemaName of schemasWithIndexSignatures) {
1114+
// Comment out spec → schema-inferred checks (these fail with passthrough/looseObject)
10391115
// ts-to-zod generates PascalCase type names
10401116
// Pattern matches: expectType<FooSchemaInferredType>({} as spec.Foo)
10411117
const pattern = new RegExp(
10421118
`(expectType<${schemaName}SchemaInferredType>\\(\\{\\} as spec\\.${schemaName}\\))`,
10431119
'g'
10441120
);
10451121
const before = content;
1046-
content = content.replace(pattern, `// Skip: looseObject index signature incompatible with spec interface\n// $1`);
1122+
content = content.replace(pattern, `// Skip: passthrough/looseObject index signature incompatible with clean spec interface\n// $1`);
10471123
if (before !== content) {
10481124
commentedCount++;
10491125
}
10501126
}
10511127
if (commentedCount > 0) {
1052-
console.log(` ✓ Commented out ${commentedCount} looseObject type checks in test file`);
1128+
console.log(` ✓ Commented out ${commentedCount} index-signature type checks in test file`);
10531129
}
10541130

10551131
return content;

0 commit comments

Comments
 (0)