Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fix-transport-exact-optional-property-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@modelcontextprotocol/core': patch
---

Fix TS2420 error when implementing Transport with exactOptionalPropertyTypes enabled

Optional callback properties on the Transport interface (`onclose`, `onerror`, `onmessage`, `setProtocolVersion`, `setSupportedProtocolVersions`) now explicitly include `| undefined` in their type signature. This makes the interface compatible with TypeScript's `exactOptionalPropertyTypes` compiler option, which was previously causing TS2420 "Class incorrectly implements interface" errors for users with that flag enabled.
10 changes: 5 additions & 5 deletions packages/core/src/shared/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ export interface Transport {
*
* This should be invoked when {@linkcode Transport.close | close()} is called as well.
*/
onclose?: () => void;
onclose?: (() => void) | undefined;

/**
* Callback for when an error occurs.
*
* Note that errors are not necessarily fatal; they are used for reporting any kind of exceptional condition out of band.
*/
onerror?: (error: Error) => void;
onerror?: ((error: Error) => void) | undefined;

/**
* Callback for when a message (request or response) is received over the connection.
Expand All @@ -114,7 +114,7 @@ export interface Transport {
*
* The {@linkcode MessageExtraInfo.requestInfo | requestInfo} can be used to get the original request information (headers, etc.)
*/
onmessage?: <T extends JSONRPCMessage>(message: T, extra?: MessageExtraInfo) => void;
onmessage?: (<T extends JSONRPCMessage>(message: T, extra?: MessageExtraInfo) => void) | undefined;

/**
* The session ID generated for this connection.
Comment on lines 118 to 120
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The sessionId property at line 122 is missing | undefined, making the fix incomplete. All five callback properties got | undefined added, but sessionId?: string was left unchanged. The test class itself declares sessionId?: string | undefined (line 19), which would fail TS2420 against the current interface under exactOptionalPropertyTypes: true.

Extended reasoning...

What the bug is

This PR adds | undefined to the five optional callback/method properties on the Transport interface to fix exactOptionalPropertyTypes compatibility. However, the sessionId property at line 122 was not updated:

sessionId?: string;  // should be: sessionId?: string | undefined;

Why this matters

Under exactOptionalPropertyTypes: true, TypeScript treats T | undefined and T as distinct types in optional property contexts. A class that declares sessionId?: string | undefined is not assignable to an interface that declares sessionId?: string. This produces the same TS2420 error that this PR is trying to fix.

Proof via the test file

The PR's own test class at transport.types.test.ts:19 declares:

class ExplicitUndefinedTransport implements Transport {
    sessionId?: string | undefined;  // line 19
    ...
}

This class compiles today only because the project does not have exactOptionalPropertyTypes enabled. Under that flag, the test would fail with TS2420 on sessionId specifically, since the interface still has sessionId?: string without | undefined.

Step-by-step example

  1. User enables exactOptionalPropertyTypes: true in their tsconfig.json
  2. User writes a class implementing Transport with sessionId?: string | undefined (a natural pattern, and the exact pattern shown in the test)
  3. TypeScript sees that the interface declares sessionId?: string (no | undefined)
  4. Under exactOptionalPropertyTypes, string | undefined is not assignable to string in optional property position
  5. TS2420 error: "Class incorrectly implements interface Transport"

Fix

Change line 122 of packages/core/src/shared/transport.ts from:

sessionId?: string;

to:

sessionId?: string | undefined;

This is consistent with the treatment applied to all other optional properties in this PR and matches the pattern already used in the test class.

Expand All @@ -124,11 +124,11 @@ export interface Transport {
/**
* Sets the protocol version used for the connection (called when the initialize response is received).
*/
setProtocolVersion?: (version: string) => void;
setProtocolVersion?: ((version: string) => void) | undefined;

/**
* Sets the supported protocol versions for header validation (called during connect).
* This allows the server to pass its supported versions to the transport.
*/
setSupportedProtocolVersions?: (versions: string[]) => void;
setSupportedProtocolVersions?: ((versions: string[]) => void) | undefined;
}
36 changes: 36 additions & 0 deletions packages/core/test/shared/transport.types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Compile-time type checks for the Transport interface.
*
* Verifies that a class declaring optional callback properties as `T | undefined`
* (the pattern required by `exactOptionalPropertyTypes: true`) is assignable to
* Transport without TS2420 errors.
*
* See: https://github.com/modelcontextprotocol/typescript-sdk/issues/1314
*/
import { test } from 'vitest';

