Skip to content

Roots Management

Learn how to implement and use filesystem roots in MCP clients to control server access to directories and files.

Overview

Roots in MCP define the boundaries of where servers can operate within the filesystem. They allow clients to expose specific directories and files to servers while maintaining security and control. Based on the MCP specification, roots provide:

  • Controlled Access: Define which directories servers can access
  • Security Boundaries: Prevent unauthorized file system access
  • Dynamic Updates: Change available roots during runtime
  • User Control: Let users decide what to expose

Client Implementation

Declaring Roots Capability

Clients that support roots must declare the capability during initialization:

php
use MCP\Client\Client;
use MCP\Types\Implementation;

$client = new Client(
    new Implementation('my-client', '1.0.0'),
    [
        'capabilities' => [
            'roots' => [
                'listChanged' => true // Support change notifications
            ]
        ]
    ]
);

Setting Up Roots

php
class RootsManager
{
    private Client $client;
    private array $roots = [];
    private array $watchers = [];

    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    public function addRoot(string $uri, string $name): void
    {
        // Validate URI format
        if (!$this->isValidFileUri($uri)) {
            throw new \InvalidArgumentException("Invalid file URI: {$uri}");
        }

        // Check if path exists and is accessible
        $path = $this->uriToPath($uri);
        if (!file_exists($path) || !is_readable($path)) {
            throw new \InvalidArgumentException("Path not accessible: {$path}");
        }

        $this->roots[] = [
            'uri' => $uri,
            'name' => $name
        ];

        // Set up file system watcher for changes
        $this->setupWatcher($uri);

        // Notify servers of root list change
        $this->notifyRootListChanged();
    }

    public function removeRoot(string $uri): void
    {
        $this->roots = array_filter(
            $this->roots,
            fn($root) => $root['uri'] !== $uri
        );

        // Remove watcher
        if (isset($this->watchers[$uri])) {
            $this->watchers[$uri]->stop();
            unset($this->watchers[$uri]);
        }

        $this->notifyRootListChanged();
    }

    public function listRoots(): array
    {
        return [
            'roots' => array_values($this->roots)
        ];
    }

    private function setupWatcher(string $uri): void
    {
        $path = $this->uriToPath($uri);

        // Set up file system watcher (using inotify or similar)
        $watcher = new FileSystemWatcher($path);

        $watcher->onChanged(function() use ($uri) {
            $this->notifyRootListChanged();
        });

        $this->watchers[$uri] = $watcher;
    }

    private function notifyRootListChanged(): void
    {
        // Send notification to all connected servers
        $this->client->sendNotification('notifications/roots/list_changed');
    }

    private function isValidFileUri(string $uri): bool
    {
        return preg_match('/^file:\/\/\/.*/', $uri) === 1;
    }

    private function uriToPath(string $uri): string
    {
        return urldecode(substr($uri, 7)); // Remove 'file://' prefix
    }
}

Server-Side Roots Usage

Requesting Roots from Client

php
class RootsAwareServer
{
    private McpServer $server;
    private array $availableRoots = [];

    public function __construct()
    {
        $this->server = new McpServer(
            new Implementation('roots-server', '1.0.0')
        );

        $this->registerRootsTools();
    }

    private function registerRootsTools(): void
    {
        // Tool to list available roots
        $this->server->tool(
            'list_available_roots',
            'List filesystem roots available to this server',
            ['type' => 'object', 'properties' => []],
            function (): array {
                // Request roots from client
                $result = $this->requestRootsFromClient();

                return [
                    'content' => [[
                        'type' => 'text',
                        'text' => json_encode($result, JSON_PRETTY_PRINT)
                    ]]
                ];
            }
        );

        // Tool to list files in a root
        $this->server->tool(
            'list_files_in_root',
            'List files in a specific root directory',
            [
                'type' => 'object',
                'properties' => [
                    'root_uri' => [
                        'type' => 'string',
                        'description' => 'URI of the root to list files from'
                    ],
                    'recursive' => [
                        'type' => 'boolean',
                        'default' => false,
                        'description' => 'List files recursively'
                    ]
                ],
                'required' => ['root_uri']
            ],
            function (array $params): array {
                $rootUri = $params['root_uri'];
                $recursive = $params['recursive'] ?? false;

                // Validate root is available
                if (!$this->isRootAvailable($rootUri)) {
                    throw new McpError(
                        ErrorCode::InvalidParams,
                        "Root not available: {$rootUri}"
                    );
                }

                $files = $this->listFilesInRoot($rootUri, $recursive);

                return [
                    'content' => [[
                        'type' => 'text',
                        'text' => json_encode([
                            'root_uri' => $rootUri,
                            'file_count' => count($files),
                            'files' => $files
                        ], JSON_PRETTY_PRINT)
                    ]]
                ];
            }
        );
    }

