Skip to main content

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 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:
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:
roots = ctx.server.roots.snapshot(ctx.session)

Root structure

Each root contains:
FieldTypeDescription
uristrRoot URI (typically a file:// URI)
namestrHuman-readable name

Example: Safe file operations (RootGuard)

Use RootGuard to check whether a path is inside one of the allowed roots:
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.
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

Search only within roots (and log what you’re doing):
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.
Last modified on May 29, 2026