Skip to content

Commit 02e5d3b

Browse files
refactor(server): carry the typed message on InboundModernRoute and drop the JSONRPCRequest casts
InboundModernRoute is now a discriminated union over messageKind that carries the guard-proved JSONRPCRequest / JSONRPCNotification body. The classifier already establishes the shape via isJSONRPCRequest / isJSONRPCNotification, so the body is threaded through on the route instead of being re-cast at every read site in serveModern. Removes the five 'as JSONRPCRequest' casts and the 'as JSONRPCRequest | JSONRPCNotification' at the call site; serveModern no longer takes a separate message argument.
1 parent 899f3fd commit 02e5d3b

2 files changed

Lines changed: 46 additions & 37 deletions

File tree

packages/core/src/shared/inboundClassification.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ import { PROTOCOL_VERSION_META_KEY } from '../types/constants.js';
6767
import { ProtocolErrorCode } from '../types/enums.js';
6868
import { ProtocolError, UnsupportedProtocolVersionError } from '../types/errors.js';
6969
import { isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, isJSONRPCResultResponse } from '../types/guards.js';
70-
import type { MessageClassification } from '../types/types.js';
70+
import type { JSONRPCNotification, JSONRPCRequest, MessageClassification } from '../types/types.js';
7171
import { envelopeClaimVersion, hasEnvelopeClaim, requestMetaOf, validateEnvelopeMeta } from './envelope.js';
7272
import { isModernProtocolVersion } from './protocolEras.js';
7373

@@ -126,17 +126,32 @@ export interface InboundLegacyRoute {
126126
requestedVersion?: string;
127127
}
128128

129-
/** The request claims the per-request envelope mechanism and is served on the modern path. */
130-
export interface InboundModernRoute {
131-
kind: 'modern';
132-
/** Whether the classified message is a request or a notification. */
133-
messageKind: 'request' | 'notification';
134-
/**
135-
* The classification handed to the per-request transport and validated by
136-
* the protocol layer against the serving instance's negotiated era.
137-
*/
138-
classification: MessageClassification;
139-
}
129+
/**
130+
* The request claims the per-request envelope mechanism and is served on the
131+
* modern path. Discriminated by `messageKind` so the typed `message` narrows
132+
* with it — the classifier has already proved the JSON-RPC shape via the
133+
* `isJSONRPCRequest` / `isJSONRPCNotification` guards, so consumers never
134+
* cast the body again.
135+
*/
136+
export type InboundModernRoute =
137+
| {
138+
kind: 'modern';
139+
messageKind: 'request';
140+
/** The classified body — guard-proved {@linkcode JSONRPCRequest} shape. */
141+
message: JSONRPCRequest;
142+
/**
143+
* The classification handed to the per-request transport and validated by
144+
* the protocol layer against the serving instance's negotiated era.
145+
*/
146+
classification: MessageClassification;
147+
}
148+
| {
149+
kind: 'modern';
150+
messageKind: 'notification';
151+
/** The classified body — guard-proved {@linkcode JSONRPCNotification} shape. */
152+
message: JSONRPCNotification;
153+
classification: MessageClassification;
154+
};
140155

141156
/** The named steps of the inbound validation ladder, in evaluation order. */
142157
export type InboundValidationRung =
@@ -461,9 +476,9 @@ function classifyBatch(body: readonly unknown[]): InboundClassificationOutcome {
461476
return { kind: 'legacy', reason: 'batch' };
462477
}
463478

464-
function classifyRequestBody(request: InboundHttpRequest, body: Record<string, unknown>): InboundClassificationOutcome {
465-
const params = body['params'];
466-
const method = body['method'] as string;
479+
function classifyRequestBody(request: InboundHttpRequest, body: JSONRPCRequest): InboundClassificationOutcome {
480+
const params = body.params;
481+
const method = body.method;
467482
const headerVersion = request.protocolVersionHeader;
468483
const headerNamesModern = headerVersion !== undefined && isModernProtocolVersion(headerVersion);
469484

@@ -523,7 +538,7 @@ function classifyRequestBody(request: InboundHttpRequest, body: Record<string, u
523538
`the body names method ${method} but the Mcp-Method header names ${request.mcpMethodHeader}`
524539
);
525540
}
526-
return { kind: 'modern', messageKind: 'request', classification: classificationForClaim(claimedVersion) };
541+
return { kind: 'modern', messageKind: 'request', message: body, classification: classificationForClaim(claimedVersion) };
527542
}
528543

529544
// No claim: legacy-era traffic — unless the protocol-version header names a
@@ -554,9 +569,9 @@ function classifyRequestBody(request: InboundHttpRequest, body: Record<string, u
554569
return { kind: 'legacy', reason: 'no-claim', ...(headerVersion !== undefined && { requestedVersion: headerVersion }) };
555570
}
556571