    private function requestRootsFromClient(): array
    {
        // Send roots/list request to client
        $request = [
            'jsonrpc' => '2.0',
            'id' => uniqid(),
            'method' => 'roots/list'
        ];

        $response = $this->sendRequestToClient($request);

        if (isset($response['result']['roots'])) {
            $this->availableRoots = $response['result']['roots'];
            return $response['result'];
        }

        throw new McpError(
            ErrorCode::InternalError,
            'Failed to get roots from client'
        );
    }

    private function isRootAvailable(string $uri): bool
    {
        foreach ($this->availableRoots as $root) {
            if ($root['uri'] === $uri) {
                return true;
            }
        }
        return false;
    }

    private function listFilesInRoot(string $rootUri, bool $recursive): array
    {
        $rootPath = $this->uriToPath($rootUri);
        $files = [];

        if ($recursive) {
            $iterator = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($rootPath)
            );
        } else {
            $iterator = new \DirectoryIterator($rootPath);
        }

        foreach ($iterator as $fileInfo) {
            if ($fileInfo->isDot()) {
                continue;
            }

            $files[] = [
                'name' => $fileInfo->getFilename(),
                'path' => $fileInfo->getPathname(),
                'uri' => $this->pathToUri($fileInfo->getPathname()),
                'type' => $fileInfo->isDir() ? 'directory' : 'file',
                'size' => $fileInfo->getSize(),
                'modified' => $fileInfo->getMTime()
            ];
        }

        return $files;
    }

    private function uriToPath(string $uri): string
    {
        return urldecode(substr($uri, 7)); // Remove 'file://' prefix
    }

    private function pathToUri(string $path): string
    {
        return 'file://' . urlencode($path);
    }
}

Practical Examples

Project-Based Roots

php
class ProjectRootsManager
{
    private RootsManager $rootsManager;
    private array $projects = [];

    public function addProject(string $name, string $path): void
    {
        $uri = 'file://' . realpath($path);
        $this->rootsManager->addRoot($uri, $name);

        $this->projects[$name] = [
            'path' => $path,
            'uri' => $uri,
            'added_at' => time()
        ];

        echo "📁 Added project root: {$name} -> {$path}\n";
    }

    public function detectProjectRoots(): array
    {
        $detectedProjects = [];
        $searchPaths = [
            $_SERVER['HOME'] . '/projects',
            $_SERVER['HOME'] . '/workspace',
            $_SERVER['HOME'] . '/code'
        ];

        foreach ($searchPaths as $searchPath) {
            if (!is_dir($searchPath)) {
                continue;
            }

            foreach (new \DirectoryIterator($searchPath) as $dir) {
                if ($dir->isDot() || !$dir->isDir()) {
                    continue;
                }

                $projectPath = $dir->getPathname();

                // Check for project indicators
                if ($this->isProjectDirectory($projectPath)) {
                    $detectedProjects[] = [
                        'name' => $dir->getFilename(),
                        'path' => $projectPath,
                        'type' => $this->detectProjectType($projectPath)
                    ];
                }
            }
        }

        return $detectedProjects;
    }

    private function isProjectDirectory(string $path): bool
    {
        $projectIndicators = [
            'composer.json',    // PHP project
            'package.json',     // Node.js project
            'requirements.txt', // Python project
            'go.mod',          // Go project
            '.git',            // Git repository
            'Makefile',        // Make-based project
            'Dockerfile'       // Docker project
        ];

        foreach ($projectIndicators as $indicator) {
            if (file_exists($path . DIRECTORY_SEPARATOR . $indicator)) {
                return true;
            }
        }

        return false;
    }

