conformance/src/scenarios/client/http-custom-headers.ts
Felix Weinberger b9041ea41b
fix: include required result members in 2026-07-28 mock responses (#341)
At the 2026-07-28 revision every server result must include resultType,
and cacheable results (server/discover, tools/list, prompts/list,
resources/list, resources/templates/list, resources/read) must also
include ttlMs and cacheScope. The client-direction mock servers omitted
these, so a strictly-validating client was failed by the suite's own
non-conformant mock.

The shared stateless mock now stamps the missing members onto handler
results and its server/discover response (withRequiredDraftResultFields),
and the hand-rolled per-scenario servers set them explicitly or route
their fallbacks through the helper. The deliberately resultType-less
probe in sep-2322-client-request-state is unchanged. New tests pin the
response shapes.
2026-06-16 15:13:23 +01:00

953 lines
34 KiB
TypeScript

import type { ScenarioContext } from '../../mock-server';
/**
* HTTP Custom Headers conformance test scenario for MCP clients (SEP-2243)
*
* Tests that clients correctly handle the `x-mcp-header` extension property:
* 1. Mirror annotated tool parameter values into `Mcp-Param-{Name}` headers
* 2. Apply correct value encoding (plain ASCII, Base64 for non-ASCII)
* 3. Reject tool definitions with invalid `x-mcp-header` annotations
*
* This is a Scenario (acts as a test server that inspects incoming requests
* from the client under test).
*/
import http from 'http';
import { ScenarioUrls, ConformanceCheck } from '../../types.js';
import { BaseHttpScenario } from './http-base.js';
const SPEC_REFERENCE_CUSTOM = {
id: 'SEP-2243-Custom-Headers',
url: 'https://modelcontextprotocol.io/specification/draft/basic/transports#custom-headers-from-tool-parameters'
};
const SPEC_REFERENCE_ENCODING = {
id: 'SEP-2243-Value-Encoding',
url: 'https://modelcontextprotocol.io/specification/draft/basic/transports#value-encoding'
};
const SPEC_REFERENCE_TOOL_DEF = {
id: 'SEP-2243-x-mcp-header',
url: 'https://modelcontextprotocol.io/specification/draft/server/tools#x-mcp-header'
};
/**
* Every requirement-level check ID HttpCustomHeadersScenario can emit
* (SEP-2243). Declared-but-unemitted checks are backfilled as FAILURE by
* getChecks() — same pattern as request-metadata.ts — so the emitted ID set
* is stable for traceability even when the client never exercises the
* feature.
*/
export const CUSTOM_HEADERS_DECLARED_CHECK_IDS = [
'sep-2243-client-supports-custom-headers',
'sep-2243-client-mirrors-designated-params',
'sep-2243-client-encode-values',
'sep-2243-client-base64-unsafe',
'sep-2243-client-omit-null'
] as const;
/**
* Every requirement-level check ID HttpInvalidToolHeadersScenario can emit
* (SEP-2243).
*/
export const INVALID_TOOL_DECLARED_CHECK_IDS = [
'sep-2243-client-reject-invalid-tool',
'sep-2243-x-mcp-header-not-empty',
'sep-2243-x-mcp-header-charset',
'sep-2243-x-mcp-header-unique',
'sep-2243-x-mcp-header-primitive-only'
] as const;
/**
* Invalid tool definitions served by HttpInvalidToolHeadersScenario, keyed by
* tool name, mapped to the SEP-2243 x-mcp-header constraint each one
* violates. The check for "client did not call this tool" is emitted under
* the constraint's requirement ID so each constraint traces to a check.
*/
const INVALID_TOOL_CONSTRAINT_IDS: Record<string, string> = {
invalid_empty_header: 'sep-2243-x-mcp-header-not-empty',
invalid_object_header: 'sep-2243-x-mcp-header-primitive-only',
invalid_array_header: 'sep-2243-x-mcp-header-primitive-only',
invalid_null_header: 'sep-2243-x-mcp-header-primitive-only',
invalid_duplicate_same_case: 'sep-2243-x-mcp-header-unique',
invalid_duplicate_diff_case: 'sep-2243-x-mcp-header-unique',
invalid_space_in_name: 'sep-2243-x-mcp-header-charset',
invalid_colon_in_name: 'sep-2243-x-mcp-header-charset',
invalid_non_ascii_name: 'sep-2243-x-mcp-header-charset',
invalid_control_char_name: 'sep-2243-x-mcp-header-charset'
};
/**
* Decodes a header value that may be Base64-encoded.
* Base64-encoded values use the format: =?base64?{Base64EncodedValue}?=
*/
function decodeHeaderValue(value: string): string {
const base64Match = value.match(/^=\?base64\?(.*)\?=$/);
if (base64Match) {
return Buffer.from(base64Match[1], 'base64').toString('utf-8');
}
return value;
}
/**
* Check if a value needs Base64 encoding per the spec:
* - Non-ASCII characters
* - Control characters
* - Leading/trailing whitespace
*/
function needsBase64Encoding(value: string): boolean {
// Check for non-ASCII or control characters
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
if (code < 0x20 || code > 0x7e) {
// Allow space (0x20) and tab (0x09) only inside values, not at edges
if (code === 0x09) return true; // tab always needs encoding
if (code < 0x20) return true; // other control chars
if (code > 0x7e) return true; // non-ASCII
}
}
// Check for leading/trailing whitespace
if (value !== value.trim()) return true;
return false;
}
/**
* Checks if a raw header value is properly encoded for a body value that
* needs Base64 encoding. Returns null if valid, error string if invalid.
*/
function validateEncodedHeader(
rawHeader: string,
bodyValue: string,
valueType?: string
): string | null {
if (needsBase64Encoding(bodyValue)) {
// Value requires Base64 encoding
const base64Match = rawHeader.match(/^=\?base64\?(.*)\?=$/);
if (!base64Match) {
return `Value '${bodyValue}' requires Base64 encoding but header was sent as plain: '${rawHeader}'`;
}
const decoded = Buffer.from(base64Match[1], 'base64').toString('utf-8');
if (valueType === 'number') {
return compareNumericValues(decoded, bodyValue);
}
if (decoded !== bodyValue) {
return `Base64-decoded header value '${decoded}' does not match body value '${bodyValue}'`;
}
return null;
}
// Plain ASCII - compare directly (after decoding if Base64 was used)
const decoded = decodeHeaderValue(rawHeader);
if (valueType === 'number') {
return compareNumericValues(decoded, bodyValue);
}
if (decoded !== bodyValue) {
return `Header value '${decoded}' (raw: '${rawHeader}') does not match body value '${bodyValue}'`;
}
return null;
}
/**
* Compare two string representations of numbers numerically.
* For integers, requires exact match. For decimals, allows
* a tolerance of 1e-9 to account for cross-SDK floating point
* representation differences.
*/
function compareNumericValues(
headerValue: string,
bodyValue: string
): string | null {
const headerNum = Number(headerValue);
const bodyNum = Number(bodyValue);
if (isNaN(headerNum) || isNaN(bodyNum)) {
return `Non-numeric value in number comparison: header='${headerValue}', body='${bodyValue}'`;
}
if (Number.isInteger(bodyNum)) {
// Integer: require exact numeric match (e.g. 42 === 42.0)
if (headerNum !== bodyNum) {
return `Numeric header value ${headerNum} does not match body value ${bodyNum}`;
}
} else {
// Decimal: allow tolerance of 1e-9
if (Math.abs(headerNum - bodyNum) > 1e-9) {
return `Numeric header value ${headerNum} does not match body value ${bodyNum} (difference ${Math.abs(headerNum - bodyNum)} exceeds tolerance 1e-9)`;
}
}
return null;
}
// ─────────────────────────────────────────────────────────────────────────────
// HttpCustomHeadersScenario - tests that clients mirror x-mcp-header params
// ─────────────────────────────────────────────────────────────────────────────
export class HttpCustomHeadersScenario extends BaseHttpScenario {
name = 'http-custom-headers';
description =
'Tests that client mirrors x-mcp-header tool parameters into Mcp-Param headers with correct encoding (SEP-2243)';
private toolCallReceived: boolean = false;
private nullToolCallReceived: boolean = false;
async start(_ctx: ScenarioContext): Promise<ScenarioUrls> {
const urls = await super.start(_ctx);
// Pass test values via context for encoding edge cases.
// The conformance client should use these values when calling test_custom_headers.
urls.context = {
toolCalls: [
{
name: 'test_custom_headers',
arguments: {
region: 'us-west1',
priority: 42,
verbose: false,
debug: true,
empty_val: '',
method_val: 'test-method',
float_val: 3.14159,
non_ascii_val: 'Hello, 世界',
whitespace_val: ' padded ',
leading_space_val: ' us-west1',
trailing_space_val: 'us-west1 ',
internal_space_val: 'us west 1',
control_char_val: 'line1\nline2',
crlf_val: 'line1\r\nline2',
tab_val: '\tindented',
query: 'SELECT * FROM users'
}
},
{
name: 'test_custom_headers_null',
arguments: {
region: 'us-east1',
priority: 1,
verbose: null,
query: 'SELECT 1'
}
}
]
};
return urls;
}
getChecks(): ConformanceCheck[] {
// Declared-but-unemitted -> FAILURE (request-metadata.ts pattern). Keeps
// the emitted ID set stable for traceability even when the client never
// calls the annotated tools. The `some()` guard makes this idempotent.
for (const id of CUSTOM_HEADERS_DECLARED_CHECK_IDS) {
if (this.checks.some((c) => c.id === id)) continue;
const missingNullCall =
id === 'sep-2243-client-omit-null' && !this.nullToolCallReceived;
this.checks.push({
id,
name: 'NotObserved',
description: `Declared check ${id} was never emitted`,
status: 'FAILURE',
timestamp: new Date().toISOString(),
errorMessage: missingNullCall
? 'Client did not send a tools/call request for test_custom_headers_null to test null/omitted parameter handling.'
: this.toolCallReceived
? 'Check was not observed: no tool call exercised it.'
: 'Client did not send a tools/call request for test_custom_headers.',
specReferences: [SPEC_REFERENCE_CUSTOM]
});
}
return this.checks;
}
protected handlePost(
req: http.IncomingMessage,
res: http.ServerResponse,
request: any
): void {
if (request.method === 'initialize') {
this.sendInitialize(res, request);
} else if (request.method === 'tools/list') {
this.handleToolsList(res, request);
} else if (request.method === 'tools/call') {
this.handleToolsCall(req, res, request);
} else if (request.id === undefined) {
this.sendNotificationAck(res);
} else {
this.sendGenericResult(res, request);
}
}
private handleToolsList(res: http.ServerResponse, request: any): void {
this.sendJson(res, {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private',
tools: [
{
name: 'test_custom_headers',
description:
'A tool with x-mcp-header annotations to test custom header mirroring and encoding',
inputSchema: {
type: 'object',
properties: {
region: {
type: 'string',
description: 'Plain ASCII string value',
'x-mcp-header': 'Region'
},
priority: {
type: 'number',
description: 'Integer numeric value',
'x-mcp-header': 'Priority'
},
verbose: {
type: 'boolean',
description: 'Boolean value',
'x-mcp-header': 'Verbose'
},
debug: {
type: 'boolean',
description: 'Boolean true value',
'x-mcp-header': 'Debug'
},
empty_val: {
type: 'string',
description: 'Empty string value',
'x-mcp-header': 'EmptyVal'
},
method_val: {
type: 'string',
description:
'Value for header named "Method" — tests that x-mcp-header "Method" produces Mcp-Param-Method (not Mcp-Method)',
'x-mcp-header': 'Method'
},
float_val: {
type: 'number',
description: 'Floating point numeric value',
'x-mcp-header': 'FloatVal'
},
non_ascii_val: {
type: 'string',
description:
'Non-ASCII string value — requires Base64 encoding',
'x-mcp-header': 'NonAscii'
},
whitespace_val: {
type: 'string',
description:
'String with leading/trailing whitespace — requires Base64 encoding',
'x-mcp-header': 'Whitespace'
},
leading_space_val: {
type: 'string',
description:
'String with leading space only — requires Base64 encoding',
'x-mcp-header': 'LeadingSpace'
},
trailing_space_val: {
type: 'string',
description:
'String with trailing space only — requires Base64 encoding',
'x-mcp-header': 'TrailingSpace'
},
internal_space_val: {
type: 'string',
description:
'String with internal spaces only — plain ASCII, no Base64',
'x-mcp-header': 'InternalSpace'
},
control_char_val: {
type: 'string',
description:
'String with control characters — requires Base64 encoding',
'x-mcp-header': 'ControlChar'
},
crlf_val: {
type: 'string',
description:
'String with carriage return and line feed — requires Base64 encoding',
'x-mcp-header': 'CrLf'
},
tab_val: {
type: 'string',
description:
'String with leading tab — requires Base64 encoding',
'x-mcp-header': 'Tab'
},
query: {
type: 'string',
description:
'No x-mcp-header annotation - should not be mirrored'
}
},
required: ['region', 'priority', 'query']
}
},
{
name: 'test_custom_headers_null',
description:
'A tool for testing null/omitted x-mcp-header parameter handling',
inputSchema: {
type: 'object',
properties: {
region: {
type: 'string',
description: 'Plain ASCII string value',
'x-mcp-header': 'Region'
},
priority: {
type: 'number',
description: 'Integer numeric value',
'x-mcp-header': 'Priority'
},
verbose: {
type: 'boolean',
description: 'Boolean value — will be null to test omission',
'x-mcp-header': 'Verbose'
},
query: {
type: 'string',
description: 'No x-mcp-header annotation'
}
},
required: ['region', 'priority', 'query']
}
}
]
}
});
}
private handleToolsCall(
req: http.IncomingMessage,
res: http.ServerResponse,
request: any
): void {
const toolName = request.params?.name;
const args = request.params?.arguments || {};
if (toolName === 'test_custom_headers') {
this.toolCallReceived = true;
// SEP-2243 "MCP clients MUST support this feature": observable as the
// client calling the annotated tool with at least one mirrored
// Mcp-Param-* header. The per-parameter checks below then validate each
// mirrored value individually.
const hasAnyParamHeader = Object.keys(req.headers).some((h) =>
h.startsWith('mcp-param-')
);
this.checks.push({
id: 'sep-2243-client-supports-custom-headers',
name: 'ClientSupportsCustomHeaders',
description:
'Client supports custom headers: calls the x-mcp-header annotated tool and mirrors at least one parameter',
status: hasAnyParamHeader ? 'SUCCESS' : 'FAILURE',
timestamp: new Date().toISOString(),
errorMessage: hasAnyParamHeader
? undefined
: 'Client called test_custom_headers but sent no Mcp-Param-* headers.',
specReferences: [SPEC_REFERENCE_CUSTOM]
});
// Check Mcp-Param-Region header (plain ASCII string)
this.checkParamHeader(req, 'Region', args.region, 'string');
// Check Mcp-Param-Priority header (integer number)
this.checkParamHeader(req, 'Priority', args.priority, 'number');
// Check Mcp-Param-Verbose header (boolean value)
// checkParamHeader already FAILs on missing header, so this also covers
// "optional parameter present → client MUST include header" without a
// separate check id.
if (args.verbose !== undefined && args.verbose !== null) {
this.checkParamHeader(req, 'Verbose', args.verbose, 'boolean');
}
// Check Mcp-Param-Debug header (boolean true value)
if (args.debug !== undefined && args.debug !== null) {
this.checkParamHeader(req, 'Debug', args.debug, 'boolean');
}
// Check Mcp-Param-EmptyVal header (empty string → empty header value)
if (args.empty_val !== undefined && args.empty_val !== null) {
this.checkParamHeader(req, 'EmptyVal', args.empty_val, 'string');
}
// Check Mcp-Param-Method header (x-mcp-header "Method" → Mcp-Param-Method, NOT Mcp-Method)
if (args.method_val !== undefined && args.method_val !== null) {
this.checkParamHeader(req, 'Method', args.method_val, 'string');
}
// Check Mcp-Param-FloatVal header (floating point number)
if (args.float_val !== undefined && args.float_val !== null) {
this.checkParamHeader(req, 'FloatVal', args.float_val, 'number');
}
// Check Mcp-Param-NonAscii header (requires Base64 encoding)
if (args.non_ascii_val !== undefined && args.non_ascii_val !== null) {
this.checkParamHeader(req, 'NonAscii', args.non_ascii_val, 'string');
}
// Check Mcp-Param-Whitespace header (leading/trailing whitespace → Base64)
if (args.whitespace_val !== undefined && args.whitespace_val !== null) {
this.checkParamHeader(req, 'Whitespace', args.whitespace_val, 'string');
}
// Check Mcp-Param-LeadingSpace header (leading space only → Base64)
if (
args.leading_space_val !== undefined &&
args.leading_space_val !== null
) {
this.checkParamHeader(
req,
'LeadingSpace',
args.leading_space_val,
'string'
);
}
// Check Mcp-Param-TrailingSpace header (trailing space only → Base64)
if (
args.trailing_space_val !== undefined &&
args.trailing_space_val !== null
) {
this.checkParamHeader(
req,
'TrailingSpace',
args.trailing_space_val,
'string'
);
}
// Check Mcp-Param-InternalSpace header (internal spaces only → plain ASCII, no Base64)
if (
args.internal_space_val !== undefined &&
args.internal_space_val !== null
) {
this.checkParamHeader(
req,
'InternalSpace',
args.internal_space_val,
'string'
);
}
// Check Mcp-Param-ControlChar header (control characters → Base64)
if (
args.control_char_val !== undefined &&
args.control_char_val !== null
) {
this.checkParamHeader(
req,
'ControlChar',
args.control_char_val,
'string'
);
}
// Check Mcp-Param-CrLf header (carriage return + line feed → Base64)
if (args.crlf_val !== undefined && args.crlf_val !== null) {
this.checkParamHeader(req, 'CrLf', args.crlf_val, 'string');
}
// Check Mcp-Param-Tab header (leading tab → Base64)
if (args.tab_val !== undefined && args.tab_val !== null) {
this.checkParamHeader(req, 'Tab', args.tab_val, 'string');
}
// Check that 'query' (no x-mcp-header) is NOT mirrored — the negative
// half of "mirror the *designated* parameter values".
const queryHeader = req.headers['mcp-param-query'] as string | undefined;
this.checks.push({
id: 'sep-2243-client-mirrors-designated-params',
name: 'ClientCustomHeaderNoMirrorUnannotated',
description:
'Client MUST NOT add Mcp-Param headers for parameters without x-mcp-header',
status: queryHeader === undefined ? 'SUCCESS' : 'FAILURE',
timestamp: new Date().toISOString(),
errorMessage:
queryHeader !== undefined
? `Found unexpected Mcp-Param-Query header '${queryHeader}' for unannotated parameter`
: undefined,
specReferences: [SPEC_REFERENCE_CUSTOM]
});
} else if (toolName === 'test_custom_headers_null') {
this.nullToolCallReceived = true;
// When value is null or not provided, client MUST omit the header
const verboseHeader = req.headers['mcp-param-verbose'] as
| string
| undefined;
this.checks.push({
id: 'sep-2243-client-omit-null',
name: 'ClientCustomHeaderOmitNull',
description:
'Client MUST omit Mcp-Param header when parameter value is null or not provided',
status: verboseHeader === undefined ? 'SUCCESS' : 'FAILURE',
timestamp: new Date().toISOString(),
errorMessage:
verboseHeader !== undefined
? `Mcp-Param-Verbose should be omitted when null/undefined, but got '${verboseHeader}'`
: undefined,
specReferences: [SPEC_REFERENCE_CUSTOM]
});
}
this.sendJson(res, {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
content: [{ type: 'text', text: 'Custom headers test completed' }]
}
});
}
private checkParamHeader(
req: http.IncomingMessage,
headerName: string,
bodyValue: any,
valueType: string
): void {
const headerKey = `mcp-param-${headerName.toLowerCase()}`;
const rawHeaderValue = req.headers[headerKey] as string | undefined;
if (bodyValue === undefined || bodyValue === null) return;
const errors: string[] = [];
if (rawHeaderValue === undefined) {
errors.push(
`Missing Mcp-Param-${headerName} header. Client MUST include headers for x-mcp-header parameters.`
);
} else {
// Convert body value to expected string representation
let expectedString: string;
switch (valueType) {
case 'number':
expectedString = String(bodyValue);
break;
case 'boolean':
expectedString = bodyValue ? 'true' : 'false';
break;
default:
expectedString = String(bodyValue);
}
// For numbers, compare numerically to allow for cross-SDK
// floating point representation differences (e.g., "42" vs "42.0").
// See SEP-2243 discussion on number precision.
const validationError = validateEncodedHeader(
rawHeaderValue,
expectedString,
valueType
);
if (validationError) {
errors.push(validationError);
}
}
// One check ID per SEP-2243 requirement, picked by what this parameter
// actually exercises: Base64 wrapping for unsafe values, primitive
// (number/boolean) string encoding, or plain mirroring. The per-parameter
// detail lives in `name`/`details`.
const needsBase64 =
typeof bodyValue === 'string' && needsBase64Encoding(String(bodyValue));
const checkId = needsBase64
? 'sep-2243-client-base64-unsafe'
: valueType === 'number' || valueType === 'boolean'
? 'sep-2243-client-encode-values'
: 'sep-2243-client-mirrors-designated-params';
this.checks.push({
id: checkId,
name: `ClientCustomHeader_${headerName}`,
description: `Client sends correct Mcp-Param-${headerName} header (${valueType} value)`,
status: errors.length === 0 ? 'SUCCESS' : 'FAILURE',
timestamp: new Date().toISOString(),
errorMessage: errors.length > 0 ? errors.join('; ') : undefined,
specReferences: [SPEC_REFERENCE_ENCODING],
details: {
headerName: `Mcp-Param-${headerName}`,
rawHeaderValue,
bodyValue,
valueType,
needsBase64
}
});
}
}
// ─────────────────────────────────────────────────────────────────────────────
// HttpInvalidToolHeadersScenario - tests that clients reject invalid tools
// ─────────────────────────────────────────────────────────────────────────────
export class HttpInvalidToolHeadersScenario extends BaseHttpScenario {
name = 'http-invalid-tool-headers';
description =
'Tests that client rejects tools with invalid x-mcp-header annotations (SEP-2243)';
allowClientError = true;
private calledTools: Set<string> = new Set();
private toolsListSent = false;
getChecks(): ConformanceCheck[] {
if (!this.toolsListSent) {
this.checks.push({
id: 'sep-2243-invalid-tool-tools-list-gate',
name: 'ClientInvalidToolHeadersToolsList',
description: 'Client requests tools/list',
status: 'FAILURE',
timestamp: new Date().toISOString(),
errorMessage: 'Client did not send a tools/list request.',
specReferences: [SPEC_REFERENCE_TOOL_DEF]
});
}
// Check that valid_tool WAS called — proves client kept valid tools
const validToolCalled = this.calledTools.has('valid_tool');
this.checks.push({
id: 'sep-2243-client-reject-invalid-tool',
name: 'ClientKeepsValidTool',
description: 'Client MUST keep valid tools while excluding invalid ones',
status: validToolCalled ? 'SUCCESS' : 'FAILURE',
timestamp: new Date().toISOString(),
errorMessage: validToolCalled
? undefined
: "Client did not call 'valid_tool'. A single malformed tool definition must not prevent other valid tools from being used.",
specReferences: [SPEC_REFERENCE_TOOL_DEF]
});
// Check that the client did NOT call any of the invalid tools. Each
// invalid tool violates one specific x-mcp-header constraint, so the
// check is emitted under that constraint's requirement ID — the client
// rejecting the tool is how the constraint is enforced on the wire.
for (const [toolName, constraintId] of Object.entries(
INVALID_TOOL_CONSTRAINT_IDS
)) {
const called = this.calledTools.has(toolName);
this.checks.push({
id: constraintId,
name: `ClientRejectsInvalidTool_${toolName}`,
description: `Client MUST NOT call tool '${toolName}' with invalid x-mcp-header`,
status: called ? 'FAILURE' : 'SUCCESS',
timestamp: new Date().toISOString(),
errorMessage: called
? `Client called '${toolName}' which has an invalid x-mcp-header. Clients MUST reject (exclude) such tools.`
: undefined,
specReferences: [SPEC_REFERENCE_TOOL_DEF]
});
}
return this.checks;
}
protected handlePost(
_req: http.IncomingMessage,
res: http.ServerResponse,
request: any
): void {
if (request.method === 'initialize') {
this.sendInitialize(res, request);
} else if (request.method === 'tools/list') {
this.handleToolsList(res, request);
} else if (request.method === 'tools/call') {
this.handleToolsCall(res, request);
} else if (request.id === undefined) {
this.sendNotificationAck(res);
} else {
this.sendGenericResult(res, request);
}
}
private handleToolsList(res: http.ServerResponse, request: any): void {
this.toolsListSent = true;
this.sendJson(res, {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private',
tools: [
// ── Valid tool (should be kept by client) ──
{
name: 'valid_tool',
description: 'A valid tool with correct x-mcp-header',
inputSchema: {
type: 'object',
properties: {
region: {
type: 'string',
'x-mcp-header': 'Region'
}
},
required: ['region']
}
},
// ── Invalid: empty x-mcp-header value ──
{
name: 'invalid_empty_header',
description:
'x-mcp-header MUST NOT be empty (MUST be rejected by client)',
inputSchema: {
type: 'object',
properties: {
value: { type: 'string', 'x-mcp-header': '' }
},
required: ['value']
}
},
// ── Invalid: x-mcp-header on object type ──
{
name: 'invalid_object_header',
description:
'x-mcp-header MUST only be on primitive types (MUST be rejected)',
inputSchema: {
type: 'object',
properties: {
data: { type: 'object', 'x-mcp-header': 'Data' }
},
required: ['data']
}
},
// ── Invalid: x-mcp-header on array type ──
{
name: 'invalid_array_header',
description:
'x-mcp-header MUST only be on primitive types (MUST be rejected)',
inputSchema: {
type: 'object',
properties: {
items: {
type: 'array',
items: { type: 'string' },
'x-mcp-header': 'Items'
}
},
required: ['items']
}
},
// ── Invalid: x-mcp-header on null type ──
{
name: 'invalid_null_header',
description:
'x-mcp-header MUST only be on primitive types (MUST be rejected)',
inputSchema: {
type: 'object',
properties: {
nil: { type: 'null', 'x-mcp-header': 'Nil' }
},
required: ['nil']
}
},
// ── Invalid: duplicate same-case x-mcp-header values ──
{
name: 'invalid_duplicate_same_case',
description:
'Duplicate x-mcp-header "Region" on two properties (MUST be rejected)',
inputSchema: {
type: 'object',
properties: {
field1: { type: 'string', 'x-mcp-header': 'Region' },
field2: { type: 'string', 'x-mcp-header': 'Region' }
},
required: ['field1', 'field2']
}
},
// ── Invalid: duplicate case-insensitive x-mcp-header values ──
{
name: 'invalid_duplicate_diff_case',
description:
'Duplicate case-insensitive x-mcp-header "MyField"/"myfield" (MUST be rejected)',
inputSchema: {
type: 'object',
properties: {
field1: { type: 'string', 'x-mcp-header': 'MyField' },
field2: { type: 'string', 'x-mcp-header': 'myfield' }
},
required: ['field1', 'field2']
}
},
// ── Invalid: space in x-mcp-header name ──
{
name: 'invalid_space_in_name',
description:
'x-mcp-header MUST NOT contain space (MUST be rejected)',
inputSchema: {
type: 'object',
properties: {
value: { type: 'string', 'x-mcp-header': 'My Region' }
},
required: ['value']
}
},
// ── Invalid: colon in x-mcp-header name ──
{
name: 'invalid_colon_in_name',
description:
'x-mcp-header MUST NOT contain colon (MUST be rejected)',
inputSchema: {
type: 'object',
properties: {
value: {
type: 'string',
'x-mcp-header': 'Region:Primary'
}
},
required: ['value']
}
},
// ── Invalid: non-ASCII in x-mcp-header name ──
{
name: 'invalid_non_ascii_name',
description:
'x-mcp-header MUST contain only ASCII chars (MUST be rejected)',
inputSchema: {
type: 'object',
properties: {
value: { type: 'string', 'x-mcp-header': 'Région' }
},
required: ['value']
}
},
// ── Invalid: control character in x-mcp-header name ──
{
name: 'invalid_control_char_name',
description:
'x-mcp-header MUST NOT contain control chars (MUST be rejected)',
inputSchema: {
type: 'object',
properties: {
value: { type: 'string', 'x-mcp-header': 'Region\t1' }
},
required: ['value']
}
}
]
}
});
}
private handleToolsCall(res: http.ServerResponse, request: any): void {
const toolName = request.params?.name;
if (toolName) this.calledTools.add(toolName);
this.sendJson(res, {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
content: [{ type: 'text', text: 'Tool call received' }]
}
});
}
}