OfflineClient.php•11.6 kB
<?php
declare(strict_types=1);
namespace OpenFGA\MCP;
use DateTimeImmutable;
use OpenFGA\ClientInterface;
use OpenFGA\Models\{AuthorizationModelInterface, StoreInterface, TupleKeyInterface};
use OpenFGA\Models\Collections\{AssertionsInterface, ConditionsInterface, TupleKeysInterface, TypeDefinitionsInterface, UserTypeFiltersInterface};
use OpenFGA\Models\Collections\BatchCheckItemsInterface;
use OpenFGA\Models\Enums\{Consistency, SchemaVersion};
use OpenFGA\Results\{Failure, FailureInterface, Success, SuccessInterface};
use Override;
use Psr\Http\Message\{RequestInterface as HttpRequestInterface, ResponseInterface as HttpResponseInterface};
use RuntimeException;
/**
* Offline client implementation for OpenFGA MCP Server.
*
* This client allows the MCP server to function without a live OpenFGA instance,
* enabling planning and coding features while returning appropriate responses
* for operations that would normally require a live connection.
*
* Read operations return minimal empty success responses, while write operations
* return failures, effectively preventing actual API calls while satisfying
* the interface contract.
*/
final class OfflineClient implements ClientInterface
{
private const string ERROR_MESSAGE = 'This operation requires a live OpenFGA instance. Please configure OPENFGA_MCP_API_URL to enable administrative features.';
#[Override]
public function batchCheck(
StoreInterface | string $store,
AuthorizationModelInterface | string $model,
BatchCheckItemsInterface $checks,
): FailureInterface | SuccessInterface {
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(['results' => []]);
}
#[Override]
public function check(
StoreInterface | string $store,
AuthorizationModelInterface | string $model,
TupleKeyInterface $tuple,
?bool $trace = null,
?object $context = null,
?TupleKeysInterface $contextualTuples = null,
?Consistency $consistency = null,
): FailureInterface | SuccessInterface {
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(['allowed' => false, 'resolution' => 'offline']);
}
#[Override]
public function createAuthorizationModel(
StoreInterface | string $store,
TypeDefinitionsInterface $typeDefinitions,
?ConditionsInterface $conditions = null,
?SchemaVersion $schemaVersion = null,
): FailureInterface | SuccessInterface {
// Write operation - returns failure in offline mode
if ($this->shouldFail(true)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(null);
}
#[Override]
public function createStore(string $name): FailureInterface | SuccessInterface
{
// Write operation - returns failure in offline mode
if ($this->shouldFail(true)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(null);
}
#[Override]
public function deleteStore(StoreInterface | string $store): FailureInterface | SuccessInterface
{
// Write operation - returns failure in offline mode
if ($this->shouldFail(true)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(null);
}
#[Override]
public function dsl(string $dsl): FailureInterface | SuccessInterface
{
// DSL parsing can work offline - returns minimal success
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(['valid' => false, 'message' => 'DSL validation requires a live OpenFGA instance']);
}
#[Override]
public function expand(
StoreInterface | string $store,
TupleKeyInterface $tuple,
AuthorizationModelInterface | string | null $model = null,
?TupleKeysInterface $contextualTuples = null,
?Consistency $consistency = null,
): FailureInterface | SuccessInterface {
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(['tree' => []]);
}
#[Override]
public function getAuthorizationModel(
StoreInterface | string $store,
AuthorizationModelInterface | string $model,
): FailureInterface | SuccessInterface {
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(null);
}
#[Override]
public function getLastRequest(): ?HttpRequestInterface
{
return null;
}
#[Override]
public function getLastResponse(): ?HttpResponseInterface
{
return null;
}
#[Override]
public function getStore(StoreInterface | string $store): FailureInterface | SuccessInterface
{
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(null);
}
#[Override]
public function listAuthorizationModels(
StoreInterface | string $store,
?string $continuationToken = null,
?int $pageSize = null,
): FailureInterface | SuccessInterface {
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(['models' => []]);
}
#[Override]
public function listObjects(
StoreInterface | string $store,
AuthorizationModelInterface | string $model,
string $type,
string $relation,
string $user,
?object $context = null,
?TupleKeysInterface $contextualTuples = null,
?Consistency $consistency = null,
): FailureInterface | SuccessInterface {
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(['objects' => []]);
}
#[Override]
public function listStores(
?string $continuationToken = null,
?int $pageSize = null,
): FailureInterface | SuccessInterface {
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(['stores' => []]);
}
#[Override]
public function listTupleChanges(
StoreInterface | string $store,
?string $continuationToken = null,
?int $pageSize = null,
?string $type = null,
?DateTimeImmutable $startTime = null,
): FailureInterface | SuccessInterface {
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(['changes' => []]);
}
#[Override]
public function listUsers(
StoreInterface | string $store,
AuthorizationModelInterface | string $model,
string $object,
string $relation,
UserTypeFiltersInterface $userFilters,
?object $context = null,
?TupleKeysInterface $contextualTuples = null,
?Consistency $consistency = null,
): FailureInterface | SuccessInterface {
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(['users' => []]);
}
#[Override]
public function readAssertions(
StoreInterface | string $store,
AuthorizationModelInterface | string $model,
): FailureInterface | SuccessInterface {
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(['assertions' => []]);
}
#[Override]
public function readTuples(
StoreInterface | string $store,
?TupleKeyInterface $tuple = null,
?string $continuationToken = null,
?int $pageSize = null,
?Consistency $consistency = null,
): FailureInterface | SuccessInterface {
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(['tuples' => []]);
}
#[Override]
public function streamedListObjects(
StoreInterface | string $store,
AuthorizationModelInterface | string $model,
string $type,
string $relation,
string $user,
?object $context = null,
?TupleKeysInterface $contextualTuples = null,
?Consistency $consistency = null,
): FailureInterface | SuccessInterface {
// Read operation - returns success in offline mode
if ($this->shouldFail(false)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(['objects' => []]);
}
#[Override]
public function writeAssertions(
StoreInterface | string $store,
AuthorizationModelInterface | string $model,
AssertionsInterface $assertions,
): FailureInterface | SuccessInterface {
// Write operation - returns failure in offline mode
if ($this->shouldFail(true)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(null);
}
#[Override]
public function writeTuples(
StoreInterface | string $store,
AuthorizationModelInterface | string $model,
?TupleKeysInterface $writes = null,
?TupleKeysInterface $deletes = null,
bool $transactional = true,
int $maxParallelRequests = 1,
int $maxTuplesPerChunk = 100,
int $maxRetries = 0,
float $retryDelaySeconds = 1.0,
bool $stopOnFirstError = false,
): FailureInterface | SuccessInterface {
// Write operation - returns failure in offline mode
if ($this->shouldFail(true)) {
return new Failure(new RuntimeException(self::ERROR_MESSAGE));
}
return new Success(null);
}
/**
* Determines if an operation should fail (write operations) or succeed (read operations).
*
* @param bool $isWriteOperation Whether the operation modifies state
* @return bool True if the operation should fail
*/
private function shouldFail(bool $isWriteOperation): bool
{
// In offline mode, write operations always fail, read operations always succeed
return $isWriteOperation;
}
}