557-
function classifyNotificationBody(request: InboundHttpRequest, body: Record<string, unknown>): InboundClassificationOutcome {
558-
const params = body['params'];
559-
const method = body['method'] as string;
572+
function classifyNotificationBody(request: InboundHttpRequest, body: JSONRPCNotification): InboundClassificationOutcome {
573+
const params = body.params;
574+
const method = body.method;
560575
const headerVersion = request.protocolVersionHeader;
561576
const headerNamesModern = headerVersion !== undefined && isModernProtocolVersion(headerVersion);
562577

@@ -603,7 +618,7 @@ function classifyNotificationBody(request: InboundHttpRequest, body: Record<stri
603618
`the notification body names method ${method} but the Mcp-Method header names ${request.mcpMethodHeader}`
604619
);
605620
}
606-
return { kind: 'modern', messageKind: 'notification', classification };
621+
return { kind: 'modern', messageKind: 'notification', message: body, classification };
607622
}
608623

609624
// Notifications carry no body claim under the current spec, so the
@@ -623,6 +638,7 @@ function classifyNotificationBody(request: InboundHttpRequest, body: Record<stri
623638
return {
624639
kind: 'modern',
625640
messageKind: 'notification',
641+
message: body,
626642
classification: { era: 'modern', revision: headerVersion }
627643
};
628644
}

packages/server/src/server/createMcpHandler.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ import type {
3434
InboundLadderRejection,
3535
InboundLegacyRoute,
3636
InboundModernRoute,
37-
JSONRPCNotification,
38-
JSONRPCRequest,
3937
RequestId
4038
} from '@modelcontextprotocol/core';
4139
import {
@@ -630,12 +628,7 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa
630628
// to turn legacy serving off (modern-only strict).
631629
const legacyHandler: LegacyHttpHandler | undefined = legacy === 'reject' ? undefined : legacyStatelessFallback(factory, reportError);
632630

633-
async function serveModern(
634-
route: InboundModernRoute,
635-
message: JSONRPCRequest | JSONRPCNotification,
636-
request: Request,
637-
authInfo: AuthInfo | undefined
638-
): Promise<Response> {
631+
async function serveModern(route: InboundModernRoute, request: Request, authInfo: AuthInfo | undefined): Promise<Response> {
639632
const claimedRevision = route.classification.revision;
640633
if (claimedRevision === undefined || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedRevision)) {
641634
// The claim names a revision this endpoint does not serve (an
@@ -646,10 +639,10 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa
646639
requested: claimedRevision ?? 'unknown'
647640
});
648641
reportError(error);
649-
return jsonRpcErrorResponse(400, error.code, error.message, error.data, echoableRequestId(message));
642+
return jsonRpcErrorResponse(400, error.code, error.message, error.data, echoableRequestId(route.message));
650643
}
651644

652-
const meta = route.messageKind === 'request' ? requestMetaOf((message as JSONRPCRequest).params) : undefined;
645+
const meta = route.messageKind === 'request' ? requestMetaOf(route.message.params) : undefined;
653646
const declaredClientCapabilities = meta?.[CLIENT_CAPABILITIES_META_KEY] as ClientCapabilities | undefined;
654647

655648
// Pre-dispatch capability gate: a request to a method whose processing
@@ -659,7 +652,7 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa
659652
// spec-mandated HTTP 400 for this error; a handler-time emission would
660653
// surface in-band on HTTP 200.
661654
if (route.messageKind === 'request') {
662-
const required = requiredClientCapabilitiesForRequest((message as JSONRPCRequest).method);
655+
const required = requiredClientCapabilitiesForRequest(route.message.method);
663656
if (required !== undefined) {
664657
const missing = missingClientCapabilities(required, declaredClientCapabilities);
665658
if (missing !== undefined) {
@@ -670,7 +663,7 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa
670663
error.code,
671664
error.message,
672665
error.data,
673-
(message as JSONRPCRequest).id
666+
route.message.id
674667
);
675668
}
676669
}
@@ -692,8 +685,8 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa
692685
// Authorization the consumer performs inside the factory therefore DOES
693686
// see listen requests, although token verification still belongs at the
694687
// middleware layer mounted in front of this entry.
695-
if (route.messageKind === 'request' && (message as JSONRPCRequest).method === 'subscriptions/listen') {
696-
return listenRouter.serve(message as JSONRPCRequest, request.signal, server.getCapabilities());
688+
if (route.messageKind === 'request' && route.message.method === 'subscriptions/listen') {
689+
return listenRouter.serve(route.message, request.signal, server.getCapabilities());
697690
}
698691

699692
// Era-write at instance binding, then modern-only handler installation —
@@ -717,7 +710,7 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa
717710
};
718711

719712
try {
720-
const response = await invoke(product, message, {
713+
const response = await invoke(product, route.message, {
721714
classification: route.classification,
722715
request,
723716
...(authInfo !== undefined && { authInfo }),
@@ -742,7 +735,7 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa
742735
await server.close().catch(() => {});
743736
inflight.delete(server);
744737
reportError(toError(error));
745-
return internalServerErrorResponse(echoableRequestId(message));
738+
return internalServerErrorResponse(echoableRequestId(route.message));
746739
}
747740
}
748741

@@ -794,7 +787,7 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa
794787
return rejectionResponse(outcome, echoableRequestId(body));
795788
}
796789
case 'modern': {
797-
return await serveModern(outcome, body as JSONRPCRequest | JSONRPCNotification, request, authInfo);
790+
return await serveModern(outcome, request, authInfo);
798791
}
799792
case 'legacy': {
800793
return await serveLegacyRoute(outcome, forwardRequest, authInfo, parsedBody);

0 commit comments

Comments
 (0)