#!/usr/bin/env php handle(); PHP); file_put_contents($router, str_replace('%AUTOLOAD%', addslashes($repoRoot . '/vendor/autoload.php'), (string) file_get_contents($router))); $port = 18765 + (getmypid() % 1000); $command = [PHP_BINARY, '-S', '127.0.0.1:' . $port, $router]; $process = proc_open($command, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $repoRoot); if (!is_resource($process)) { throw new RuntimeException('Could not start PHP built-in server.'); } $assertions = 0; try { waitForServer($port); $sse = httpRequest( 'POST', 'http://127.0.0.1:' . $port . '/mcp', [ 'Authorization: Bearer test-token', 'Content-Type: application/json', 'Accept: application/json, text/event-stream', ], '{"jsonrpc":"2.0","id":1,"method":"ping","params":{}}' ); assertContains('HTTP/1.1 200 OK', $sse['headers'], 'SSE POST returns 200', $assertions); assertContains('text/event-stream', $sse['headers'], 'SSE POST returns event stream content type', $assertions); assertContains('X-Accel-Buffering: no', $sse['headers'], 'SSE POST disables proxy buffering', $assertions); assertContains("event: message\n", $sse['body'], 'SSE POST emits a message event', $assertions); assertContains('data: {"jsonrpc":"2.0","id":1,"result":[]}', $sse['body'], 'SSE POST emits JSON-RPC response data', $assertions); $json = httpRequest( 'POST', 'http://127.0.0.1:' . $port . '/mcp', [ 'Authorization: Bearer test-token', 'Content-Type: application/json', 'Accept: application/json', ], '{"jsonrpc":"2.0","id":2,"method":"ping","params":{}}' ); assertContains('application/json', $json['headers'], 'JSON POST preserves application/json content type', $assertions); assertContains('"id":2', $json['body'], 'JSON POST emits JSON-RPC response body', $assertions); $get = httpRequest( 'GET', 'http://127.0.0.1:' . $port . '/mcp', [ 'Authorization: Bearer test-token', 'Accept: text/event-stream', ], null ); assertContains('HTTP/1.1 405 Method Not Allowed', $get['headers'], 'GET returns method-not-allowed until standalone streams exist', $assertions); assertContains('Allow: POST', $get['headers'], 'GET advertises supported method', $assertions); $origin = httpRequest( 'POST', 'http://127.0.0.1:' . $port . '/mcp', [ 'Authorization: Bearer test-token', 'Content-Type: application/json', 'Accept: application/json', 'Origin: https://example.invalid', ], '{"jsonrpc":"2.0","id":3,"method":"ping","params":{}}' ); assertContains('HTTP/1.1 403 Forbidden', $origin['headers'], 'disallowed browser origin returns forbidden', $assertions); fwrite(STDOUT, "OK {$assertions} assertions\n"); } finally { proc_terminate($process); proc_close($process); foreach ($pipes as $pipe) { if (is_resource($pipe)) { fclose($pipe); } } @unlink($router); @rmdir($tmpDir); } function waitForServer(int $port): void { $deadline = microtime(true) + 5; while (microtime(true) < $deadline) { $socket = @fsockopen('127.0.0.1', $port, $errno, $errstr, 0.1); if (is_resource($socket)) { fclose($socket); return; } usleep(100000); } throw new RuntimeException('Timed out waiting for test HTTP server.'); } /** * @param array $headers * @return array{headers:string,body:string} */ function httpRequest(string $method, string $url, array $headers, ?string $body): array { $curl = curl_init($url); if ($curl === false) { throw new RuntimeException('Could not initialize curl.'); } curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); curl_setopt($curl, CURLOPT_HEADER, true); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); if ($body !== null) { curl_setopt($curl, CURLOPT_POSTFIELDS, $body); } $raw = curl_exec($curl); if (!is_string($raw)) { throw new RuntimeException('curl failed: ' . curl_error($curl)); } $headerSize = (int) curl_getinfo($curl, CURLINFO_HEADER_SIZE); curl_close($curl); return [ 'headers' => substr($raw, 0, $headerSize), 'body' => substr($raw, $headerSize), ]; } function assertContains(string $needle, string $haystack, string $message, int &$assertions): void { $assertions++; if (strpos($haystack, $needle) !== false) { return; } fwrite(STDERR, "FAIL: {$message}\nNeedle: {$needle}\nHaystack: {$haystack}\n"); exit(1); }