Skip to content

Commit 0b25188

Browse files
ochafikchenxi-null
authored andcommitted
add MakeUnknownsNotOptional to fix optionality of Zod unknown() fields
1 parent 35443bc commit 0b25188

File tree

2 files changed

+52
-21
lines changed

2 files changed

+52
-21
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"dist"
3636
],
3737
"scripts": {
38-
"fetch:spec-types": "test -f src/spec.types.ts || curl -o src/spec.types.ts https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/heads/main/schema/draft/schema.ts",
38+
"fetch:spec-types": "curl -o src/spec.types.ts https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/heads/main/schema/draft/schema.ts",
3939
"build": "npm run build:esm && npm run build:cjs",
4040
"build:esm": "mkdir -p dist/esm && echo '{\"type\": \"module\"}' > dist/esm/package.json && tsc -p tsconfig.prod.json",
4141
"build:esm:w": "npm run build:esm -- -w",

src/spec.types.test.ts

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,51 @@
1+
/**
2+
* This contains:
3+
* - Static type checks to verify the Spec's types are compatible with the SDK's types
4+
* (mutually assignable, w/ slight affordances to get rid of ZodObject.passthrough() index signatures, etc)
5+
* - Runtime checks to verify all Spec types have a static check
6+
* (a few don't have SDK types, see TODOs in this file)
7+
*/
18
import * as SDKTypes from "./types.js";
29
import * as SpecTypes from "./spec.types.js";
310

411
/* eslint-disable @typescript-eslint/no-unused-vars */
512
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
613
/* eslint-disable @typescript-eslint/no-require-imports */
714

8-
// Deep version that recursively removes index signatures (caused by ZodObject.passthrough()) and turns unknowns into `object | undefined`
9-
// TODO: make string index mapping tighter
10-
// TODO: split into multiple transformations if needed
15+
// Removes index signatures added by ZodObject.passthrough().
1116
type RemovePassthrough<T> = T extends object
1217
? T extends Array<infer U>
1318
? Array<RemovePassthrough<U>>
1419
: T extends Function
1520
? T
1621
: {[K in keyof T as string extends K ? never : K]: RemovePassthrough<T[K]>}
17-
: T;
22+
: T;
23+
24+
type IsUnknown<T> = [unknown] extends [T] ? [T] extends [unknown] ? true : false : false;
25+
26+
// Turns {x?: unknown} into {x: unknown} but keeps {_meta?: unknown} unchanged (and leaves other optional properties unchanged, e.g. {x?: string}).
27+
// This works around an apparent quirk of ZodObject.unknown() (makes fields optional)
28+
type MakeUnknownsNotOptional<T> =
29+
IsUnknown<T> extends true
30+
? unknown
31+
: (T extends object
32+
? (T extends Array<infer U>
33+
? Array<MakeUnknownsNotOptional<U>>
34+
: (T extends Function
35+
? T
36+
: Pick<T, never> & {
37+
// Start with empty object to avoid duplicates
38+
// Make unknown properties required (except _meta)
39+
[K in keyof T as '_meta' extends K ? never : IsUnknown<T[K]> extends true ? K : never]-?: unknown;
40+
} &
41+
Pick<T, {
42+
// Pick all _meta and non-unknown properties with original modifiers
43+
[K in keyof T]: '_meta' extends K ? K : IsUnknown<T[K]> extends true ? never : K
44+
}[keyof T]> & {
45+
// Recurse on the picked properties
46+
[K in keyof Pick<T, {[K in keyof T]: '_meta' extends K ? K : IsUnknown<T[K]> extends true ? never : K}[keyof T]>]: MakeUnknownsNotOptional<T[K]>
47+
}))
48+
: T);
1849

1950
function checkCancelledNotification(
2051
sdk: SDKTypes.CancelledNotification,
@@ -36,7 +67,7 @@ function checkImplementation(
3667
) {
3768
sdk = spec;
3869
spec = sdk;
39-
}
70+
}
4071
function checkProgressNotification(
4172
sdk: SDKTypes.ProgressNotification,
4273
spec: SpecTypes.ProgressNotification
@@ -564,70 +595,70 @@ function checkJSONRPCMessage(
564595
spec = sdk;
565596
}
566597
function checkCreateMessageRequest(
567-
sdk: RemovePassthrough<SDKTypes.CreateMessageRequest>, // TODO(quirk): some {} typ>e
598+
sdk: RemovePassthrough<SDKTypes.CreateMessageRequest>,
568599
spec: SpecTypes.CreateMessageRequest
569600
) {
570601
sdk = spec;
571602
spec = sdk;
572603
}
573604
function checkInitializeRequest(
574-
sdk: RemovePassthrough<SDKTypes.InitializeRequest>, // TODO(quirk): some {} type
605+
sdk: RemovePassthrough<SDKTypes.InitializeRequest>,
575606
spec: SpecTypes.InitializeRequest
576607
) {
577608
sdk = spec;
578609
spec = sdk;
579610
}
580611
function checkInitializeResult(
581-
sdk: RemovePassthrough<SDKTypes.InitializeResult>, // TODO(quirk): some {} type
612+
sdk: RemovePassthrough<SDKTypes.InitializeResult>,
582613
spec: SpecTypes.InitializeResult
583614
) {
584615
sdk = spec;
585616
spec = sdk;
586617
}
587618
function checkClientCapabilities(
588-
sdk: RemovePassthrough<SDKTypes.ClientCapabilities>, // TODO(quirk): {}
619+
sdk: RemovePassthrough<SDKTypes.ClientCapabilities>,
589620
spec: SpecTypes.ClientCapabilities
590621
) {
591622
sdk = spec;
592623
spec = sdk;
593624
}
594625
function checkServerCapabilities(
595-
sdk: RemovePassthrough<SDKTypes.ServerCapabilities>, // TODO(quirk): {}
626+
sdk: RemovePassthrough<SDKTypes.ServerCapabilities>,
596627
spec: SpecTypes.ServerCapabilities
597628
) {
598629
sdk = spec;
599630
spec = sdk;
600631
}
601632
function checkClientRequest(
602-
sdk: RemovePassthrough<SDKTypes.ClientRequest>, // TODO(quirk): capabilities.logging is {}
633+
sdk: RemovePassthrough<SDKTypes.ClientRequest>,
603634
spec: SpecTypes.ClientRequest
604635
) {
605636
sdk = spec;
606637
spec = sdk;
607638
}
608639
function checkServerRequest(
609-
sdk: RemovePassthrough<SDKTypes.ServerRequest>, // TODO(quirk): some {} typ
640+
sdk: RemovePassthrough<SDKTypes.ServerRequest>,
610641
spec: SpecTypes.ServerRequest
611642
) {
612643
sdk = spec;
613644
spec = sdk;
614645
}
615646
function checkLoggingMessageNotification(
616-
sdk: SDKTypes.LoggingMessageNotification,
647+
sdk: MakeUnknownsNotOptional<SDKTypes.LoggingMessageNotification>,
617648
spec: SpecTypes.LoggingMessageNotification
618649
) {
619650
sdk = spec;
620-
// spec = sdk; // TODO(bug): data is optional
651+
spec = sdk;
621652
}
622653
function checkServerNotification(
623-
sdk: SDKTypes.ServerNotification,
654+
sdk: MakeUnknownsNotOptional<SDKTypes.ServerNotification>,
624655
spec: SpecTypes.ServerNotification
625656
) {
626657
sdk = spec;
627-
// spec = sdk; // TODO(bug): data is optional
658+
spec = sdk;
628659
}
629660

630-
// TODO(bug): missing type in SDK
661+
// TODO(bug): missing type in SDK. This dead code is checked by the test suite below.
631662
// function checkModelHint(
632663
// RemovePassthrough< sdk: SDKTypes.ModelHint>,
633664
// spec: SpecTypes.ModelHint
@@ -636,7 +667,7 @@ function checkServerNotification(
636667
// spec = sdk;
637668
// }
638669

639-
// TODO(bug): missing type in SDK
670+
// TODO(bug): missing type in SDK. This dead code is checked by the test suite below.
640671
// function checkModelPreferences(
641672
// RemovePassthrough< sdk: SDKTypes.ModelPreferences>,
642673
// spec: SpecTypes.ModelPreferences
@@ -645,7 +676,7 @@ function checkServerNotification(
645676
// spec = sdk;
646677
// }
647678

648-
// TODO(bug): missing type in SDK
679+
// TODO(bug): missing type in SDK. This dead code is checked by the test suite below.
649680
// function checkAnnotations(
650681
// RemovePassthrough< sdk: SDKTypes.Annotations>,
651682
// spec: SpecTypes.Annotations
@@ -661,7 +692,7 @@ describe('Spec Types', () => {
661692
const specTypesContent = require('fs').readFileSync(SPEC_TYPES_FILE, 'utf-8');
662693
const typeNames = [...specTypesContent.matchAll(/export\s+interface\s+(\w+)\b/g)].map(m => m[1]);
663694
const testContent = require('fs').readFileSync(THIS_SOURCE_FILE, 'utf-8');
664-
695+
665696
it('should define some expected types', () => {
666697
expect(typeNames).toContain('JSONRPCNotification');
667698
expect(typeNames).toContain('ElicitResult');

0 commit comments

Comments
 (0)