@@ -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