@@ -464,6 +464,25 @@ export const MCP_NAME_HEADER_SOURCE: Readonly<Record<string, 'name' | 'uri'>> =
464464 'resources/read' : 'uri'
465465} ;
466466
467+ /** Strip RFC 9110 optional whitespace (SP / HTAB) around a field value in linear time. */
468+ function stripHttpOws ( value : string ) : string {
469+ let start = 0 ;
470+ while ( start < value . length ) {
471+ const code = value . codePointAt ( start ) ;
472+ if ( code !== 0x09 && code !== 0x20 ) break ;
473+ start += 1 ;
474+ }
475+
476+ let end = value . length ;
477+ while ( end > start ) {
478+ const code = value . codePointAt ( end - 1 ) ;
479+ if ( code !== 0x09 && code !== 0x20 ) break ;
480+ end -= 1 ;
481+ }
482+
483+ return start === 0 && end === value . length ? value : value . slice ( start , end ) ;
484+ }
485+
467486/**
468487 * SEP-2243 standard-header server-side validation, evaluated by the HTTP
469488 * entry on a modern-classified request immediately after
@@ -537,19 +556,20 @@ export function validateStandardRequestHeaders(request: InboundHttpRequest, rout
537556 ) ;
538557 }
539558
540- const decoded = decodeMcpParamValue ( request . mcpNameHeader ) ;
559+ const normalizedNameHeader = stripHttpOws ( request . mcpNameHeader ) ;
560+ const decoded = decodeMcpParamValue ( normalizedNameHeader ) ;
541561 if ( decoded === undefined ) {
542562 return crossCheckMismatch (
543563 'name-header-invalid-encoding' ,
544- request . mcpNameHeader ,
564+ normalizedNameHeader ,
545565 'the Mcp-Name header carries an invalid Base64 sentinel value' ,
546566 'standard-header-validation'
547567 ) ;
548568 }
549569 if ( bodyValue !== undefined && decoded !== bodyValue ) {
550570 return crossCheckMismatch (
551571 'name-header-mismatch' ,
552- request . mcpNameHeader ,
572+ normalizedNameHeader ,
553573 `the body carries params.${ sourceField } ="${ bodyValue } " but the Mcp-Name header names "${ decoded } "` ,
554574 'standard-header-validation'
555575 ) ;
@@ -818,6 +838,18 @@ function classifyNotificationBody(request: InboundHttpRequest, body: JSONRPCNoti
818838 * `modern`) or a ladder rejection; it never throws.
819839 */
820840export function classifyInboundRequest ( request : InboundHttpRequest ) : InboundClassificationOutcome {
841+ // RFC 9110 §5.5: field parsing excludes optional whitespace around a
842+ // field value. Fetch implementations normally perform this normalization,
843+ // but transport-neutral callers and some runtimes can expose raw OWS.
844+ request = {
845+ ...request ,
846+ ...( request . protocolVersionHeader !== undefined && {
847+ protocolVersionHeader : stripHttpOws ( request . protocolVersionHeader )
848+ } ) ,
849+ ...( request . mcpMethodHeader !== undefined && { mcpMethodHeader : stripHttpOws ( request . mcpMethodHeader ) } ) ,
850+ ...( request . mcpNameHeader !== undefined && { mcpNameHeader : stripHttpOws ( request . mcpNameHeader ) } )
851+ } ;
852+
821853 if ( request . httpMethod . toUpperCase ( ) !== 'POST' ) {
822854 // Body-less 2025-era session operations (and any other non-POST
823855 // method): the modern era is POST-only.
0 commit comments