    private function detectProjectType(string $path): string
    {
        if (file_exists($path . '/composer.json')) return 'php';
        if (file_exists($path . '/package.json')) return 'nodejs';
        if (file_exists($path . '/requirements.txt')) return 'python';
        if (file_exists($path . '/go.mod')) return 'go';
        if (file_exists($path . '/Cargo.toml')) return 'rust';
        if (file_exists($path . '/.git')) return 'git';

        return 'unknown';
    }
}

Repository-Based Roots

php
class RepositoryRootsManager
{
    public function addRepositoryRoots(array $repositories): void
    {
        foreach ($repositories as $repo) {
            $this->addRepositoryRoot($repo);
        }
    }

    private function addRepositoryRoot(array $repo): void
    {
        $path = $repo['path'];
        $name = $repo['name'] ?? basename($path);

        // Validate repository
        if (!$this->isValidRepository($path)) {
            throw new \InvalidArgumentException("Invalid repository: {$path}");
        }

        $uri = 'file://' . realpath($path);
        $this->rootsManager->addRoot($uri, $name);

        echo "🔗 Added repository root: {$name}\n";
        echo "  Path: {$path}\n";
        echo "  Type: " . $this->getRepositoryType($path) . "\n";
        echo "  Branch: " . $this->getCurrentBranch($path) . "\n";
    }

    private function isValidRepository(string $path): bool
    {
        return is_dir($path . '/.git') ||
               is_dir($path . '/.svn') ||
               is_dir($path . '/.hg');
    }

    private function getCurrentBranch(string $path): string
    {
        $gitHead = $path . '/.git/HEAD';

        if (file_exists($gitHead)) {
            $head = trim(file_get_contents($gitHead));
            if (preg_match('/ref: refs\/heads\/(.+)/', $head, $matches)) {
                return $matches[1];
            }
        }

        return 'unknown';
    }
}

Security Considerations

Path Validation

php
class SecureRootsValidator
{
    private array $allowedBasePaths;
    private array $deniedPaths;

    public function __construct(array $allowedBasePaths, array $deniedPaths = [])
    {
        $this->allowedBasePaths = array_map('realpath', $allowedBasePaths);
        $this->deniedPaths = array_map('realpath', $deniedPaths);
    }

    public function validateRoot(string $uri): bool
    {
        $path = $this->uriToPath($uri);
        $realPath = realpath($path);

        if (!$realPath) {
            return false; // Path doesn't exist
        }

        // Check against denied paths
        foreach ($this->deniedPaths as $deniedPath) {
            if ($deniedPath && strpos($realPath, $deniedPath) === 0) {
                return false;
            }
        }

        // Check against allowed base paths
        foreach ($this->allowedBasePaths as $allowedPath) {
            if ($allowedPath && strpos($realPath, $allowedPath) === 0) {
                return true;
            }
        }

        return false;
    }

    public function sanitizeRoots(array $roots): array
    {
        $sanitized = [];

        foreach ($roots as $root) {
            if ($this->validateRoot($root['uri'])) {
                $sanitized[] = [
                    'uri' => $root['uri'],
                    'name' => htmlspecialchars($root['name'], ENT_QUOTES, 'UTF-8')
                ];
            }
        }

        return $sanitized;
    }

    private function uriToPath(string $uri): string
    {
        if (!preg_match('/^file:\/\/(.*)$/', $uri, $matches)) {
            throw new \InvalidArgumentException("Invalid file URI: {$uri}");
        }

        return urldecode($matches[1]);
    }
}

Access Control

php
class RootsAccessControl
{
    private array $userPermissions = [];
    private array $rootPermissions = [];

    public function setUserPermissions(string $userId, array $permissions): void
    {
        $this->userPermissions[$userId] = $permissions;
    }

    public function setRootPermissions(string $rootUri, array $permissions): void
    {
        $this->rootPermissions[$rootUri] = $permissions;
    }

