@@ -239,24 +239,39 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
239239 'Tests that client mirrors x-mcp-header tool parameters into Mcp-Param headers with correct encoding (SEP-2243)' ;
240240
241241 private toolCallReceived : boolean = false ;
242+ private nullToolCallReceived : boolean = false ;
242243
243244 async start ( ) : Promise < ScenarioUrls > {
244245 const urls = await super . start ( ) ;
245246 // Pass test values via context for encoding edge cases.
246247 // The conformance client should use these values when calling test_custom_headers.
247248 urls . context = {
248- toolCall : {
249- name : 'test_custom_headers' ,
250- arguments : {
251- region : 'us-west1' ,
252- priority : 42 ,
253- verbose : false ,
254- empty_val : '' ,
255- method_val : 'test-method' ,
256- float_val : 3.14159 ,
257- query : 'SELECT * FROM users'
249+ toolCalls : [
250+ {
251+ name : 'test_custom_headers' ,
252+ arguments : {
253+ region : 'us-west1' ,
254+ priority : 42 ,
255+ verbose : false ,
256+ empty_val : '' ,
257+ method_val : 'test-method' ,
258+ float_val : 3.14159 ,
259+ non_ascii_val : 'Hello, 世界' ,
260+ whitespace_val : ' padded ' ,
261+ control_char_val : 'line1\nline2' ,
262+ query : 'SELECT * FROM users'
263+ }
264+ } ,
265+ {
266+ name : 'test_custom_headers_null' ,
267+ arguments : {
268+ region : 'us-east1' ,
269+ priority : 1 ,
270+ verbose : null ,
271+ query : 'SELECT 1'
272+ }
258273 }
259- }
274+ ]
260275 } ;
261276 return urls ;
262277 }
@@ -274,6 +289,19 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
274289 specReferences : [ SPEC_REFERENCE_CUSTOM ]
275290 } ) ;
276291 }
292+ if ( ! this . nullToolCallReceived ) {
293+ this . checks . push ( {
294+ id : 'client-custom-header-omit-null' ,
295+ name : 'ClientCustomHeaderOmitNull' ,
296+ description :
297+ 'Client MUST omit Mcp-Param header when parameter value is null or not provided' ,
298+ status : 'FAILURE' ,
299+ timestamp : new Date ( ) . toISOString ( ) ,
300+ errorMessage :
301+ 'Client did not send a tools/call request for test_custom_headers_null to test null/omitted parameter handling.' ,
302+ specReferences : [ SPEC_REFERENCE_CUSTOM ]
303+ } ) ;
304+ }
277305 return this . checks ;
278306 }
279307
@@ -339,6 +367,24 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
339367 description : 'Floating point numeric value' ,
340368 'x-mcp-header' : 'FloatVal'
341369 } ,
370+ non_ascii_val : {
371+ type : 'string' ,
372+ description :
373+ 'Non-ASCII string value — requires Base64 encoding' ,
374+ 'x-mcp-header' : 'NonAscii'
375+ } ,
376+ whitespace_val : {
377+ type : 'string' ,
378+ description :
379+ 'String with leading/trailing whitespace — requires Base64 encoding' ,
380+ 'x-mcp-header' : 'Whitespace'
381+ } ,
382+ control_char_val : {
383+ type : 'string' ,
384+ description :
385+ 'String with control characters — requires Base64 encoding' ,
386+ 'x-mcp-header' : 'ControlChar'
387+ } ,
342388 query : {
343389 type : 'string' ,
344390 description :
@@ -347,6 +393,36 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
347393 } ,
348394 required : [ 'region' , 'priority' , 'query' ]
349395 }
396+ } ,
397+ {
398+ name : 'test_custom_headers_null' ,
399+ description :
400+ 'A tool for testing null/omitted x-mcp-header parameter handling' ,
401+ inputSchema : {
402+ type : 'object' ,
403+ properties : {
404+ region : {
405+ type : 'string' ,
406+ description : 'Plain ASCII string value' ,
407+ 'x-mcp-header' : 'Region'
408+ } ,
409+ priority : {
410+ type : 'number' ,
411+ description : 'Integer numeric value' ,
412+ 'x-mcp-header' : 'Priority'
413+ } ,
414+ verbose : {
415+ type : 'boolean' ,
416+ description : 'Boolean value — will be null to test omission' ,
417+ 'x-mcp-header' : 'Verbose'
418+ } ,
419+ query : {
420+ type : 'string' ,
421+ description : 'No x-mcp-header annotation'
422+ }
423+ } ,
424+ required : [ 'region' , 'priority' , 'query' ]
425+ }
350426 }
351427 ]
352428 }
@@ -358,91 +434,120 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
358434 res : http . ServerResponse ,
359435 request : any
360436 ) : void {
361- this . toolCallReceived = true ;
437+ const toolName = request . params ?. name ;
362438 const args = request . params ?. arguments || { } ;
363439
364- // Check Mcp-Param-Region header (plain ASCII string)
365- this . checkParamHeader ( req , 'Region' , args . region , 'string' ) ;
440+ if ( toolName === 'test_custom_headers' ) {
441+ this . toolCallReceived = true ;
442+
443+ // Check Mcp-Param-Region header (plain ASCII string)
444+ this . checkParamHeader ( req , 'Region' , args . region , 'string' ) ;
445+
446+ // Check Mcp-Param-Priority header (integer number)
447+ this . checkParamHeader ( req , 'Priority' , args . priority , 'number' ) ;
448+
449+ // Check Mcp-Param-Verbose header (boolean value)
450+ if ( args . verbose !== undefined && args . verbose !== null ) {
451+ this . checkParamHeader ( req , 'Verbose' , args . verbose , 'boolean' ) ;
452+
453+ // Explicit check: optional parameter present → client MUST include header
454+ const verboseHeader = req . headers [ 'mcp-param-verbose' ] as
455+ | string
456+ | undefined ;
457+ this . checks . push ( {
458+ id : 'client-custom-header-optional-present' ,
459+ name : 'ClientCustomHeaderOptionalPresent' ,
460+ description :
461+ 'Client MUST include Mcp-Param header when optional parameter is provided' ,
462+ status : verboseHeader !== undefined ? 'SUCCESS' : 'FAILURE' ,
463+ timestamp : new Date ( ) . toISOString ( ) ,
464+ errorMessage :
465+ verboseHeader === undefined
466+ ? `Optional parameter 'verbose' was provided with value '${ args . verbose } ' but Mcp-Param-Verbose header is missing. Client MUST include the header when the parameter is present.`
467+ : undefined ,
468+ specReferences : [ SPEC_REFERENCE_CUSTOM ] ,
469+ details : {
470+ parameter : 'verbose' ,
471+ bodyValue : args . verbose ,
472+ headerPresent : verboseHeader !== undefined
473+ }
474+ } ) ;
475+ }
366476
367- // Check Mcp-Param-Priority header (integer number)
368- this . checkParamHeader ( req , 'Priority' , args . priority , 'number' ) ;
477+ // Check Mcp-Param-EmptyVal header (empty string → empty header value)
478+ if ( args . empty_val !== undefined && args . empty_val !== null ) {
479+ this . checkParamHeader ( req , 'EmptyVal' , args . empty_val , 'string' ) ;
480+ }
369481
370- // Check Mcp-Param-Verbose header (boolean value)
371- if ( args . verbose !== undefined && args . verbose !== null ) {
372- this . checkParamHeader ( req , 'Verbose' , args . verbose , 'boolean' ) ;
482+ // Check Mcp-Param-Method header (x-mcp-header "Method" → Mcp-Param-Method, NOT Mcp-Method)
483+ if ( args . method_val !== undefined && args . method_val !== null ) {
484+ this . checkParamHeader ( req , 'Method' , args . method_val , 'string' ) ;
485+ }
486+
487+ // Check Mcp-Param-FloatVal header (floating point number)
488+ if ( args . float_val !== undefined && args . float_val !== null ) {
489+ this . checkParamHeader ( req , 'FloatVal' , args . float_val , 'number' ) ;
490+ }
491+
492+ // Check Mcp-Param-NonAscii header (requires Base64 encoding)
493+ if ( args . non_ascii_val !== undefined && args . non_ascii_val !== null ) {
494+ this . checkParamHeader ( req , 'NonAscii' , args . non_ascii_val , 'string' ) ;
495+ }
496+
497+ // Check Mcp-Param-Whitespace header (leading/trailing whitespace → Base64)
498+ if ( args . whitespace_val !== undefined && args . whitespace_val !== null ) {
499+ this . checkParamHeader ( req , 'Whitespace' , args . whitespace_val , 'string' ) ;
500+ }
501+
502+ // Check Mcp-Param-ControlChar header (control characters → Base64)
503+ if (
504+ args . control_char_val !== undefined &&
505+ args . control_char_val !== null
506+ ) {
507+ this . checkParamHeader (
508+ req ,
509+ 'ControlChar' ,
510+ args . control_char_val ,
511+ 'string'
512+ ) ;
513+ }
514+
515+ // Check that 'query' (no x-mcp-header) is NOT mirrored
516+ const queryHeader = req . headers [ 'mcp-param-query' ] as string | undefined ;
517+ if ( queryHeader !== undefined ) {
518+ this . checks . push ( {
519+ id : 'client-custom-header-no-mirror-unannotated' ,
520+ name : 'ClientCustomHeaderNoMirrorUnannotated' ,
521+ description :
522+ 'Client MUST NOT add Mcp-Param headers for parameters without x-mcp-header' ,
523+ status : 'FAILURE' ,
524+ timestamp : new Date ( ) . toISOString ( ) ,
525+ errorMessage : `Found unexpected Mcp-Param-Query header '${ queryHeader } ' for unannotated parameter` ,
526+ specReferences : [ SPEC_REFERENCE_CUSTOM ]
527+ } ) ;
528+ }
529+ } else if ( toolName === 'test_custom_headers_null' ) {
530+ this . nullToolCallReceived = true ;
373531
374- // Explicit check: optional parameter present → client MUST include header
375- const verboseHeader = req . headers [ 'mcp-param-verbose' ] as
376- | string
377- | undefined ;
378- this . checks . push ( {
379- id : 'client-custom-header-optional-present' ,
380- name : 'ClientCustomHeaderOptionalPresent' ,
381- description :
382- 'Client MUST include Mcp-Param header when optional parameter is provided' ,
383- status : verboseHeader !== undefined ? 'SUCCESS' : 'FAILURE' ,
384- timestamp : new Date ( ) . toISOString ( ) ,
385- errorMessage :
386- verboseHeader === undefined
387- ? `Optional parameter 'verbose' was provided with value '${ args . verbose } ' but Mcp-Param-Verbose header is missing. Client MUST include the header when the parameter is present.`
388- : undefined ,
389- specReferences : [ SPEC_REFERENCE_CUSTOM ] ,
390- details : {
391- parameter : 'verbose' ,
392- bodyValue : args . verbose ,
393- headerPresent : verboseHeader !== undefined
394- }
395- } ) ;
396- } else {
397532 // When value is null or not provided, client MUST omit the header
398- const headerValue = req . headers [ 'mcp-param-verbose' ] as
533+ const verboseHeader = req . headers [ 'mcp-param-verbose' ] as
399534 | string
400535 | undefined ;
401536 this . checks . push ( {
402537 id : 'client-custom-header-omit-null' ,
403538 name : 'ClientCustomHeaderOmitNull' ,
404539 description :
405540 'Client MUST omit Mcp-Param header when parameter value is null or not provided' ,
406- status : headerValue === undefined ? 'SUCCESS' : 'FAILURE' ,
541+ status : verboseHeader === undefined ? 'SUCCESS' : 'FAILURE' ,
407542 timestamp : new Date ( ) . toISOString ( ) ,
408543 errorMessage :
409- headerValue !== undefined
410- ? `Mcp-Param-Verbose should be omitted when null/undefined, but got '${ headerValue } '`
544+ verboseHeader !== undefined
545+ ? `Mcp-Param-Verbose should be omitted when null/undefined, but got '${ verboseHeader } '`
411546 : undefined ,
412547 specReferences : [ SPEC_REFERENCE_CUSTOM ]
413548 } ) ;
414549 }
415550
416- // Check Mcp-Param-EmptyVal header (empty string → empty header value)
417- if ( args . empty_val !== undefined && args . empty_val !== null ) {
418- this . checkParamHeader ( req , 'EmptyVal' , args . empty_val , 'string' ) ;
419- }
420-
421- // Check Mcp-Param-Method header (x-mcp-header "Method" → Mcp-Param-Method, NOT Mcp-Method)
422- if ( args . method_val !== undefined && args . method_val !== null ) {
423- this . checkParamHeader ( req , 'Method' , args . method_val , 'string' ) ;
424- }
425-
426- // Check Mcp-Param-FloatVal header (floating point number)
427- if ( args . float_val !== undefined && args . float_val !== null ) {
428- this . checkParamHeader ( req , 'FloatVal' , args . float_val , 'number' ) ;
429- }
430-
431- // Check that 'query' (no x-mcp-header) is NOT mirrored
432- const queryHeader = req . headers [ 'mcp-param-query' ] as string | undefined ;
433- if ( queryHeader !== undefined ) {
434- this . checks . push ( {
435- id : 'client-custom-header-no-mirror-unannotated' ,
436- name : 'ClientCustomHeaderNoMirrorUnannotated' ,
437- description :
438- 'Client MUST NOT add Mcp-Param headers for parameters without x-mcp-header' ,
439- status : 'FAILURE' ,
440- timestamp : new Date ( ) . toISOString ( ) ,
441- errorMessage : `Found unexpected Mcp-Param-Query header '${ queryHeader } ' for unannotated parameter` ,
442- specReferences : [ SPEC_REFERENCE_CUSTOM ]
443- } ) ;
444- }
445-
446551 this . sendJson ( res , {
447552 jsonrpc : '2.0' ,
448553 id : request . id ,
@@ -540,6 +645,20 @@ export class HttpInvalidToolHeadersScenario extends BaseHttpScenario {
540645 } ) ;
541646 }
542647
648+ // Check that valid_tool WAS called — proves client kept valid tools
649+ const validToolCalled = this . calledTools . has ( 'valid_tool' ) ;
650+ this . checks . push ( {
651+ id : 'client-keeps-valid-tool' ,
652+ name : 'ClientKeepsValidTool' ,
653+ description : 'Client MUST keep valid tools while excluding invalid ones' ,
654+ status : validToolCalled ? 'SUCCESS' : 'FAILURE' ,
655+ timestamp : new Date ( ) . toISOString ( ) ,
656+ errorMessage : validToolCalled
657+ ? undefined
658+ : "Client did not call 'valid_tool'. A single malformed tool definition must not prevent other valid tools from being used." ,
659+ specReferences : [ SPEC_REFERENCE_TOOL_DEF ]
660+ } ) ;
661+
543662 // Check that the client did NOT call any of the invalid tools
544663 const invalidTools = [
545664 'invalid_empty_header' ,
0 commit comments