Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c6ec829
client/http-standard-headers: SKIPPED (not FAILURE) for unexercised m…
pcarleton May 14, 2026
7c71145
client/http-standard-headers: de-dup guard in checkMcpNameHeader
pcarleton May 14, 2026
15bf72e
client/http-standard-headers: replace -> /\//g for slug generation
pcarleton May 14, 2026
8bf9b2e
client/http-custom-headers: base64 regex (.+) -> (.*) so empty string…
pcarleton May 14, 2026
94ea27f
client/http-custom-headers: emit SUCCESS for no-mirror-unannotated
pcarleton May 14, 2026
d700b7c
client/http-custom-headers: drop redundant optional-present check
pcarleton May 14, 2026
216ad98
server/http-standard-headers: defaultArgs must use real number/boolea…
pcarleton May 14, 2026
100a824
server/http-standard-headers: createAcceptanceCheck must also assert …
pcarleton May 14, 2026
3356974
use DRAFT_PROTOCOL_VERSION constant instead of 'DRAFT-2026-v1' literal
pcarleton May 14, 2026
16d24c8
sep-2243.yaml: spec-first rewrite + move to src/seps/
pcarleton May 14, 2026
cb482e8
add negative test for HttpStandardHeadersScenario
pcarleton May 14, 2026
607190e
server/http-standard-headers: severity fixes for whitespace + malform…
pcarleton May 14, 2026
006330f
server/http-standard-headers: split rejection check into status (MUST…
pcarleton May 14, 2026
937b25d
rename check ids to sep-2243-* prefix; kebab-case throughout
pcarleton May 14, 2026
1d2d267
client: extract BaseHttpScenario to client/http-base.ts; both client …
pcarleton May 14, 2026
65afa3d
use req.setEncoding('utf8') instead of per-chunk Buffer.toString()
pcarleton May 14, 2026
cfa8a80
server/http-standard-headers: malformed-base64 padding/chars back to …
pcarleton May 15, 2026
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
156 changes: 156 additions & 0 deletions src/scenarios/client/http-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Shared HTTP test-server scaffold for client-under-test SEP-2243 scenarios.
*
* A scenario that needs to act as a Streamable-HTTP MCP server, inspect
* incoming client requests, and emit ConformanceChecks should extend this
* class and implement handlePost() + getChecks(). start()/stop() and the
* GET/DELETE/body-parse boilerplate are handled here.
*/

import http from 'http';
import {
Scenario,
ScenarioUrls,
ConformanceCheck,
SpecVersion,
DRAFT_PROTOCOL_VERSION
} from '../../types.js';

export abstract class BaseHttpScenario implements Scenario {
abstract name: string;
abstract description: string;
abstract specVersions: SpecVersion[];
allowClientError?: boolean;

protected server: http.Server | null = null;
protected checks: ConformanceCheck[] = [];
protected port: number = 0;
protected sessionId: string = `session-${Date.now()}`;

async start(): Promise<ScenarioUrls> {
return new Promise((resolve, reject) => {
this.server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
this.server.on('error', reject);
this.server.listen(0, () => {
const address = this.server!.address();
if (address && typeof address === 'object') {
this.port = address.port;
resolve({ serverUrl: `http://localhost:${this.port}` });
} else {
reject(new Error('Failed to get server address'));
}
});
});
}

async stop(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.server) {
this.server.close((err) => {
if (err) reject(err);
else {
this.server = null;
resolve();
}
});
} else {
resolve();
}
});
}

abstract getChecks(): ConformanceCheck[];

protected handleRequest(
req: http.IncomingMessage,
res: http.ServerResponse
): void {
if (req.method === 'GET') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'mcp-session-id': this.sessionId
});
res.write('data: \n\n');
return;
}
if (req.method === 'DELETE') {
res.writeHead(200);
res.end();
return;
}
if (req.method !== 'POST') {
res.writeHead(405);
res.end('Method Not Allowed');
return;
}

// Decode the stream as UTF-8 so multi-byte characters that straddle a
// chunk boundary aren't corrupted by per-chunk Buffer.toString().
req.setEncoding('utf8');
let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
try {
const request = JSON.parse(body);
this.handlePost(req, res, request);
} catch (error) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
jsonrpc: '2.0',
error: { code: -32700, message: `Parse error: ${error}` }
})
);
}
});
}

protected abstract handlePost(
req: http.IncomingMessage,
res: http.ServerResponse,
request: any
): void;

protected sendJson(res: http.ServerResponse, body: object): void {
res.writeHead(200, {
'Content-Type': 'application/json',
'mcp-session-id': this.sessionId
});
res.end(JSON.stringify(body));
}

protected sendInitialize(
res: http.ServerResponse,
request: any,
capabilities: object = { tools: {} }
): void {
this.sendJson(res, {
jsonrpc: '2.0',
id: request.id,
result: {
protocolVersion: DRAFT_PROTOCOL_VERSION,
serverInfo: { name: this.name + '-server', version: '1.0.0' },
capabilities
}
});
}

protected sendNotificationAck(res: http.ServerResponse): void {
res.writeHead(202);
res.end();
}

protected sendGenericResult(res: http.ServerResponse, request: any): void {
this.sendJson(res, {
jsonrpc: '2.0',
id: request.id,
result: {}
});
}
}
Loading