Files
redmine/redMCP/bin/test-mcp-http-handler.php
T
2026-05-04 09:50:11 -04:00

165 lines
5.3 KiB
PHP
Executable File

#!/usr/bin/env php
<?php
declare(strict_types=1);
$repoRoot = dirname(__DIR__);
$tmpDir = sys_get_temp_dir() . '/redmcp-http-test-' . getmypid();
if (!mkdir($tmpDir, 0775, true) && !is_dir($tmpDir)) {
throw new RuntimeException('Could not create temp test dir.');
}
$router = $tmpDir . '/router.php';
file_put_contents($router, <<<'PHP'
<?php
declare(strict_types=1);
use RedMCP\McpDispatcher;
use RedMCP\McpHttpHandler;
use RedMCP\RedmineClient;
require '%AUTOLOAD%';
$handler = new McpHttpHandler(
new McpDispatcher(RedmineClient::fromCredentials('http://127.0.0.1', 'test-key')),
'test-token',
'/mcp'
);
$handler->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<int,string> $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);
}