|
| 1 | +import http from 'http'; |
| 2 | +import { |
| 3 | + Scenario, |
| 4 | + ScenarioUrls, |
| 5 | + ConformanceCheck, |
| 6 | + SpecVersion, |
| 7 | + DRAFT_PROTOCOL_VERSION |
| 8 | +} from '../../types'; |
| 9 | + |
| 10 | +export class StatelessClientScenario implements Scenario { |
| 11 | + name = 'stateless-client'; |
| 12 | + specVersions: SpecVersion[] = ['2026-06-18', DRAFT_PROTOCOL_VERSION]; |
| 13 | + description = 'Tests stateless MCP client behavior (SEP-2575)'; |
| 14 | + |
| 15 | + private server: http.Server | null = null; |
| 16 | + private checks: ConformanceCheck[] = []; |
| 17 | + |
| 18 | + async start(): Promise<ScenarioUrls> { |
| 19 | + return new Promise((resolve, reject) => { |
| 20 | + this.server = http.createServer((req, res) => { |
| 21 | + this.handleRequest(req, res); |
| 22 | + }); |
| 23 | + this.server.on('error', reject); |
| 24 | + this.server.listen(0, () => { |
| 25 | + const address = this.server!.address(); |
| 26 | + if (address && typeof address === 'object') { |
| 27 | + resolve({ serverUrl: `http://localhost:${address.port}` }); |
| 28 | + } |
| 29 | + }); |
| 30 | + }); |
| 31 | + } |
| 32 | + |
| 33 | + async stop(): Promise<void> { |
| 34 | + return new Promise((resolve) => { |
| 35 | + if (this.server) { |
| 36 | + this.server.close(() => { resolve(); }); |
| 37 | + } else { |
| 38 | + resolve(); |
| 39 | + } |
| 40 | + }); |
| 41 | + } |
| 42 | + |
| 43 | + getChecks(): ConformanceCheck[] { |
| 44 | + return this.checks; |
| 45 | + } |
| 46 | + |
| 47 | + private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void { |
| 48 | + let body = ''; |
| 49 | + req.on('data', (chunk) => { body += chunk.toString(); }); |
| 50 | + req.on('end', () => { |
| 51 | + const request = JSON.parse(body); |
| 52 | + |
| 53 | + // TEST 1: Verify client can call server/discover |
| 54 | + if (request.method === 'server/discover') { |
| 55 | + this.checks.push({ |
| 56 | + id: 'client-calls-discover', |
| 57 | + name: 'ClientCallsDiscover', |
| 58 | + description: 'Client is able to successfully call server/discover', |
| 59 | + status: 'SUCCESS', |
| 60 | + timestamp: new Date().toISOString(), |
| 61 | + specReferences: [{ id: 'SEP-2575', url: '' }] |
| 62 | + }); |
| 63 | + |
| 64 | + // Respond with valid discovery payload to keep client happy |
| 65 | + res.writeHead(200, { 'Content-Type': 'application/json' }); |
| 66 | + res.end(JSON.stringify({ |
| 67 | + jsonrpc: '2.0', |
| 68 | + id: request.id, |
| 69 | + result: { |
| 70 | + supportedVersions: ['2026-06-18'], |
| 71 | + capabilities: {}, |
| 72 | + serverInfo: { name: 'test', version: '1.0' } |
| 73 | + } |
| 74 | + })); |
| 75 | + return; |
| 76 | + } |
| 77 | + |
| 78 | + // TEST 2: Verify inline _meta on every request |
| 79 | + const meta = request.params?._meta; |
| 80 | + const hasProtocolVersion = meta?.['io.modelcontextprotocol/protocolVersion']; |
| 81 | + const hasClientInfo = meta?.['io.modelcontextprotocol/clientInfo']; |
| 82 | + const hasCapabilities = meta?.['io.modelcontextprotocol/clientCapabilities']; |
| 83 | + |
| 84 | + const metaIsValid = hasProtocolVersion && hasClientInfo && hasCapabilities; |
| 85 | + |
| 86 | + this.checks.push({ |
| 87 | + id: 'client-populates-meta', |
| 88 | + name: 'ClientPopulatesMeta', |
| 89 | + description: 'Client populates _meta on every request with all three required fields', |
| 90 | + status: metaIsValid ? 'SUCCESS' : 'FAILURE', |
| 91 | + timestamp: new Date().toISOString(), |
| 92 | + specReferences: [{ id: 'SEP-2575', url: '' }], |
| 93 | + details: { meta } |
| 94 | + }); |
| 95 | + |
| 96 | + // Return generic response to unblock client |
| 97 | + res.writeHead(200, { 'Content-Type': 'application/json' }); |
| 98 | + res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: {} })); |
| 99 | + }); |
| 100 | + } |
| 101 | +} |
0 commit comments