Server.php•11.6 kB
<?php
declare(strict_types=1);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
error_reporting(E_ALL);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/Helpers.php';
use OpenFGA\Authentication\{ClientCredentialAuthentication, TokenAuthentication};
use OpenFGA\{Client, ClientInterface};
use OpenFGA\MCP\{ConfigurableHttpServerTransport, DebugLogger, LoggingStdioTransport, OfflineClient};
use OpenFGA\MCP\Documentation\DocumentationIndexSingleton;
use PhpMcp\Server\Defaults\BasicContainer;
use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\{StdioServerTransport};
try {
// Check if OpenFGA configuration is provided
$apiUrl = getConfiguredString('OPENFGA_MCP_API_URL', '');
$hasToken = '' !== getConfiguredString('OPENFGA_MCP_API_TOKEN', '');
$hasClientId = '' !== getConfiguredString('OPENFGA_MCP_API_CLIENT_ID', '');
// Determine if we're in offline mode
$isOfflineMode = ('' === $apiUrl && ! $hasToken && ! $hasClientId);
if ($isOfflineMode) {
// Use offline client for planning and coding features
$openfga = new OfflineClient;
fwrite(STDERR, "[INFO] Starting OpenFGA MCP Server in OFFLINE MODE\n");
fwrite(STDERR, "[INFO] Available features: Planning (Prompts) and Coding assistance\n");
fwrite(STDERR, "[INFO] To enable administrative features, configure OPENFGA_MCP_API_URL\n\n");
} else {
// Use real client for full functionality
$authentication = null;
if ($hasToken) {
$authentication = new TokenAuthentication(
token: getConfiguredString('OPENFGA_MCP_API_TOKEN', ''),
);
}
if ($hasClientId) {
$authentication = new ClientCredentialAuthentication(
clientId: getConfiguredString('OPENFGA_MCP_API_CLIENT_ID', ''),
clientSecret: getConfiguredString('OPENFGA_MCP_API_CLIENT_SECRET', ''),
issuer: getConfiguredString('OPENFGA_MCP_API_ISSUER', ''),
audience: getConfiguredString('OPENFGA_MCP_API_AUDIENCE', ''),
);
}
// Use provided URL or default
$finalUrl = '' !== $apiUrl ? $apiUrl : 'http://127.0.0.1:8080';
$openfga = new Client(
url: $finalUrl,
authentication: $authentication,
);
// Validate connection to OpenFGA during startup
try {
$openfga->listStores(pageSize: 1)->success(static function (): void {
// Connection successful
})->failure(static function (mixed $error): void {
throw new RuntimeException('Failed to connect to OpenFGA: ' . (string) $error);
});
} catch (Throwable $connectionError) {
fwrite(STDERR, '[WARNING] Could not validate OpenFGA connection: ' . $connectionError->getMessage() . "\n");
fwrite(STDERR, "[WARNING] The server will start but operations may fail.\n");
fwrite(STDERR, "[WARNING] Please verify your OPENFGA_MCP_API_URL and authentication settings.\n\n");
}
fwrite(STDERR, "[INFO] Starting OpenFGA MCP Server in ONLINE MODE\n");
fwrite(STDERR, sprintf('[INFO] Connected to: %s%s', $finalUrl, PHP_EOL));
fwrite(STDERR, "[INFO] All features enabled: Planning, Coding, and Administrative\n\n");
}
$container = new BasicContainer;
$container->set(ClientInterface::class, $openfga);
$server = Server::make()
->withServerInfo('OpenFGA MCP Server', '2.0.0')
->withContainer($container)
->build();
$server->discover(
basePath: __DIR__,
scanDirs: ['Tools', 'Resources', 'Prompts', 'Completions'],
saveToCache: false,
);
// Initialize documentation index early for better performance
fwrite(STDERR, "[INFO] Initializing documentation index...\n");
try {
$startTime = microtime(true);
$docIndex = DocumentationIndexSingleton::getInstance();
$docIndex->initialize();
$endTime = microtime(true);
$loadTime = round(($endTime - $startTime) * 1000.0, 2);
// Get statistics about loaded documentation
$sdks = $docIndex->getSdkList();
$sdkCount = count($sdks);
fwrite(STDERR, "[INFO] Documentation index initialized successfully in {$loadTime}ms\n");
fwrite(STDERR, "[INFO] Loaded documentation for {$sdkCount} SDKs\n");
// Show details about each SDK
foreach ($sdks as $sdk) {
$overview = $docIndex->getSdkOverview($sdk);
if (null !== $overview) {
$classCount = count($overview['classes']);
$sectionCount = count($overview['sections']);
$chunkCount = $overview['total_chunks'];
fwrite(STDERR, "[INFO] - {$overview['name']}: {$classCount} classes, {$sectionCount} sections, {$chunkCount} chunks\n");
}
}
// Also check for general documentation
foreach (['general', 'authoring'] as $generalDoc) {
$overview = $docIndex->getSdkOverview($generalDoc);
if (null !== $overview) {
$sectionCount = count($overview['sections']);
$chunkCount = $overview['total_chunks'];
fwrite(STDERR, "[INFO] - {$overview['name']}: {$sectionCount} sections, {$chunkCount} chunks\n");
}
}
DebugLogger::logServerLifecycle('documentation_initialized', [
'load_time_ms' => $loadTime,
'sdk_count' => $sdkCount,
'sdks' => $docIndex->getSdkList(),
]);
} catch (Throwable $docError) {
fwrite(STDERR, '[WARNING] Failed to initialize documentation index: ' . $docError->getMessage() . "\n");
fwrite(STDERR, "[WARNING] Documentation features will initialize on first use\n");
DebugLogger::logServerLifecycle('documentation_initialization_failed', [
'error' => $docError->getMessage(),
'file' => $docError->getFile(),
'line' => $docError->getLine(),
]);
}
$transport = match (getConfiguredString('OPENFGA_MCP_TRANSPORT')) {
'http' => new ConfigurableHttpServerTransport(
host: getConfiguredString('OPENFGA_MCP_TRANSPORT_HOST', '127.0.0.1'),
port: getConfiguredInt('OPENFGA_MCP_TRANSPORT_PORT', 9090),
enableJsonResponse: false === getConfiguredBool('OPENFGA_MCP_TRANSPORT_SSE', true),
stateless: getConfiguredBool('OPENFGA_MCP_TRANSPORT_STATELESS', false),
),
default => getConfiguredBool('OPENFGA_MCP_DEBUG', true)
? new LoggingStdioTransport
: new StdioServerTransport,
};
// Log server startup
DebugLogger::logServerLifecycle('startup', [
'version' => '2.0.0',
'mode' => $isOfflineMode ? 'offline' : 'online',
'transport' => '' !== getConfiguredString('OPENFGA_MCP_TRANSPORT', '') ? getConfiguredString('OPENFGA_MCP_TRANSPORT', '') : 'stdio',
'debug' => getConfiguredBool('OPENFGA_MCP_DEBUG', true),
'api_url' => $isOfflineMode ? null : ('' !== $apiUrl ? $apiUrl : 'http://127.0.0.1:8080'),
]);
// Register exception handler for uncaught exceptions
set_exception_handler(static function (Throwable $exception): void {
// Check if this is the CallToolRequest stdClass issue
if (
$exception instanceof TypeError
&& str_contains($exception->getMessage(), 'CallToolRequest::__construct()')
&& str_contains($exception->getMessage(), 'must be of type array, stdClass given')
) {
// Log the specific issue
DebugLogger::logServerLifecycle('calltoolrequest_stdclass_error', [
'error' => $exception->getMessage(),
'workaround' => 'Tool called without arguments - this is a known MCP client issue',
'suggestion' => 'MCP client should provide empty array [] instead of empty object {} for tool arguments',
]);
// Log helpful message to stderr
fwrite(STDERR, "[TOOL CALL ERROR] MCP client called tool without proper arguments\n");
fwrite(STDERR, "This is a known issue where empty arguments {} should be sent as [] instead\n");
fwrite(STDERR, "Tool call was ignored - client should retry with proper arguments\n");
// Don't exit - let the server continue running
return;
}
DebugLogger::logServerLifecycle('uncaught_exception', [
'error' => $exception->getMessage(),
'class' => $exception::class,
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
]);
// Also log to stderr for visibility
fwrite(STDERR, '[UNCAUGHT EXCEPTION] ' . $exception->getMessage() . "\n");
fwrite(STDERR, 'File: ' . $exception->getFile() . ':' . $exception->getLine() . "\n");
// Exit with error code
exit(1);
});
// Register error handler for fatal errors
set_error_handler(static function ($severity, $message, $file, $line): false {
// Check if this is a fatal error type
if (($severity & (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_PARSE)) !== 0) {
DebugLogger::logServerLifecycle('fatal_error', [
'severity' => $severity,
'message' => $message,
'file' => $file,
'line' => $line,
]);
// Return false to let PHP's internal error handler also process it
return false;
}
// For non-fatal errors, let PHP handle them normally
return false;
});
// Register shutdown handler to catch fatal errors and abnormal terminations
register_shutdown_function(static function (): void {
$error = error_get_last();
// Check if shutdown was due to a fatal error
if (null !== $error && (($error['type'] & (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_PARSE)) !== 0)) {
DebugLogger::logServerLifecycle('fatal_shutdown', [
'reason' => 'fatal_error',
'error' => $error['message'],
'file' => $error['file'],
'line' => $error['line'],
'type' => $error['type'],
]);
} else {
// Normal shutdown
DebugLogger::logServerLifecycle('shutdown', [
'reason' => 'normal_termination',
]);
}
});
// Register signal handlers for graceful shutdown
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGTERM, static function (): void {
DebugLogger::logServerLifecycle('shutdown', [
'reason' => 'SIGTERM',
]);
exit(0);
});
pcntl_signal(SIGINT, static function (): void {
DebugLogger::logServerLifecycle('shutdown', [
'reason' => 'SIGINT',
]);
exit(0);
});
}
$server->listen($transport);
} catch (Throwable $throwable) {
fwrite(STDERR, '[CRITICAL ERROR] ' . $throwable->getMessage() . "\n");
// Log critical error
DebugLogger::logServerLifecycle('critical_error', [
'error' => $throwable->getMessage(),
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
'trace' => $throwable->getTraceAsString(),
]);
exit(1);
}