189 lines
4.9 KiB
PHP
Executable File
189 lines
4.9 KiB
PHP
Executable File
#!/usr/bin/env php
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use RedMCP\McpEnvironment;
|
|
|
|
require __DIR__ . '/../vendor/autoload.php';
|
|
|
|
$options = getopt('', ['host:', 'port:', 'path:', 'pid-file:', 'debug-log:', 'status', 'stop', 'force', 'help']);
|
|
if (isset($options['help'])) {
|
|
fwrite(
|
|
STDOUT,
|
|
"Usage: redmcp-http-server.php [--host 127.0.0.1] [--port 8765] [--path /mcp] [--pid-file /tmp/redmcp-http-server.pid] [--debug-log /tmp/redmcp-mcp.log] [--status|--stop] [--force]\n"
|
|
);
|
|
exit(0);
|
|
}
|
|
|
|
$host = (string) ($options['host'] ?? '127.0.0.1');
|
|
$port = (int) ($options['port'] ?? 8765);
|
|
$path = (string) ($options['path'] ?? '/mcp');
|
|
$pidFile = (string) ($options['pid-file'] ?? '/tmp/redmcp-http-server.pid');
|
|
$debugLog = isset($options['debug-log']) ? (string) $options['debug-log'] : null;
|
|
$force = isset($options['force']);
|
|
|
|
if (isset($options['status'])) {
|
|
showStatus($pidFile);
|
|
exit(0);
|
|
}
|
|
|
|
if (isset($options['stop'])) {
|
|
stopServer($pidFile);
|
|
exit(0);
|
|
}
|
|
|
|
if (isLivePidFile($pidFile)) {
|
|
fwrite(STDERR, "redMCP HTTP server already appears to be running with PID " . trim((string) file_get_contents($pidFile)) . ".\n");
|
|
fwrite(STDERR, "Use --stop first, or --force to replace a stale PID file.\n");
|
|
exit(1);
|
|
}
|
|
|
|
if (is_file($pidFile)) {
|
|
if (!$force) {
|
|
fwrite(STDERR, "Stale PID file exists at {$pidFile}. Use --force to remove it.\n");
|
|
exit(1);
|
|
}
|
|
unlink($pidFile);
|
|
}
|
|
|
|
try {
|
|
$env = McpEnvironment::load(__DIR__ . '/../.env');
|
|
if ($env['mcp_server_token'] === null) {
|
|
throw new RuntimeException('MCP_SERVER_TOKEN is required for the network MCP server.');
|
|
}
|
|
} catch (Throwable $exception) {
|
|
fwrite(STDERR, $exception->getMessage() . PHP_EOL);
|
|
exit(1);
|
|
}
|
|
|
|
putenv('MCP_HTTP_PATH=' . $path);
|
|
if ($debugLog !== null && $debugLog !== '') {
|
|
putenv('MCP_DEBUG_LOG=' . $debugLog);
|
|
}
|
|
$router = __DIR__ . '/../app/mcp-http-router.php';
|
|
$command = [
|
|
PHP_BINARY,
|
|
'-S',
|
|
$host . ':' . $port,
|
|
$router,
|
|
];
|
|
|
|
fwrite(STDERR, "redMCP HTTP server listening on http://{$host}:{$port}{$path}\n");
|
|
fwrite(STDERR, "Authorization: Bearer <MCP_SERVER_TOKEN> is required.\n");
|
|
if ($debugLog !== null && $debugLog !== '') {
|
|
fwrite(STDERR, "Debug log: {$debugLog}\n");
|
|
}
|
|
|
|
$descriptorSpec = [
|
|
0 => STDIN,
|
|
1 => STDOUT,
|
|
2 => STDERR,
|
|
];
|
|
$process = proc_open($command, $descriptorSpec, $pipes);
|
|
if (!is_resource($process)) {
|
|
fwrite(STDERR, "Could not start PHP built-in HTTP server.\n");
|
|
exit(1);
|
|
}
|
|
|
|
$status = proc_get_status($process);
|
|
$pid = (int) ($status['pid'] ?? 0);
|
|
if ($pid <= 0) {
|
|
proc_terminate($process);
|
|
fwrite(STDERR, "Could not determine HTTP server PID.\n");
|
|
exit(1);
|
|
}
|
|
|
|
$pidDir = dirname($pidFile);
|
|
if ($pidDir !== '' && $pidDir !== '.' && !is_dir($pidDir)) {
|
|
mkdir($pidDir, 0775, true);
|
|
}
|
|
file_put_contents($pidFile, (string) $pid);
|
|
fwrite(STDERR, "PID file: {$pidFile} ({$pid})\n");
|
|
|
|
$exitCode = proc_close($process);
|
|
if (is_file($pidFile) && trim((string) file_get_contents($pidFile)) === (string) $pid) {
|
|
unlink($pidFile);
|
|
}
|
|
exit((int) $exitCode);
|
|
|
|
function showStatus(string $pidFile): void
|
|
{
|
|
if (!is_file($pidFile)) {
|
|
fwrite(STDOUT, "stopped: no PID file at {$pidFile}\n");
|
|
return;
|
|
}
|
|
|
|
$pid = (int) trim((string) file_get_contents($pidFile));
|
|
if ($pid > 0 && pidAlive($pid)) {
|
|
fwrite(STDOUT, "running: PID {$pid} from {$pidFile}\n");
|
|
return;
|
|
}
|
|
|
|
fwrite(STDOUT, "stale: PID file {$pidFile} points to non-running PID {$pid}\n");
|
|
}
|
|
|
|
function stopServer(string $pidFile): void
|
|
{
|
|
if (!is_file($pidFile)) {
|
|
fwrite(STDOUT, "stopped: no PID file at {$pidFile}\n");
|
|
return;
|
|
}
|
|
|
|
$pid = (int) trim((string) file_get_contents($pidFile));
|
|
if ($pid <= 0 || !pidAlive($pid)) {
|
|
unlink($pidFile);
|
|
fwrite(STDOUT, "removed stale PID file {$pidFile}\n");
|
|
return;
|
|
}
|
|
|
|
if (!stopPid($pid)) {
|
|
fwrite(STDERR, "could not stop PID {$pid}\n");
|
|
exit(1);
|
|
}
|
|
|
|
$deadline = time() + 5;
|
|
while (pidAlive($pid) && time() < $deadline) {
|
|
usleep(100000);
|
|
}
|
|
if (pidAlive($pid)) {
|
|
fwrite(STDERR, "PID {$pid} did not stop within timeout\n");
|
|
exit(1);
|
|
}
|
|
|
|
if (is_file($pidFile)) {
|
|
unlink($pidFile);
|
|
}
|
|
fwrite(STDOUT, "stopped PID {$pid}\n");
|
|
}
|
|
|
|
function isLivePidFile(string $pidFile): bool
|
|
{
|
|
if (!is_file($pidFile)) {
|
|
return false;
|
|
}
|
|
|
|
$pid = (int) trim((string) file_get_contents($pidFile));
|
|
return $pid > 0 && pidAlive($pid);
|
|
}
|
|
|
|
function pidAlive(int $pid): bool
|
|
{
|
|
if (function_exists('posix_kill')) {
|
|
return posix_kill($pid, 0);
|
|
}
|
|
|
|
exec('kill -0 ' . escapeshellarg((string) $pid) . ' 2>/dev/null', $output, $exitCode);
|
|
return $exitCode === 0;
|
|
}
|
|
|
|
function stopPid(int $pid): bool
|
|
{
|
|
if (function_exists('posix_kill')) {
|
|
return posix_kill($pid, 15);
|
|
}
|
|
|
|
exec('kill ' . escapeshellarg((string) $pid) . ' 2>/dev/null', $output, $exitCode);
|
|
return $exitCode === 0;
|
|
}
|