Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions examples/servers/typescript/custom-header-presence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { createServer, type IncomingHttpHeaders } from 'http';

export type HeaderPresenceBehavior =
| 'correct'
| 'empty-is-absent'
| 'absent-is-empty';

function decodeHeader(value: string): string | undefined {
if (!value.startsWith('=?base64?') || !value.endsWith('?=')) return value;
const encoded = value.slice('=?base64?'.length, -2);
const decoded = Buffer.from(encoded, 'base64');
return decoded.toString('base64') === encoded
? decoded.toString('utf8')
: undefined;
}

/**
* A narrow HTTP fixture, not a complete MCP server. The two broken modes
* deliberately conflate missing and empty headers in opposite directions.
*/
export function createCustomHeaderPresenceServer(
behavior: HeaderPresenceBehavior = 'correct',
allParamsRequired = false
) {
const properties = {
required_region: { type: 'string', 'x-mcp-header': 'RequiredRegion' },
region: { type: 'string', 'x-mcp-header': 'Region' }
};
const requests: Array<{ body: any; headers: IncomingHttpHeaders }> = [];
const server = createServer(async (req, res) => {
let body: any;
try {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(Buffer.from(chunk));
body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
} catch {
res.writeHead(400).end();
return;
}
requests.push({ body, headers: req.headers });

const reply = (status: number, payload: object) => {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ jsonrpc: '2.0', id: body.id, ...payload }));
};
if (body.method === 'tools/list') {
reply(200, {
result: {
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private',
tools: [
{
name: 'test_header_presence',
inputSchema: {
type: 'object',
properties,
required: allParamsRequired
? ['required_region', 'region']
: ['required_region']
}
}
]
}
});
return;
}
if (body.method !== 'tools/call') {
reply(404, { error: { code: -32601, message: 'Method not found' } });
return;
}

const args = body.params?.arguments ?? {};
for (const [param, schema] of Object.entries(properties)) {
const header = req.headers[
`mcp-param-${schema['x-mcp-header'].toLowerCase()}`
] as string | undefined;
const present =
behavior === 'empty-is-absent' ? Boolean(header) : header !== undefined;
const value = args[param];
const mismatch =
value === undefined || value === null
? present
: (!present && behavior !== 'absent-is-empty') ||
decodeHeader(header ?? '') !== value;
if (mismatch) {
reply(400, { error: { code: -32020, message: 'Header mismatch' } });
return;
}
}
reply(200, {
result: {
resultType: 'complete',
content: [{ type: 'text', text: 'ok' }]
}
});
});
return { server, requests };
}
117 changes: 117 additions & 0 deletions src/scenarios/server/http-standard-headers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {
CUSTOM_HEADER_SERVER_DECLARED_CHECK_IDS
} from './http-standard-headers';
import type { ConformanceCheck } from '../../types';
import {
createCustomHeaderPresenceServer,
type HeaderPresenceBehavior
} from '../../../examples/servers/typescript/custom-header-presence';

/**
* Pins the untestable-failure policy (issue #248) for the SEP-2243 server
Expand Down Expand Up @@ -46,6 +50,119 @@ function mockFetchTarget(
const findAll = (checks: ConformanceCheck[], id: string) =>
checks.filter((c) => c.id === id);

describe('http-custom-header-server-validation - empty versus absent', () => {
const caseNames = [
'ServerRejectsMissingEmptyCustomHeader',
'ServerRejectsEmptyHeaderForOmittedParam',
'ServerRejectsEmptyHeaderForNullParam'
];

async function runPresenceScenario(
behavior: HeaderPresenceBehavior,
allParamsRequired = false
) {
const fixture = createCustomHeaderPresenceServer(
behavior,
allParamsRequired
);
await new Promise<void>((resolve) =>
fixture.server.listen(0, '127.0.0.1', resolve)
);
const address = fixture.server.address();
if (!address || typeof address === 'string') {
throw new Error('Fixture did not listen on a TCP port');
}
try {
const checks = await new HttpCustomHeaderServerValidationScenario().run(
testContext(`http://127.0.0.1:${address.port}/mcp`)
);
return { checks, requests: fixture.requests };
} finally {
fixture.server.closeAllConnections();
await new Promise<void>((resolve, reject) =>
fixture.server.close((error) => (error ? reject(error) : resolve()))
);
}
}

test('rejects all three mismatches and preserves required parameters', async () => {
const { checks, requests } = await runPresenceScenario('correct');
for (const name of caseNames) {
for (const checkName of [name, `${name}ErrorCode`]) {
expect(
checks.find((c) => c.name === checkName)?.status,
checkName
).toBe('SUCCESS');
}
}
expect(checks.every((c) => c.status === 'SUCCESS')).toBe(true);

const emptyHeaders = requests.filter(
(r) => r.headers['mcp-param-region'] === ''
);
expect(emptyHeaders).toHaveLength(2);
expect(emptyHeaders[0].body.params.arguments).not.toHaveProperty('region');
expect(emptyHeaders[1].body.params.arguments).toHaveProperty(
'region',
null
);
for (const request of emptyHeaders) {
expect(request.body.params.arguments.required_region).toBe(
'test-default'
);
expect(request.headers['mcp-param-requiredregion']).toBe('test-default');
}
const missingHeader = requests.find(
(r) => r.body.params?.arguments?.region === ''
);
expect(missingHeader).toBeDefined();
expect(missingHeader?.headers).not.toHaveProperty('mcp-param-region');
});

test.each<{
behavior: HeaderPresenceBehavior;
failedCases: string[];
}>([
{
behavior: 'empty-is-absent',
failedCases: caseNames.slice(1)
},
{
behavior: 'absent-is-empty',
failedCases: caseNames.slice(0, 1)
}
])('detects $behavior behavior', async ({ behavior, failedCases }) => {
const { checks } = await runPresenceScenario(behavior);
expect(
checks.filter((c) => c.status === 'FAILURE').map((c) => c.name)
).toEqual(failedCases.flatMap((name) => [name, `${name}ErrorCode`]));
for (const name of caseNames.filter((n) => !failedCases.includes(n))) {
expect(checks.find((c) => c.name === name)?.status, name).toBe('SUCCESS');
}
});

test('marks omitted/null probes untestable without an optional string parameter', async () => {
const { checks } = await runPresenceScenario('correct', true);
expect(
checks.find((c) => c.name === 'ServerRejectsMissingEmptyCustomHeader')
?.status
).toBe('SUCCESS');
for (const name of caseNames.slice(1)) {
expect(
checks.find((c) => c.name === name),
name
).toMatchObject({
status: 'FAILURE',
details: { untestable: true },
errorMessage: expect.stringContaining(
'optional string-typed x-mcp-header'
)
});
}
expect(checks.some((c) => c.status === 'SKIPPED')).toBe(false);
});
});

describe('http-custom-header-server-validation — missing fixture policy', () => {
test('emits untestable FAILUREs for every declared check when no x-mcp-header tool exists', async () => {
const mockUrl = mockFetchTarget((reqBody) => {
Expand Down
Loading