@@ -39,7 +39,16 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3939import { dirname , join } from 'node:path' ;
4040import { fileURLToPath } from 'node:url' ;
4141import { generate } from 'ts-to-zod' ;
42- import { Project , SyntaxKind , Node , CallExpression , PropertyAssignment } from 'ts-morph' ;
42+ import {
43+ Project ,
44+ SyntaxKind ,
45+ Node ,
46+ CallExpression ,
47+ PropertyAssignment ,
48+ SourceFile ,
49+ InterfaceDeclaration ,
50+ TypeAliasDeclaration
51+ } from 'ts-morph' ;
4352
4453const __filename = fileURLToPath ( import . meta. url ) ;
4554const __dirname = dirname ( __filename ) ;
@@ -240,66 +249,94 @@ function removeIndexSignaturesFromTypes(content: string): string {
240249}
241250
242251/**
243- * Configuration for converting base interfaces to union types.
244- * Maps base interface name to its union members (the concrete types that extend it).
252+ * Check if an interface transitively extends a base interface.
245253 */
246- const BASE_TO_UNION_CONFIG : Record < string , string [ ] > = {
247- Request : [
248- 'InitializeRequest' ,
249- 'PingRequest' ,
250- 'ListResourcesRequest' ,
251- 'ListResourceTemplatesRequest' ,
252- 'ReadResourceRequest' ,
253- 'SubscribeRequest' ,
254- 'UnsubscribeRequest' ,
255- 'ListPromptsRequest' ,
256- 'GetPromptRequest' ,
257- 'ListToolsRequest' ,
258- 'CallToolRequest' ,
259- 'SetLevelRequest' ,
260- 'CompleteRequest' ,
261- 'CreateMessageRequest' ,
262- 'ListRootsRequest' ,
263- 'ElicitRequest' ,
264- 'GetTaskRequest' ,
265- 'GetTaskPayloadRequest' ,
266- 'CancelTaskRequest' ,
267- 'ListTasksRequest'
268- ] ,
269- Notification : [
270- 'CancelledNotification' ,
271- 'InitializedNotification' ,
272- 'ProgressNotification' ,
273- 'ResourceListChangedNotification' ,
274- 'ResourceUpdatedNotification' ,
275- 'PromptListChangedNotification' ,
276- 'ToolListChangedNotification' ,
277- 'LoggingMessageNotification' ,
278- 'RootsListChangedNotification' ,
279- 'TaskStatusNotification' ,
280- 'ElicitationCompleteNotification'
281- ] ,
282- Result : [
283- 'EmptyResult' ,
284- 'InitializeResult' ,
285- 'CompleteResult' ,
286- 'GetPromptResult' ,
287- 'ListPromptsResult' ,
288- 'ListResourceTemplatesResult' ,
289- 'ListResourcesResult' ,
290- 'ReadResourceResult' ,
291- 'CallToolResult' ,
292- 'ListToolsResult' ,
293- 'CreateTaskResult' ,
294- 'GetTaskResult' ,
295- 'GetTaskPayloadResult' ,
296- 'ListTasksResult' ,
297- 'CancelTaskResult' ,
298- 'CreateMessageResult' ,
299- 'ListRootsResult' ,
300- 'ElicitResult'
301- ]
302- } ;
254+ function extendsBase (
255+ iface : InterfaceDeclaration ,
256+ baseName : string ,
257+ sourceFile : SourceFile ,
258+ checked : Set < string > = new Set ( )
259+ ) : boolean {
260+ const name = iface . getName ( ) ;
261+ if ( checked . has ( name ) ) return false ;
262+ checked . add ( name ) ;
263+
264+ for ( const ext of iface . getExtends ( ) ) {
265+ // Handle generic types like "Foo<T>" -> "Foo"
266+ const extName = ext . getText ( ) . split ( '<' ) [ 0 ] . trim ( ) ;
267+ if ( extName === baseName ) return true ;
268+
269+ const parent = sourceFile . getInterface ( extName ) ;
270+ if ( parent && extendsBase ( parent , baseName , sourceFile , checked ) ) {
271+ return true ;
272+ }
273+ }
274+ return false ;
275+ }
276+
277+ /**
278+ * Check if a type alias references a base type (e.g., `type EmptyResult = Result`).
279+ */
280+ function referencesBase ( alias : TypeAliasDeclaration , baseName : string ) : boolean {
281+ const typeText = alias . getTypeNode ( ) ?. getText ( ) || '' ;
282+ // Match patterns like "Result", "Result & Foo", "Foo & Result"
283+ const pattern = new RegExp ( `\\b${ baseName } \\b` ) ;
284+ return pattern . test ( typeText ) ;
285+ }
286+
287+ /**
288+ * Auto-discover union members by finding types that extend/reference a base type.
289+ *
290+ * Finds:
291+ * - Interfaces that transitively extend the base (e.g., ListResourcesRequest → PaginatedRequest → Request)
292+ * - Type aliases that reference the base (e.g., type EmptyResult = Result)
293+ *
294+ * Filters by naming convention (*Request, *Notification, *Result) and excludes abstract bases.
295+ */
296+ function findUnionMembers (
297+ sourceFile : SourceFile ,
298+ baseName : string ,
299+ exclusions : Set < string >
300+ ) : string [ ] {
301+ const members : string [ ] = [ ] ;
302+
303+ // Find interfaces that extend base (transitively)
304+ for ( const iface of sourceFile . getInterfaces ( ) ) {
305+ const name = iface . getName ( ) ;
306+ if ( exclusions . has ( name ) ) continue ;
307+ if ( ! name . endsWith ( baseName ) ) continue ;
308+ if ( extendsBase ( iface , baseName , sourceFile ) ) {
309+ members . push ( name ) ;
310+ }
311+ }
312+
313+ // Find type aliases that reference base
314+ for ( const alias of sourceFile . getTypeAliases ( ) ) {
315+ const name = alias . getName ( ) ;
316+ if ( exclusions . has ( name ) ) continue ;
317+ if ( ! name . endsWith ( baseName ) ) continue ;
318+ // Skip union types we're creating (McpRequest, etc.)
319+ if ( name . startsWith ( 'Mcp' ) ) continue ;
320+ // Skip Client/Server subsets
321+ if ( name . startsWith ( 'Client' ) || name . startsWith ( 'Server' ) ) continue ;
322+ if ( referencesBase ( alias , baseName ) ) {
323+ members . push ( name ) ;
324+ }
325+ }
326+
327+ return members . sort ( ) ;
328+ }
329+
330+ /**
331+ * Abstract base types that should be excluded from union discovery.
332+ * These are intermediate types in the hierarchy, not concrete MCP messages.
333+ */
334+ const UNION_EXCLUSIONS = new Set ( [
335+ 'JSONRPCRequest' ,
336+ 'JSONRPCNotification' ,
337+ 'PaginatedRequest' ,
338+ 'PaginatedResult'
339+ ] ) ;
303340
304341/**
305342 * Convert base interfaces to union types in sdk.types.ts.
@@ -313,35 +350,51 @@ const BASE_TO_UNION_CONFIG: Record<string, string[]> = {
313350 * interface InitializeResult extends Result { ... }
314351 * type McpResult = InitializeResult | CompleteResult | ... // Union with Mcp prefix
315352 *
353+ * Union members are auto-discovered by finding types that:
354+ * - Extend the base interface (transitively), or
355+ * - Are type aliases referencing the base type
356+ * - Match the naming convention (*Request, *Notification, *Result)
357+ * - Are not in the exclusion list (abstract bases like PaginatedRequest)
358+ *
316359 * This enables TypeScript union narrowing while preserving backwards compatibility.
317360 * The base type keeps its original name, and the union gets an "Mcp" prefix.
318361 */
319362function convertBaseTypesToUnions ( content : string ) : string {
320363 const project = new Project ( { useInMemoryFileSystem : true } ) ;
321364 const sourceFile = project . createSourceFile ( 'types.ts' , content ) ;
322365
323- console . log ( ' 🔧 Converting base types to unions...' ) ;
366+ console . log ( ' 🔧 Converting base types to unions (auto-discovering members) ...' ) ;
324367
325- for ( const [ baseName , unionMembers ] of Object . entries ( BASE_TO_UNION_CONFIG ) ) {
368+ const baseNames = [ 'Request' , 'Notification' , 'Result' ] ;
369+
370+ for ( const baseName of baseNames ) {
326371 const baseInterface = sourceFile . getInterface ( baseName ) ;
327372 if ( ! baseInterface ) {
328373 console . warn ( ` ⚠️ Interface ${ baseName } not found` ) ;
329374 continue ;
330375 }
331376
377+ // Auto-discover union members
378+ const unionMembers = findUnionMembers ( sourceFile , baseName , UNION_EXCLUSIONS ) ;
379+
380+ if ( unionMembers . length === 0 ) {
381+ console . warn ( ` ⚠️ No members found for ${ baseName } ` ) ;
382+ continue ;
383+ }
384+
332385 // Base interface keeps its original name (Request, Notification, Result)
333386 // Union type gets Mcp prefix (McpRequest, McpNotification, McpResult)
334387 const unionName = `Mcp${ baseName } ` ;
335388
336389 // Add the union type alias after the base interface
337- const unionType = unionMembers . join ( ' | ' ) ;
390+ const unionType = unionMembers . join ( '\n | ' ) ;
338391 const insertPos = baseInterface . getEnd ( ) ;
339392 sourceFile . insertText (
340393 insertPos ,
341- `\n\n/** Union of all MCP ${ baseName . toLowerCase ( ) } types for type narrowing. */\nexport type ${ unionName } = ${ unionType } ;`
394+ `\n\n/** Union of all MCP ${ baseName . toLowerCase ( ) } types for type narrowing. */\nexport type ${ unionName } =\n | ${ unionType } ;`
342395 ) ;
343396
344- console . log ( ` ✓ Created ${ unionName } as union of ${ unionMembers . length } types ` ) ;
397+ console . log ( ` ✓ Created ${ unionName } with ${ unionMembers . length } auto-discovered members ` ) ;
345398 }
346399
347400 return sourceFile . getFullText ( ) ;
0 commit comments