Skip to content

Commit a068950

Browse files
ochafikclaude
andcommitted
feat: add union types for type narrowing (McpRequest, McpNotification, McpResult)
- Generate discriminated union types from base interfaces for type narrowing - Naming scheme: base types keep original names (Request, Notification, Result), union types get Mcp prefix (McpRequest, McpNotification, McpResult) - Client and Server classes now default to union types for better DX - Add deprecated aliases (RequestBase, NotificationBase, ResultBase) for migration - Fix type errors in tests and examples by adding proper type casts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d43cab5 commit a068950

File tree

14 files changed

+275
-280
lines changed

14 files changed

+275
-280
lines changed

scripts/generate-schemas.ts

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,12 @@ const BASE_TO_UNION_CONFIG: Record<string, string[]> = {
276276
* interface InitializeResult extends Result { ... }
277277
*
278278
* Into:
279-
* interface ResultBase { _meta?: {...} }
280-
* interface InitializeResult extends ResultBase { ... }
281-
* type Result = InitializeResult | CompleteResult | ...
279+
* interface Result { _meta?: {...} } // Base stays as-is
280+
* interface InitializeResult extends Result { ... }
281+
* type McpResult = InitializeResult | CompleteResult | ... // Union with Mcp prefix
282282
*
283-
* This enables TypeScript union narrowing while preserving the extends hierarchy.
283+
* This enables TypeScript union narrowing while preserving backwards compatibility.
284+
* The base type keeps its original name, and the union gets an "Mcp" prefix.
284285
*/
285286
function convertBaseTypesToUnions(content: string): string {
286287
const project = new Project({ useInMemoryFileSystem: true });
@@ -295,37 +296,16 @@ function convertBaseTypesToUnions(content: string): string {
295296
continue;
296297
}
297298

298-
const baseRename = `${baseName}Base`;
299-
300-
// 1. Rename the base interface to ResultBase
301-
baseInterface.rename(baseRename);
302-
303-
// 2. Update all extends clauses that reference the old name
304-
for (const iface of sourceFile.getInterfaces()) {
305-
for (const ext of iface.getExtends()) {
306-
if (ext.getText() === baseName) {
307-
ext.replaceWithText(baseRename);
308-
}
309-
}
310-
}
311-
312-
// 3. Update type aliases that use intersection with the base
313-
for (const typeAlias of sourceFile.getTypeAliases()) {
314-
const typeNode = typeAlias.getTypeNode();
315-
if (typeNode) {
316-
const text = typeNode.getText();
317-
if (text.includes(baseName) && !text.includes(baseRename)) {
318-
typeAlias.setType(text.replace(new RegExp(`\\b${baseName}\\b`, 'g'), baseRename));
319-
}
320-
}
321-
}
299+
// Base interface keeps its original name (Request, Notification, Result)
300+
// Union type gets Mcp prefix (McpRequest, McpNotification, McpResult)
301+
const unionName = `Mcp${baseName}`;
322302

323-
// 4. Add the union type alias after the base interface
303+
// Add the union type alias after the base interface
324304
const unionType = unionMembers.join(' | ');
325305
const insertPos = baseInterface.getEnd();
326-
sourceFile.insertText(insertPos, `\n\n/** Union of all ${baseName.toLowerCase()} types for type narrowing. */\nexport type ${baseName} = ${unionType};`);
306+
sourceFile.insertText(insertPos, `\n\n/** Union of all MCP ${baseName.toLowerCase()} types for type narrowing. */\nexport type ${unionName} = ${unionType};`);
327307

328-
console.log(` ✓ Converted ${baseName} to union of ${unionMembers.length} types`);
308+
console.log(` ✓ Created ${unionName} as union of ${unionMembers.length} types`);
329309
}
330310

331311
return sourceFile.getFullText();

src/client/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ import {
4848
type ListChangedHandlers,
4949
type Request,
5050
type Notification,
51-
type Result
51+
type Result,
52+
type McpRequest,
53+
type McpNotification,
54+
type McpResult
5255
} from '../types.js';
5356
import { AjvJsonSchemaValidator } from '../validation/ajv-provider.js';
5457
import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator } from '../validation/types.js';
@@ -231,9 +234,9 @@ export type ClientOptions = ProtocolOptions & {
231234
* ```
232235
*/
233236
export class Client<
234-
RequestT extends Request = Request,
235-
NotificationT extends Notification = Notification,
236-
ResultT extends Result = Result
237+
RequestT extends Request = McpRequest,
238+
NotificationT extends Notification = McpNotification,
239+
ResultT extends Result = McpResult
237240
> extends Protocol<ClientRequest | RequestT, ClientNotification | NotificationT, ClientResult | ResultT> {
238241
private _serverCapabilities?: ServerCapabilities;
239242
private _serverVersion?: Implementation;

src/examples/server/simpleStreamableHttp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ const getServer = () => {
488488
text: `Completed ${duration}ms delay`
489489
}
490490
]
491-
});
491+
} as CallToolResult);
492492
})();
493493

494494
// Return CreateTaskResult with the created task

src/examples/server/simpleTaskInteractive.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ const createServer = (): Server => {
561561
console.log(`[Server] Completing task with result: ${text}`);
562562
await taskStore.storeTaskResult(task.taskId, 'completed', {
563563
content: [{ type: 'text', text }]
564-
});
564+
} as CallToolResult);
565565
} else if (name === 'write_haiku') {
566566
const topic = args?.topic ?? 'nature';
567567
console.log(`[Server] write_haiku: topic '${topic}'`);
@@ -586,14 +586,14 @@ const createServer = (): Server => {
586586
console.log('[Server] Completing task with haiku');
587587
await taskStore.storeTaskResult(task.taskId, 'completed', {
588588
content: [{ type: 'text', text: `Haiku:\n${haiku}` }]
589-
});
589+
} as CallToolResult);
590590
}
591591
} catch (error) {
592592
console.error(`[Server] Task ${task.taskId} failed:`, error);
593593
await taskStore.storeTaskResult(task.taskId, 'failed', {
594594
content: [{ type: 'text', text: `Error: ${error}` }],
595595
isError: true
596-
});
596+
} as CallToolResult);
597597
} finally {
598598
activeTaskExecutions.delete(task.taskId);
599599
}

0 commit comments

Comments
 (0)