import type { Transport } from '../../src/shared/transport.js';
import type { JSONRPCMessage, MessageExtraInfo } from '../../src/types/types.js';

// A concrete class that uses the explicit `| undefined` union form for optional callbacks.
// With the old Transport interface (no `| undefined` on callbacks), this class would produce
// TS2420 under `exactOptionalPropertyTypes: true`.
class ExplicitUndefinedTransport implements Transport {
sessionId?: string | undefined;
onclose?: (() => void) | undefined;
onerror?: ((error: Error) => void) | undefined;
onmessage?: (<T extends JSONRPCMessage>(message: T, extra?: MessageExtraInfo) => void) | undefined;
setProtocolVersion?: ((version: string) => void) | undefined;
setSupportedProtocolVersions?: ((versions: string[]) => void) | undefined;

async start(): Promise<void> {}
async close(): Promise<void> {}
async send(_message: JSONRPCMessage): Promise<void> {}
}

test('Transport allows explicit | undefined on optional callback properties', () => {
const transport: Transport = new ExplicitUndefinedTransport();
// The mere fact this file compiles is the assertion.
// We also verify runtime assignability here.
expect(transport).toBeDefined();
});
Comment on lines +15 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 This type test does not actually verify exactOptionalPropertyTypes compatibility because the project tsconfig does not enable that flag (it is not part of strict mode). Without exactOptionalPropertyTypes: true in the compilation, TypeScript treats prop?: T and prop?: T | undefined identically, so this test passes with or without the PR changes. Consider adding a dedicated tsconfig with exactOptionalPropertyTypes: true for this test file, or using @ts-expect-error annotations to assert the old signatures would fail.

Extended reasoning...

The issue

The test file transport.types.test.ts is intended to verify that the updated Transport interface works correctly under exactOptionalPropertyTypes: true. The comment at lines 16-17 explicitly states: "With the old Transport interface (no | undefined on callbacks), this class would produce TS2420 under exactOptionalPropertyTypes: true." However, the test is not actually compiled with that flag enabled.

Why it does not work

The project tsconfig chain is: packages/core/tsconfig.json@modelcontextprotocol/tsconfig (at common/tsconfig/tsconfig.json). The base config sets "strict": true but does not set "exactOptionalPropertyTypes": true. This is a common misconception — exactOptionalPropertyTypes is a separate opt-in flag that is not included in strict mode.

Without exactOptionalPropertyTypes: true, TypeScript treats prop?: T and prop?: T | undefined as equivalent for assignability purposes. This means the ExplicitUndefinedTransport class would successfully implement the old Transport interface (without | undefined) just fine.

Step-by-step proof

  1. Consider the old interface: onclose?: () => void;
  2. The test class declares: onclose?: (() => void) | undefined;
  3. Without exactOptionalPropertyTypes, TypeScript internally expands onclose?: () => void to onclose?: (() => void) | undefined anyway.
  4. So the class property matches the interface property — no TS2420 error, regardless of the PR changes.
  5. If someone reverted the | undefined additions in transport.ts, this test would still pass.

Impact

The test cannot catch regressions. If a future change accidentally removes the | undefined from the Transport interface properties, this test will continue to pass, giving false confidence that the exactOptionalPropertyTypes fix is still in place.

How to fix

Either:

  • Create a separate tsconfig.exactOptional.json that extends the base config and adds "exactOptionalPropertyTypes": true, then configure this test to use it.
  • Use @ts-expect-error annotations with a negative test case that asserts the old signatures would fail.
  • Add a triple-slash directive or use tsd / dtslint for more robust type-level testing.

Note: The actual code fix in transport.ts is correct and valuable for downstream consumers who enable exactOptionalPropertyTypes. This is purely a test quality issue.

Loading