diff --git a/.gitignore b/.gitignore index 6c4bf1a6..694735b6 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,9 @@ web_modules/ # Output of 'npm pack' *.tgz +# Output of 'npm run fetch:spec-types' +spec.types.ts + # Yarn Integrity file .yarn-integrity diff --git a/package.json b/package.json index 24ba826b..894081d7 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "dist" ], "scripts": { + "fetch:spec-types": "curl -o spec.types.ts https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/heads/main/schema/draft/schema.ts", "build": "npm run build:esm && npm run build:cjs", "build:esm": "mkdir -p dist/esm && echo '{\"type\": \"module\"}' > dist/esm/package.json && tsc -p tsconfig.prod.json", "build:esm:w": "npm run build:esm -- -w", @@ -43,7 +44,7 @@ "examples:simple-server:w": "tsx --watch src/examples/server/simpleStreamableHttp.ts --oauth", "prepack": "npm run build:esm && npm run build:cjs", "lint": "eslint src/", - "test": "jest", + "test": "npm run fetch:spec-types && jest", "start": "npm run server", "server": "tsx watch --clear-screen=false src/cli.ts server", "client": "tsx src/cli.ts client" diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts new file mode 100644 index 00000000..09cd6c2d --- /dev/null +++ b/src/spec.types.test.ts @@ -0,0 +1,705 @@ +/** + * This contains: + * - Static type checks to verify the Spec's types are compatible with the SDK's types + * (mutually assignable, w/ slight affordances to get rid of ZodObject.passthrough() index signatures, etc) + * - Runtime checks to verify each Spec type has a static check + * (note: a few don't have SDK types, see MISSING_SDK_TYPES below) + */ +import * as SDKTypes from "./types.js"; +import * as SpecTypes from "../spec.types.js"; +import fs from "node:fs"; + +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ + +// Removes index signatures added by ZodObject.passthrough(). +type RemovePassthrough = T extends object + ? T extends Array + ? Array> + : T extends Function + ? T + : {[K in keyof T as string extends K ? never : K]: RemovePassthrough} + : T; + +type IsUnknown = [unknown] extends [T] ? [T] extends [unknown] ? true : false : false; + +// Turns {x?: unknown} into {x: unknown} but keeps {_meta?: unknown} unchanged (and leaves other optional properties unchanged, e.g. {x?: string}). +// This works around an apparent quirk of ZodObject.unknown() (makes fields optional) +type MakeUnknownsNotOptional = + IsUnknown extends true + ? unknown + : (T extends object + ? (T extends Array + ? Array> + : (T extends Function + ? T + : Pick & { + // Start with empty object to avoid duplicates + // Make unknown properties required (except _meta) + [K in keyof T as '_meta' extends K ? never : IsUnknown extends true ? K : never]-?: unknown; + } & + Pick extends true ? never : K + }[keyof T]> & { + // Recurse on the picked properties + [K in keyof Pick extends true ? never : K}[keyof T]>]: MakeUnknownsNotOptional + })) + : T); + +function checkCancelledNotification( + sdk: SDKTypes.CancelledNotification, + spec: SpecTypes.CancelledNotification +) { + sdk = spec; + spec = sdk; +} +function checkBaseMetadata( + sdk: RemovePassthrough, + spec: SpecTypes.BaseMetadata +) { + sdk = spec; + spec = sdk; +} +function checkImplementation( + sdk: RemovePassthrough, + spec: SpecTypes.Implementation +) { + sdk = spec; + spec = sdk; +} +function checkProgressNotification( + sdk: SDKTypes.ProgressNotification, + spec: SpecTypes.ProgressNotification +) { + sdk = spec; + spec = sdk; +} + +function checkSubscribeRequest( + sdk: SDKTypes.SubscribeRequest, + spec: SpecTypes.SubscribeRequest +) { + sdk = spec; + spec = sdk; +} +function checkUnsubscribeRequest( + sdk: SDKTypes.UnsubscribeRequest, + spec: SpecTypes.UnsubscribeRequest +) { + sdk = spec; + spec = sdk; +} +function checkPaginatedRequest( + sdk: SDKTypes.PaginatedRequest, + spec: SpecTypes.PaginatedRequest +) { + sdk = spec; + spec = sdk; +} +function checkPaginatedResult( + sdk: SDKTypes.PaginatedResult, + spec: SpecTypes.PaginatedResult +) { + sdk = spec; + spec = sdk; +} +function checkListRootsRequest( + sdk: SDKTypes.ListRootsRequest, + spec: SpecTypes.ListRootsRequest +) { + sdk = spec; + spec = sdk; +} +function checkListRootsResult( + sdk: RemovePassthrough, + spec: SpecTypes.ListRootsResult +) { + sdk = spec; + spec = sdk; +} +function checkRoot( + sdk: RemovePassthrough, + spec: SpecTypes.Root +) { + sdk = spec; + spec = sdk; +} +function checkElicitRequest( + sdk: RemovePassthrough, + spec: SpecTypes.ElicitRequest +) { + sdk = spec; + spec = sdk; +} +function checkElicitResult( + sdk: RemovePassthrough, + spec: SpecTypes.ElicitResult +) { + sdk = spec; + spec = sdk; +} +function checkCompleteRequest( + sdk: RemovePassthrough, + spec: SpecTypes.CompleteRequest +) { + sdk = spec; + spec = sdk; +} +function checkCompleteResult( + sdk: SDKTypes.CompleteResult, + spec: SpecTypes.CompleteResult +) { + sdk = spec; + spec = sdk; +} +function checkProgressToken( + sdk: SDKTypes.ProgressToken, + spec: SpecTypes.ProgressToken +) { + sdk = spec; + spec = sdk; +} +function checkCursor( + sdk: SDKTypes.Cursor, + spec: SpecTypes.Cursor +) { + sdk = spec; + spec = sdk; +} +function checkRequest( + sdk: SDKTypes.Request, + spec: SpecTypes.Request +) { + sdk = spec; + spec = sdk; +} +function checkResult( + sdk: SDKTypes.Result, + spec: SpecTypes.Result +) { + sdk = spec; + spec = sdk; +} +function checkRequestId( + sdk: SDKTypes.RequestId, + spec: SpecTypes.RequestId +) { + sdk = spec; + spec = sdk; +} +function checkJSONRPCRequest( + sdk: SDKTypes.JSONRPCRequest, + spec: SpecTypes.JSONRPCRequest +) { + sdk = spec; + spec = sdk; +} +function checkJSONRPCNotification( + sdk: SDKTypes.JSONRPCNotification, + spec: SpecTypes.JSONRPCNotification +) { + sdk = spec; + spec = sdk; +} +function checkJSONRPCResponse( + sdk: SDKTypes.JSONRPCResponse, + spec: SpecTypes.JSONRPCResponse +) { + sdk = spec; + spec = sdk; +} +function checkEmptyResult( + sdk: SDKTypes.EmptyResult, + spec: SpecTypes.EmptyResult +) { + sdk = spec; + spec = sdk; +} +function checkNotification( + sdk: SDKTypes.Notification, + spec: SpecTypes.Notification +) { + sdk = spec; + spec = sdk; +} +function checkClientResult( + sdk: SDKTypes.ClientResult, + spec: SpecTypes.ClientResult +) { + sdk = spec; + spec = sdk; +} +function checkClientNotification( + sdk: SDKTypes.ClientNotification, + spec: SpecTypes.ClientNotification +) { + sdk = spec; + spec = sdk; +} +function checkServerResult( + sdk: SDKTypes.ServerResult, + spec: SpecTypes.ServerResult +) { + sdk = spec; + spec = sdk; +} +function checkResourceTemplateReference( + sdk: RemovePassthrough, + spec: SpecTypes.ResourceTemplateReference +) { + sdk = spec; + spec = sdk; +} +function checkPromptReference( + sdk: RemovePassthrough, + spec: SpecTypes.PromptReference +) { + sdk = spec; + spec = sdk; +} +function checkToolAnnotations( + sdk: RemovePassthrough, + spec: SpecTypes.ToolAnnotations +) { + sdk = spec; + spec = sdk; +} +function checkTool( + sdk: RemovePassthrough, + spec: SpecTypes.Tool +) { + sdk = spec; + spec = sdk; +} +function checkListToolsRequest( + sdk: SDKTypes.ListToolsRequest, + spec: SpecTypes.ListToolsRequest +) { + sdk = spec; + spec = sdk; +} +function checkListToolsResult( + sdk: RemovePassthrough, + spec: SpecTypes.ListToolsResult +) { + sdk = spec; + spec = sdk; +} +function checkCallToolResult( + sdk: RemovePassthrough, + spec: SpecTypes.CallToolResult +) { + sdk = spec; + spec = sdk; +} +function checkCallToolRequest( + sdk: SDKTypes.CallToolRequest, + spec: SpecTypes.CallToolRequest +) { + sdk = spec; + spec = sdk; +} +function checkToolListChangedNotification( + sdk: SDKTypes.ToolListChangedNotification, + spec: SpecTypes.ToolListChangedNotification +) { + sdk = spec; + spec = sdk; +} +function checkResourceListChangedNotification( + sdk: SDKTypes.ResourceListChangedNotification, + spec: SpecTypes.ResourceListChangedNotification +) { + sdk = spec; + spec = sdk; +} +function checkPromptListChangedNotification( + sdk: SDKTypes.PromptListChangedNotification, + spec: SpecTypes.PromptListChangedNotification +) { + sdk = spec; + spec = sdk; +} +function checkRootsListChangedNotification( + sdk: SDKTypes.RootsListChangedNotification, + spec: SpecTypes.RootsListChangedNotification +) { + sdk = spec; + spec = sdk; +} +function checkResourceUpdatedNotification( + sdk: SDKTypes.ResourceUpdatedNotification, + spec: SpecTypes.ResourceUpdatedNotification +) { + sdk = spec; + spec = sdk; +} +function checkSamplingMessage( + sdk: RemovePassthrough, + spec: SpecTypes.SamplingMessage +) { + sdk = spec; + spec = sdk; +} +function checkCreateMessageResult( + sdk: RemovePassthrough, + spec: SpecTypes.CreateMessageResult +) { + sdk = spec; + spec = sdk; +} +function checkSetLevelRequest( + sdk: SDKTypes.SetLevelRequest, + spec: SpecTypes.SetLevelRequest +) { + sdk = spec; + spec = sdk; +} +function checkPingRequest( + sdk: SDKTypes.PingRequest, + spec: SpecTypes.PingRequest +) { + sdk = spec; + spec = sdk; +} +function checkInitializedNotification( + sdk: SDKTypes.InitializedNotification, + spec: SpecTypes.InitializedNotification +) { + sdk = spec; + spec = sdk; +} +function checkListResourcesRequest( + sdk: SDKTypes.ListResourcesRequest, + spec: SpecTypes.ListResourcesRequest +) { + sdk = spec; + spec = sdk; +} +function checkListResourcesResult( + sdk: RemovePassthrough, + spec: SpecTypes.ListResourcesResult +) { + sdk = spec; + spec = sdk; +} +function checkListResourceTemplatesRequest( + sdk: SDKTypes.ListResourceTemplatesRequest, + spec: SpecTypes.ListResourceTemplatesRequest +) { + sdk = spec; + spec = sdk; +} +function checkListResourceTemplatesResult( + sdk: RemovePassthrough, + spec: SpecTypes.ListResourceTemplatesResult +) { + sdk = spec; + spec = sdk; +} +function checkReadResourceRequest( + sdk: SDKTypes.ReadResourceRequest, + spec: SpecTypes.ReadResourceRequest +) { + sdk = spec; + spec = sdk; +} +function checkReadResourceResult( + sdk: RemovePassthrough, + spec: SpecTypes.ReadResourceResult +) { + sdk = spec; + spec = sdk; +} +function checkResourceContents( + sdk: RemovePassthrough, + spec: SpecTypes.ResourceContents +) { + sdk = spec; + spec = sdk; +} +function checkTextResourceContents( + sdk: RemovePassthrough, + spec: SpecTypes.TextResourceContents +) { + sdk = spec; + spec = sdk; +} +function checkBlobResourceContents( + sdk: RemovePassthrough, + spec: SpecTypes.BlobResourceContents +) { + sdk = spec; + spec = sdk; +} +function checkResource( + sdk: RemovePassthrough, + spec: SpecTypes.Resource +) { + sdk = spec; + spec = sdk; +} +function checkResourceTemplate( + sdk: RemovePassthrough, + spec: SpecTypes.ResourceTemplate +) { + sdk = spec; + spec = sdk; +} +function checkPromptArgument( + sdk: RemovePassthrough, + spec: SpecTypes.PromptArgument +) { + sdk = spec; + spec = sdk; +} +function checkPrompt( + sdk: RemovePassthrough, + spec: SpecTypes.Prompt +) { + sdk = spec; + spec = sdk; +} +function checkListPromptsRequest( + sdk: SDKTypes.ListPromptsRequest, + spec: SpecTypes.ListPromptsRequest +) { + sdk = spec; + spec = sdk; +} +function checkListPromptsResult( + sdk: RemovePassthrough, + spec: SpecTypes.ListPromptsResult +) { + sdk = spec; + spec = sdk; +} +function checkGetPromptRequest( + sdk: SDKTypes.GetPromptRequest, + spec: SpecTypes.GetPromptRequest +) { + sdk = spec; + spec = sdk; +} +function checkTextContent( + sdk: RemovePassthrough, + spec: SpecTypes.TextContent +) { + sdk = spec; + spec = sdk; +} +function checkImageContent( + sdk: RemovePassthrough, + spec: SpecTypes.ImageContent +) { + sdk = spec; + spec = sdk; +} +function checkAudioContent( + sdk: RemovePassthrough, + spec: SpecTypes.AudioContent +) { + sdk = spec; + spec = sdk; +} +function checkEmbeddedResource( + sdk: RemovePassthrough, + spec: SpecTypes.EmbeddedResource +) { + sdk = spec; + spec = sdk; +} +function checkResourceLink( + sdk: RemovePassthrough, + spec: SpecTypes.ResourceLink +) { + sdk = spec; + spec = sdk; +} +function checkContentBlock( + sdk: RemovePassthrough, + spec: SpecTypes.ContentBlock +) { + sdk = spec; + spec = sdk; +} +function checkPromptMessage( + sdk: RemovePassthrough, + spec: SpecTypes.PromptMessage +) { + sdk = spec; + spec = sdk; +} +function checkGetPromptResult( + sdk: RemovePassthrough, + spec: SpecTypes.GetPromptResult +) { + sdk = spec; + spec = sdk; +} +function checkBooleanSchema( + sdk: RemovePassthrough, + spec: SpecTypes.BooleanSchema +) { + sdk = spec; + spec = sdk; +} +function checkStringSchema( + sdk: RemovePassthrough, + spec: SpecTypes.StringSchema +) { + sdk = spec; + spec = sdk; +} +function checkNumberSchema( + sdk: RemovePassthrough, + spec: SpecTypes.NumberSchema +) { + sdk = spec; + spec = sdk; +} +function checkEnumSchema( + sdk: RemovePassthrough, + spec: SpecTypes.EnumSchema +) { + sdk = spec; + spec = sdk; +} +function checkPrimitiveSchemaDefinition( + sdk: RemovePassthrough, + spec: SpecTypes.PrimitiveSchemaDefinition +) { + sdk = spec; + spec = sdk; +} +function checkJSONRPCError( + sdk: SDKTypes.JSONRPCError, + spec: SpecTypes.JSONRPCError +) { + sdk = spec; + spec = sdk; +} +function checkJSONRPCMessage( + sdk: SDKTypes.JSONRPCMessage, + spec: SpecTypes.JSONRPCMessage +) { + sdk = spec; + spec = sdk; +} +function checkCreateMessageRequest( + sdk: RemovePassthrough, + spec: SpecTypes.CreateMessageRequest +) { + sdk = spec; + spec = sdk; +} +function checkInitializeRequest( + sdk: RemovePassthrough, + spec: SpecTypes.InitializeRequest +) { + sdk = spec; + spec = sdk; +} +function checkInitializeResult( + sdk: RemovePassthrough, + spec: SpecTypes.InitializeResult +) { + sdk = spec; + spec = sdk; +} +function checkClientCapabilities( + sdk: RemovePassthrough, + spec: SpecTypes.ClientCapabilities +) { + sdk = spec; + spec = sdk; +} +function checkServerCapabilities( + sdk: RemovePassthrough, + spec: SpecTypes.ServerCapabilities +) { + sdk = spec; + spec = sdk; +} +function checkClientRequest( + sdk: RemovePassthrough, + spec: SpecTypes.ClientRequest +) { + sdk = spec; + spec = sdk; +} +function checkServerRequest( + sdk: RemovePassthrough, + spec: SpecTypes.ServerRequest +) { + sdk = spec; + spec = sdk; +} +function checkLoggingMessageNotification( + sdk: MakeUnknownsNotOptional, + spec: SpecTypes.LoggingMessageNotification +) { + sdk = spec; + spec = sdk; +} +function checkServerNotification( + sdk: MakeUnknownsNotOptional, + spec: SpecTypes.ServerNotification +) { + sdk = spec; + spec = sdk; +} +function checkLoggingLevel( + sdk: SDKTypes.LoggingLevel, + spec: SpecTypes.LoggingLevel +) { + sdk = spec; + spec = sdk; +} + +// This file is .gitignore'd, and fetched by `npm run fetch:spec-types` (called by `npm run test`) +const SPEC_TYPES_FILE = 'spec.types.ts'; +const SDK_TYPES_FILE = 'src/types.ts'; + +const MISSING_SDK_TYPES = [ + // These are inlined in the SDK: + 'Role', + + // These aren't supported by the SDK yet: + // TODO: Add definitions to the SDK + 'Annotations', + 'ModelHint', + 'ModelPreferences', +] + +function extractExportedTypes(source: string): string[] { + return [...source.matchAll(/export\s+(?:interface|class|type)\s+(\w+)\b/g)].map(m => m[1]); +} + +describe('Spec Types', () => { + const specTypes = extractExportedTypes(fs.readFileSync(SPEC_TYPES_FILE, 'utf-8')); + const sdkTypes = extractExportedTypes(fs.readFileSync(SDK_TYPES_FILE, 'utf-8')); + const testSource = fs.readFileSync(__filename, 'utf-8'); + + it('should define some expected types', () => { + expect(specTypes).toContain('JSONRPCNotification'); + expect(specTypes).toContain('ElicitResult'); + expect(specTypes).toHaveLength(91); + }); + + it('should have up to date list of missing sdk types', () => { + for (const typeName of MISSING_SDK_TYPES) { + expect(sdkTypes).not.toContain(typeName); + } + }); + + for (const type of specTypes) { + if (MISSING_SDK_TYPES.includes(type)) { + continue; // Skip missing SDK types + } + it(`${type} should have a compatibility test`, () => { + expect(testSource).toContain(`function check${type}(`); + }); + } +});