    public function canUserAccessRoot(string $userId, string $rootUri, string $operation): bool
    {
        // Check user-level permissions
        $userPerms = $this->userPermissions[$userId] ?? [];
        if (!in_array($operation, $userPerms)) {
            return false;
        }

        // Check root-level permissions
        $rootPerms = $this->rootPermissions[$rootUri] ?? ['read', 'write'];
        if (!in_array($operation, $rootPerms)) {
            return false;
        }

        return true;
    }

    public function filterRootsForUser(string $userId, array $roots): array
    {
        return array_filter($roots, function($root) use ($userId) {
            return $this->canUserAccessRoot($userId, $root['uri'], 'read');
        });
    }
}

Complete Roots Example

File Manager MCP Client

php
#!/usr/bin/env php
<?php

require_once __DIR__ . '/vendor/autoload.php';

use MCP\Client\Client;
use MCP\Client\Transport\StdioClientTransport;
use MCP\Types\Implementation;
use MCP\Types\McpError;
use function Amp\async;

class FileManagerClient
{
    private Client $client;
    private RootsManager $rootsManager;
    private SecureRootsValidator $validator;

    public function __construct()
    {
        $this->client = new Client(
            new Implementation('file-manager-client', '1.0.0'),
            [
                'capabilities' => [
                    'roots' => [
                        'listChanged' => true
                    ]
                ]
            ]
        );

        $this->rootsManager = new RootsManager($this->client);

        // Set up security validator
        $this->validator = new SecureRootsValidator(
            ['/home/user/projects', '/home/user/documents'], // Allowed base paths
            ['/home/user/.ssh', '/etc', '/var'] // Denied paths
        );

        $this->setupDefaultRoots();
    }

    private function setupDefaultRoots(): void
    {
        // Auto-detect project directories
        $projectManager = new ProjectRootsManager($this->rootsManager);
        $detectedProjects = $projectManager->detectProjectRoots();

        foreach ($detectedProjects as $project) {
            try {
                $uri = 'file://' . realpath($project['path']);

                if ($this->validator->validateRoot($uri)) {
                    $this->rootsManager->addRoot($uri, $project['name']);
                    echo "✅ Added detected project: {$project['name']}\n";
                }
            } catch (\Exception $e) {
                echo "⚠️ Skipped project {$project['name']}: {$e->getMessage()}\n";
            }
        }
    }

    public function connectToFileServer(string $serverPath): void
    {
        async(function () use ($serverPath) {
            try {
                echo "🔌 Connecting to file server...\n";

                $transport = new StdioClientTransport([
                    'command' => 'php',
                    'args' => [$serverPath]
                ]);

                $this->client->connect($transport)->await();

                echo "✅ Connected to file server\n";

                // Demonstrate roots functionality
                $this->demonstrateRootsFeatures();

            } catch (\Exception $e) {
                echo "❌ Connection failed: {$e->getMessage()}\n";
            }
        })->await();
    }

    private function demonstrateRootsFeatures(): void
    {
        async(function () {
            try {
                // List available roots
                echo "\n📁 Available Roots:\n";
                $roots = $this->rootsManager->listRoots();

                foreach ($roots['roots'] as $root) {
                    echo "  - {$root['name']}: {$root['uri']}\n";
                }

                // Call server tool that uses roots
                if (!empty($roots['roots'])) {
                    $firstRoot = $roots['roots'][0];

                    echo "\n🔍 Listing files in root: {$firstRoot['name']}\n";

                    $result = $this->client->callTool('list_files_in_root', [
                        'root_uri' => $firstRoot['uri'],
                        'recursive' => false
                    ])->await();

                    $fileData = json_decode($result['content'][0]['text'], true);
                    echo "Found {$fileData['file_count']} items\n";

                    foreach (array_slice($fileData['files'], 0, 5) as $file) {
                        echo "  - {$file['name']} ({$file['type']})\n";
                    }
                }

            } catch (\Exception $e) {
                echo "❌ Roots demonstration failed: {$e->getMessage()}\n";
            }
        })->await();
    }

