ConfigurationParser.php•8.24 kB
<?php
declare(strict_types=1);
namespace OpenFGA\MCP;
use Psr\Log\{LoggerInterface, NullLogger};
use function gettype;
use function in_array;
use function is_array;
use function is_bool;
use function is_string;
use function sprintf;
use function strlen;
final readonly class ConfigurationParser
{
/**
* @var array<string, string> List of all supported configuration keys and their types
*/
private const array SUPPORTED_CONFIG = [
// Core OpenFGA Connection
'OPENFGA_MCP_API_URL' => 'string',
'OPENFGA_MCP_API_TOKEN' => 'string',
'OPENFGA_MCP_API_CLIENT_ID' => 'string',
'OPENFGA_MCP_API_CLIENT_SECRET' => 'string',
'OPENFGA_MCP_API_ISSUER' => 'string',
'OPENFGA_MCP_API_AUDIENCE' => 'string',
// Security & Access Control
'OPENFGA_MCP_API_WRITEABLE' => 'bool',
'OPENFGA_MCP_API_RESTRICT' => 'bool',
'OPENFGA_MCP_API_STORE' => 'string',
'OPENFGA_MCP_API_MODEL' => 'string',
// Transport Configuration (rarely set via query params but supported)
'OPENFGA_MCP_TRANSPORT' => 'string',
'OPENFGA_MCP_TRANSPORT_HOST' => 'string',
'OPENFGA_MCP_TRANSPORT_PORT' => 'int',
'OPENFGA_MCP_TRANSPORT_SSE' => 'bool',
'OPENFGA_MCP_TRANSPORT_STATELESS' => 'bool',
// Debug & Logging
'OPENFGA_MCP_DEBUG' => 'bool',
];
private LoggerInterface $logger;
public function __construct(?LoggerInterface $logger = null)
{
$this->logger = $logger ?? new NullLogger;
}
/**
* Parse JSON configuration and apply to $_ENV.
*
* @param string $jsonConfig JSON-encoded configuration string
* @return ConfigurationResult Result object with status and details
*/
public function parseAndApply(string $jsonConfig): ConfigurationResult
{
$errors = [];
$appliedKeys = [];
$appliedValues = [];
// Decode JSON
$config = json_decode($jsonConfig, true);
if (JSON_ERROR_NONE !== json_last_error()) {
$errors[] = sprintf('Invalid JSON: %s', json_last_error_msg());
return new ConfigurationResult(false, $errors, $appliedKeys, $appliedValues);
}
if (! is_array($config)) {
$errors[] = 'Configuration must be a JSON object';
return new ConfigurationResult(false, $errors, $appliedKeys, $appliedValues);
}
// Process each configuration value
/** @var mixed $value */
foreach ($config as $key => $value) {
// Check if key is supported
if (! isset(self::SUPPORTED_CONFIG[$key])) {
$this->logger->warning('Unsupported configuration key', ['key' => $key]);
continue; // Skip unsupported keys silently
}
// Validate and convert value based on expected type
$expectedType = self::SUPPORTED_CONFIG[$key];
$processedValue = $this->processValue($value, $expectedType, $key, $errors);
if (null !== $processedValue) {
// Apply to $_ENV (highest precedence)
$_ENV[$key] = $processedValue;
$appliedKeys[] = $key;
$appliedValues[$key] = $processedValue;
$this->logger->debug('Configuration value set', [
'key' => $key,
'type' => $expectedType,
'value' => $this->maskSensitiveValue($key, $processedValue),
]);
}
}
// Validate configuration combinations
$this->validateConfigurationCombinations($appliedValues, $errors);
return new ConfigurationResult(
[] === $errors,
$errors,
$appliedKeys,
$appliedValues,
);
}
/**
* Mask sensitive values for logging.
*
* @param string $key Configuration key
* @param string $value Configuration value
* @return string Masked value if sensitive, original otherwise
*/
private function maskSensitiveValue(string $key, string $value): string
{
$sensitiveKeys = [
'OPENFGA_MCP_API_TOKEN',
'OPENFGA_MCP_API_CLIENT_SECRET',
];
if (in_array($key, $sensitiveKeys, true) && 4 < strlen($value)) {
return substr($value, 0, 4) . str_repeat('*', min(12, strlen($value) - 4));
}
return $value;
}
/**
* Process and validate a single configuration value.
*
* @param mixed $value The raw value from JSON
* @param string $expectedType The expected type (string, int, bool)
* @param string $key The configuration key (for error messages)
* @param array<string> $errors Array to collect error messages
* @return string|null Processed value or null if invalid
*/
private function processValue(mixed $value, string $expectedType, string $key, array &$errors): ?string
{
switch ($expectedType) {
case 'string':
if (! is_string($value) && ! is_numeric($value)) {
$errors[] = sprintf('%s must be a string, %s given', $key, gettype($value));
return null;
}
return (string) $value;
case 'int':
if (! is_numeric($value)) {
$errors[] = sprintf('%s must be numeric, %s given', $key, gettype($value));
return null;
}
return (string) (int) $value;
case 'bool':
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
if (is_string($value) && in_array(strtolower($value), ['true', 'false', '1', '0'], true)) {
return in_array(strtolower($value), ['true', '1'], true) ? 'true' : 'false';
}
if (is_numeric($value)) {
return ((bool) $value) ? 'true' : 'false';
}
$errors[] = sprintf('%s must be a boolean, %s given', $key, gettype($value));
return null;
default:
$errors[] = sprintf('Unknown type %s for key %s', $expectedType, $key);
return null;
}
}
/**
* Validate that certain configuration combinations are valid.
*
* @param array<string, string> $config Applied configuration values
* @param array<string> $errors Array to collect error messages
*/
private function validateConfigurationCombinations(array $config, array &$errors): void
{
// If OAuth2 client credentials are provided, all related fields must be present
$hasClientId = isset($config['OPENFGA_MCP_API_CLIENT_ID']) && '' !== $config['OPENFGA_MCP_API_CLIENT_ID'];
$hasClientSecret = isset($config['OPENFGA_MCP_API_CLIENT_SECRET']) && '' !== $config['OPENFGA_MCP_API_CLIENT_SECRET'];
$hasIssuer = isset($config['OPENFGA_MCP_API_ISSUER']) && '' !== $config['OPENFGA_MCP_API_ISSUER'];
$hasAudience = isset($config['OPENFGA_MCP_API_AUDIENCE']) && '' !== $config['OPENFGA_MCP_API_AUDIENCE'];
if (($hasClientId || $hasClientSecret) && (! $hasClientId || ! $hasClientSecret || ! $hasIssuer || ! $hasAudience)) {
$errors[] = 'OAuth2 client credentials require all of: OPENFGA_MCP_API_CLIENT_ID, OPENFGA_MCP_API_CLIENT_SECRET, OPENFGA_MCP_API_ISSUER, OPENFGA_MCP_API_AUDIENCE';
}
// If restricted mode is enabled, store and model must be provided
$isRestricted = isset($config['OPENFGA_MCP_API_RESTRICT']) && 'true' === $config['OPENFGA_MCP_API_RESTRICT'];
if ($isRestricted) {
$hasStore = isset($config['OPENFGA_MCP_API_STORE']) && '' !== $config['OPENFGA_MCP_API_STORE'];
$hasModel = isset($config['OPENFGA_MCP_API_MODEL']) && '' !== $config['OPENFGA_MCP_API_MODEL'];
if (! $hasStore || ! $hasModel) {
$errors[] = 'Restricted mode requires both OPENFGA_MCP_API_STORE and OPENFGA_MCP_API_MODEL to be set';
}
}
}
}