ObjectCompletionProvider.php•4.83 kB
<?php
declare(strict_types=1);
namespace OpenFGA\MCP\Completions;
use OpenFGA\Models\TupleKey;
use OpenFGA\Responses\ReadTuplesResponseInterface;
use Override;
use PhpMcp\Server\Contracts\SessionInterface;
use Throwable;
use function array_slice;
use function assert;
final readonly class ObjectCompletionProvider extends AbstractCompletions
{
    /**
     * Get completion suggestions for object identifiers from OpenFGA relationship tuples.
     *
     * @param  string           $currentValue
     * @param  SessionInterface $session
     * @return array<string>
     */
    #[Override]
    public function getCompletions(string $currentValue, SessionInterface $session): array
    {
        // Return common object patterns in offline mode
        if ($this->isOffline()) {
            return $this->getCommonObjectPatterns($currentValue);
        }
        try {
            // Try to get store ID from session context
            $storeId = $this->extractStoreIdFromSession($session);
            if (null === $storeId) {
                return $this->getCommonObjectPatterns($currentValue);
            }
            // Check if access to this store is restricted
            if ($this->isRestricted($storeId)) {
                return [];
            }
            // Read relationship tuples to extract objects
            $objects = [];
            // Use wildcard tuple (empty strings act as wildcards) to read all tuples
            $tuple = new TupleKey(
                user: '',
                relation: '',
                object: '',
            );
            $this->client->readTuples(
                store: $storeId,
                tuple: $tuple,
                pageSize: 50,
            )
                ->failure(static function (): void {
                    // If we can't fetch tuples, objects will remain empty
                })
                ->success(static function (mixed $response) use (&$objects): void {
                    assert($response instanceof ReadTuplesResponseInterface);
                    $tuples = $response->getTuples();
                    foreach ($tuples as $tuple) {
                        $object = $tuple->getKey()->getObject();
                        if ('' !== $object) {
                            $objects[] = $object;
                        }
                    }
                });
            if ([] === $objects) {
                return $this->getCommonObjectPatterns($currentValue);
            }
            // Remove duplicates and sort
            $objects = array_unique($objects);
            sort($objects);
            // Limit to reasonable number for performance
            $objects = array_slice($objects, 0, 50);
            return $this->filterCompletions($objects, $currentValue);
        } catch (Throwable) {
            // Handle any unexpected errors gracefully
            return $this->getCommonObjectPatterns($currentValue);
        }
    }
    /**
     * Get common object identifier patterns as fallback.
     *
     * @param  string        $currentValue
     * @return array<string>
     */
    private function getCommonObjectPatterns(string $currentValue): array
    {
        // If the value already has a type prefix, provide common ID suggestions
        if (str_contains($currentValue, ':')) {
            [$type] = explode(':', $currentValue, 2);
            // Provide common ID patterns based on the type
            $suggestions = match ($type) {
                'document', 'doc' => [
                    $type . ':budget',
                    $type . ':plan',
                    $type . ':report',
                    $type . ':proposal',
                ],
                'folder' => [
                    $type . ':root',
                    $type . ':shared',
                    $type . ':public',
                    $type . ':private',
                ],
                'user' => [
                    $type . ':alice',
                    $type . ':bob',
                    $type . ':admin',
                ],
                'group' => [
                    $type . ':admins',
                    $type . ':editors',
                    $type . ':viewers',
                ],
                // For unknown types, suggest generic IDs
                default => [
                    $type . ':1',
                    $type . ':default',
                    $type . ':main',
                ],
            };
            return $this->filterCompletions($suggestions, $currentValue);
        }
        // If no type prefix, suggest common type prefixes
        $commonPatterns = [
            'document:',
            'doc:',
            'folder:',
            'user:',
            'group:',
        ];
        return $this->filterCompletions($commonPatterns, $currentValue);
    }
}