Add roots capability (client workspace declaration) #10
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Add the Roots capability — client declares which filesystem/URL roots are in scope for this session, and the server reads them to scope its operations.
Goal
Today the
shell/read_file/search_filestools have no idea which directory the user considers "the project." They take an absolute path and trust the caller. Roots give the server first-class workspace awareness: it can refuse paths outside declared roots, defaultcwdto the first root, and surface root-aware tool descriptions.Methods to add
roots/list{ roots: [{ uri, name? }] }.uriisfile://….notifications/roots/list_changedroots/listagain.API for lmcp
Cache the result of
roots/listper session; invalidate on receipt ofnotifications/roots/list_changed.Capabilities (server declares it can use roots; client declares it can serve them)
Scope (v1)
roots/listrequest +notifications/roots/list_changedhandling.server:roots().server:path_in_roots(path)for opt-in path enforcement (does not auto-enforce — tools choose to call it).Out of scope
Depends on
roots/listto the client.Priority
Medium. Concrete safety win for the shell/file family once it lands. Transport-gated like Sampling.
Implemented. Builds on the bidirectional transport (#16) and follows the same pattern as #9 (sampling).
Added in lmcp.lua:
self._roots_cache[session_id]per-session cacheserver:roots(session_id, on_fetched)— firesroots/listrequest via SSE; callback receives(roots_list, err)when client responds; cache populated as side effectserver:roots_cached(session_id)— synchronous lookup; returns nil if no fetch has completedserver:path_in_roots(session_id, path)— synchronous: true/false/nil (nil = no cache yet, caller should:roots()first). Handles bothfile://URIs and bare paths uniformlynotifications/roots/list_changedfrom client → invalidates cache for the sending session. handle_request restructured to dispatch known notifications (side-effect) before the JSON-RPCid == nilearly return.server_requestnow omitsparamswhen nil or empty (avoids the{}→[]gotcha on the wire). Fixes spec-strict clients readingroots/listrequests.Verified end-to-end:
check_pathbefore any fetch →resultkey absent (caller treats as nil)server:roots(ctx.session_id, cb)→roots/listrequest appears on SSE with no paramscount=2, cache populatedcheck_path "/home/user/project/x.lua"→ true;"/var/log/x"→ falsenotifications/roots/list_changed→ no response (correctly suppressed); cache invalidatedcheck_path→ nil (cache cleared)Same handler-await limit as #9 (tool handler cannot block on the callback). For practical use today: prime the cache on session-start (via a tool the client always calls first), then synchronous
path_in_rootsworks in subsequent handlers. Full async patterns wait on #20.