> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dedaluslabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Roots

> Access client filesystem boundaries

Roots are filesystem boundaries **advertised by the client**. Servers can use roots to understand what parts of the client's filesystem are intended to be in-scope (for example, "this project folder"), and to **enforce guardrails** when reading or writing files.

In MCP, Roots are a **client capability** (`roots/list`). Dedalus MCP provides a server-side `RootsService` that:

* fetches roots from the client,
* caches a per-session snapshot, and
* offers a `RootGuard` helper for path checks.

## Basic usage

Fetch the latest roots for the current session, then use them:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from dedalus_mcp import get_context, tool

@tool(description="List client roots")
async def list_roots() -> list[str]:
    ctx = get_context()
    server = ctx.server
    if server is None:
        raise RuntimeError("No active server in context")

    roots = await server.roots.refresh(ctx.session)  # fetch from client
    return [f"{r.name}: {r.uri}" for r in roots]
```

You don't need to re-fetch every time, you can read the cached snapshot:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
roots = ctx.server.roots.snapshot(ctx.session)
```

***

## Root structure

Each root contains:

| Field  | Type  | Description                          |
| ------ | ----- | ------------------------------------ |
| `uri`  | `str` | Root URI (typically a `file://` URI) |
| `name` | `str` | Human-readable name                  |

***

## Example: Safe file operations (RootGuard)

Use `RootGuard` to check whether a path is inside one of the allowed roots:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from pathlib import Path
from dedalus_mcp import get_context, tool

@tool(description="Read a file, restricted to roots")
async def safe_read(filepath: str) -> str:
    ctx = get_context()
    server = ctx.server
    if server is None:
        raise RuntimeError("No active server in context")

    # Make sure we have an up-to-date snapshot
    await server.roots.refresh(ctx.session)

    guard = server.roots.guard(ctx.session)
    target = Path(filepath).expanduser().resolve()

    if not guard.within(target):
        raise ValueError("Path is outside allowed roots")

    return target.read_text(encoding="utf-8")
```

***

## Example: Project discovery (file:// roots)

If your client roots are `file://...` URIs, you can walk them to discover projects.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from pathlib import Path
from urllib.parse import urlparse, unquote

from dedalus_mcp import get_context, tool

def file_uri_to_path(uri: str) -> Path:
    parsed = urlparse(uri)
    if parsed.scheme != "file":
        raise ValueError(f"Unsupported root scheme: {parsed.scheme!r}")
    return Path(unquote(parsed.path)).expanduser().resolve()

@tool(description="Find project roots by marker files")
async def find_projects() -> list[dict]:
    ctx = get_context()
    server = ctx.server
    if server is None:
        raise RuntimeError("No active server in context")

    roots = await server.roots.refresh(ctx.session)
    projects: list[dict] = []

    for root in roots:
        root_path = file_uri_to_path(str(root.uri))

        for marker in ["package.json", "pyproject.toml", "Cargo.toml"]:
            if (root_path / marker).exists():
                projects.append(
                    {
                        "root": root.name,
                        "path": str(root_path),
                        "type": marker,
                    }
                )

    return projects
```

***

## Example: Scoped search

Search only within roots (and log what you're doing):

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from pathlib import Path
from urllib.parse import urlparse, unquote

from dedalus_mcp import get_context, tool

def file_uri_to_path(uri: str) -> Path:
    parsed = urlparse(uri)
    if parsed.scheme != "file":
        raise ValueError(f"Unsupported root scheme: {parsed.scheme!r}")
    return Path(unquote(parsed.path)).expanduser().resolve()

@tool(description="Search for files within roots")
async def search_files(pattern: str) -> list[str]:
    ctx = get_context()
    server = ctx.server
    if server is None:
        raise RuntimeError("No active server in context")

    roots = await server.roots.refresh(ctx.session)
    await ctx.info("Searching roots", data={"roots": len(roots), "pattern": pattern})

    matches: list[str] = []
    for root in roots:
        await ctx.debug("Searching root", data={"root": root.name, "uri": str(root.uri)})
        root_path = file_uri_to_path(str(root.uri))

        for match in root_path.rglob(pattern):
            matches.append(str(match))

    await ctx.info("Search complete", data={"matches": len(matches)})
    return matches
```

***

## Notes

* **Caching**: `server.roots.snapshot(session)` returns the cached roots. `await server.roots.refresh(session)` updates the cache by calling the client.
* **Client-driven updates**: If the client sends `roots/list_changed`, Dedalus MCP updates the snapshot (debounced) for that session automatically.
* **Security**: Roots are guidance + a boundary for your own checks. If you're doing file I/O, always enforce a guard (`RootGuard.within(...)`) before reading/writing.
