Skip to content

Commit 7068291

Browse files
simplify: explicit kind brand on transports replaces duck-typing discriminator
ChannelTransport gets `readonly kind?: 'channel'` (optional, back-compat). RequestTransport / ClientTransport get `readonly kind: 'request'` (required). isRequestTransport / isChannelTransport check the brand instead of probing for `onrequest` / `fetch` / `start` / `send`. The SHTTP transport classes now `implements RequestTransport` only (not also ChannelTransport — the brands are mutually exclusive). They keep the start/send/onmessage methods for back-compat with v1 callers, but McpServer/Client.connect() routes them via the request-shaped path.
1 parent 20fbba0 commit 7068291

6 files changed

Lines changed: 27 additions & 14 deletions

File tree

packages/client/src/client/clientTransport.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ export type ClientFetchOptions = {
6565
* interface is adapted via {@linkcode channelAsClientTransport}.
6666
*/
6767
export interface ClientTransport {
68+
/** Explicit shape brand. Required so {@linkcode isChannelTransport} can discriminate without duck-typing. */
69+
readonly kind: 'request';
70+
6871
/**
6972
* Send one JSON-RPC request and resolve with the terminal response.
7073
* Any progress/notifications received before the response are surfaced
@@ -104,8 +107,7 @@ export interface ClientTransport {
104107
* request-shaped path.
105108
*/
106109
export function isChannelTransport(t: Transport | ClientTransport): t is Transport {
107-
if (typeof (t as ClientTransport).fetch === 'function') return false;
108-
return typeof (t as Transport).start === 'function' && typeof (t as Transport).send === 'function';
110+
return (t as ClientTransport).kind !== 'request';
109111
}
110112

111113
/**
@@ -134,6 +136,7 @@ export function channelAsClientTransport(pipe: Transport, dispatcher: Dispatcher
134136
}
135137
};
136138
return {
139+
kind: 'request',
137140
driver,
138141
async fetch(request, opts) {
139142
await ensureStarted();

packages/client/src/client/streamableHttp.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import type {
77
JSONRPCNotification,
88
JSONRPCRequest,
99
JSONRPCResultResponse,
10-
Notification,
11-
Transport
10+
Notification
1211
} from '@modelcontextprotocol/core';
1312
import {
1413
createFetchWithInit,
@@ -194,7 +193,9 @@ export type StreamableHTTPClientTransportOptions = {
194193
* {@linkcode Client.connect}) and the legacy pipe-shaped {@linkcode Transport} (deprecated; kept for
195194
* direct callers and v1 compat).
196195
*/
197-
export class StreamableHTTPClientTransport implements ClientTransport, Transport {
196+
export class StreamableHTTPClientTransport implements ClientTransport {
197+
readonly kind = 'request' as const;
198+
198199
private _abortController?: AbortController;
199200
private _url: URL;
200201
private _resourceMetadataUrl?: URL;

packages/client/test/client/client.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ function mockTransport(handler: (req: JSONRPCRequest, opts?: ClientFetchOptions)
2424
const sent: JSONRPCRequest[] = [];
2525
const notified: Notification[] = [];
2626
const ct: ClientTransport = {
27+
kind: 'request',
2728
async fetch(req, opts) {
2829
sent.push(req);
2930
return handler(req, opts);

packages/core/src/shared/transport.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ export type TransportSendOptions = {
8686
* For request/response-shaped transports (Streamable HTTP), see {@linkcode RequestTransport}.
8787
*/
8888
export interface ChannelTransport {
89+
/**
90+
* Explicit shape brand. Optional (defaults to `'channel'`) so existing
91+
* `Transport` implementations don't need to declare it.
92+
*/
93+
readonly kind?: 'channel';
94+
8995
/**
9096
* Starts processing messages on the transport, including any connection steps that might need to be taken.
9197
*
@@ -173,13 +179,13 @@ export type AttachOptions = {
173179
* per inbound message. The transport itself never imports or references a `Dispatcher`.
174180
*/
175181
export interface RequestTransport {
182+
/** Explicit shape brand. Required so {@linkcode isRequestTransport} can discriminate without duck-typing. */
183+
readonly kind: 'request';
184+
176185
/**
177186
* Callback slot for inbound JSON-RPC requests. Set by `McpServer.connect()`.
178187
* The transport calls this per request and writes the yielded messages
179188
* (notifications + one terminal response) to the HTTP response stream.
180-
*
181-
* Transports MUST declare this property (initialised to `undefined`) so
182-
* {@linkcode isRequestTransport} can discriminate before `connect()` runs.
183189
*/
184190
onrequest?: ((req: JSONRPCRequest, env?: RequestEnv) => AsyncIterable<JSONRPCMessage>) | undefined;
185191

@@ -218,5 +224,5 @@ export interface RequestTransport {
218224

219225
/** Type guard distinguishing {@linkcode RequestTransport} from {@linkcode ChannelTransport}. */
220226
export function isRequestTransport(t: ChannelTransport | RequestTransport): t is RequestTransport {
221-
return 'onrequest' in t;
227+
return (t as RequestTransport).kind === 'request';
222228
}

packages/middleware/node/src/streamableHttp.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
1212
import { getRequestListener } from '@hono/node-server';
1313
import type {
1414
AuthInfo,
15-
ChannelTransport,
1615
JSONRPCErrorResponse,
1716
JSONRPCMessage,
1817
JSONRPCNotification,
@@ -96,7 +95,9 @@ export function toNodeHttpHandler(
9695
* });
9796
* ```
9897
*/
99-
export class NodeStreamableHTTPServerTransport implements ChannelTransport, RequestTransport {
98+
export class NodeStreamableHTTPServerTransport implements RequestTransport {
99+
readonly kind = 'request' as const;
100+
100101
private _webStandardTransport: WebStandardStreamableHTTPServerTransport;
101102
private _requestListener: ReturnType<typeof getRequestListener>;
102103
// Store auth and parsedBody per request for passing through to handleRequest

packages/server/src/server/streamableHttp.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import type {
1616
AuthInfo,
17-
ChannelTransport,
1817
JSONRPCErrorResponse,
1918
JSONRPCMessage,
2019
JSONRPCNotification,
@@ -157,7 +156,9 @@ export interface HandleRequestOptions {
157156
* {@linkcode Transport} interface methods route outbound messages through the
158157
* per-session {@linkcode Backchannel2511}.
159158
*/
160-
export class WebStandardStreamableHTTPServerTransport implements ChannelTransport, RequestTransport {
159+
export class WebStandardStreamableHTTPServerTransport implements RequestTransport {
160+
readonly kind = 'request' as const;
161+
161162
private _options: WebStandardStreamableHTTPServerTransportOptions;
162163
private _session?: SessionCompat;
163164
private _backchannel = new Backchannel2511();
@@ -171,7 +172,7 @@ export class WebStandardStreamableHTTPServerTransport implements ChannelTranspor
171172
onerror?: (error: Error) => void;
172173
onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void;
173174

174-
/** {@linkcode RequestTransport.onrequest} — set by `McpServer.connect()`. Declared so {@linkcode isRequestTransport} matches. */
175+
/** {@linkcode RequestTransport.onrequest} — set by `McpServer.connect()`. */
175176
onrequest: ((req: JSONRPCRequest, env?: RequestEnv) => AsyncIterable<JSONRPCMessage>) | undefined = undefined;
176177
/** {@linkcode RequestTransport.onnotification} — set by `McpServer.connect()`. */
177178
onnotification?: (n: JSONRPCNotification) => void | Promise<void>;

0 commit comments

Comments
 (0)