    public function addCustomRoot(string $path, string $name): void
    {
        try {
            $uri = 'file://' . realpath($path);

            if (!$this->validator->validateRoot($uri)) {
                throw new \InvalidArgumentException("Root validation failed for: {$path}");
            }

            $this->rootsManager->addRoot($uri, $name);
            echo "✅ Added custom root: {$name} -> {$path}\n";

        } catch (\Exception $e) {
            echo "❌ Failed to add root: {$e->getMessage()}\n";
        }
    }
}

// Usage example
$client = new FileManagerClient();

// Add custom roots
$client->addCustomRoot('/home/user/my-special-project', 'Special Project');
$client->addCustomRoot('/home/user/shared-docs', 'Shared Documents');

// Connect to a file server that can use these roots
$client->connectToFileServer('./file-server.php');

User Interface Integration

Web-Based Roots Manager

php
class WebRootsInterface
{
    private RootsManager $rootsManager;

    public function handleRootsRequest(Request $request): JsonResponse
    {
        return match($request->getMethod()) {
            'GET' => $this->listRoots(),
            'POST' => $this->addRoot($request),
            'DELETE' => $this->removeRoot($request),
            default => response()->json(['error' => 'Method not allowed'], 405)
        };
    }

    private function listRoots(): JsonResponse
    {
        $roots = $this->rootsManager->listRoots();

        return response()->json([
            'roots' => $roots['roots'],
            'count' => count($roots['roots'])
        ]);
    }

    private function addRoot(Request $request): JsonResponse
    {
        $request->validate([
            'path' => 'required|string',
            'name' => 'required|string|max:255'
        ]);

        try {
            $path = $request->input('path');
            $name = $request->input('name');

            // Security validation
            if (!$this->isPathSafe($path)) {
                return response()->json([
                    'error' => 'Path not allowed for security reasons'
                ], 403);
            }

            $uri = 'file://' . realpath($path);
            $this->rootsManager->addRoot($uri, $name);

            return response()->json([
                'success' => true,
                'message' => "Root '{$name}' added successfully",
                'root' => ['uri' => $uri, 'name' => $name]
            ]);

        } catch (\Exception $e) {
            return response()->json([
                'error' => $e->getMessage()
            ], 400);
        }
    }
}

Best Practices

1. Security

  • Validate all paths before exposing as roots
  • Implement access controls based on user permissions
  • Monitor root access and log suspicious activity
  • Use allowlists rather than denylists for path validation

2. User Experience

  • Auto-detect projects and suggest them as roots
  • Provide clear descriptions of what each root contains
  • Allow easy addition/removal of roots through UI
  • Show which servers are using which roots

3. Performance

  • Cache root listings to avoid repeated filesystem operations
  • Use efficient file watchers for change detection
  • Limit recursive operations to prevent performance issues
  • Implement pagination for large directory listings

4. Error Handling

  • Handle missing directories gracefully
  • Validate permissions before operations
  • Provide helpful error messages for path issues
  • Implement fallback mechanisms for inaccessible roots

Testing

Roots Testing

php
class RootsTest extends TestCase
{
    private RootsManager $rootsManager;
    private string $testDir;

    protected function setUp(): void
    {
        $this->testDir = sys_get_temp_dir() . '/mcp_roots_test_' . uniqid();
        mkdir($this->testDir, 0755, true);

        $this->rootsManager = new RootsManager($this->createMockClient());
    }

    protected function tearDown(): void
    {
        $this->removeDirectory($this->testDir);
    }

    public function testAddRoot(): void
    {
        $uri = 'file://' . $this->testDir;
        $this->rootsManager->addRoot($uri, 'Test Root');

        $roots = $this->rootsManager->listRoots();

        $this->assertCount(1, $roots['roots']);
        $this->assertEquals($uri, $roots['roots'][0]['uri']);
        $this->assertEquals('Test Root', $roots['roots'][0]['name']);
    }

    public function testInvalidPath(): void
    {
        $this->expectException(\InvalidArgumentException::class);

        $this->rootsManager->addRoot('file:///nonexistent/path', 'Invalid Root');
    }
}

See Also

Released under the MIT License.