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:
| 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:
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
Example: Scoped search
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 January 27, 2026