cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / mcp / roots.go
roots.go 2.6K
1// The roots capability is deprecated (SEP-2577), but every protocol2// version keeps it for a year after 2026-07-28 and hosts in use share3// their project folders through it (Claude Code among them), so the4// server follows a client's roots where the client offers them, and5// Options.Roots otherwise. Clients of 2026-07-28 and later can't be6// asked in the middle of a call, so they get Options.Roots.7//8//lint:file-ignore SA1019 roots are deprecated, and followed for the hosts that still share them910package mcp1112import (13 "context"14 "net/url"15 "path/filepath"1617 sdk "github.com/modelcontextprotocol/go-sdk/mcp"1819 "src.rest/cat/012/internal/confine"20)2122// roots are the folders workbooks may be in for the client of ss: its23// roots when it shares any, else the server's.24func (s *Server) roots(ctx context.Context, ss *sdk.ServerSession) []confine.Root {25 if ss == nil {26 return s.fallback27 }28 s.mu.Lock()29 cached, ok := s.clientRoots[ss]30 s.mu.Unlock()31 if ok {32 return cached33 }34 var roots []confine.Root35 if p := ss.InitializeParams(); p != nil && p.Capabilities != nil && p.Capabilities.RootsV2 != nil {36 if res, err := ss.ListRoots(ctx, nil); err == nil {37 roots = rootsOf(res.Roots)38 }39 }40 if len(roots) == 0 {41 roots = s.fallback42 }43 s.mu.Lock()44 s.clientRoots[ss] = roots45 s.mu.Unlock()46 return roots47}4849// forgetRoots drops the roots kept for a client, which said they50// changed.51func (s *Server) forgetRoots(_ context.Context, req *sdk.RootsListChangedRequest) {52 s.mu.Lock()53 delete(s.clientRoots, req.Session)54 s.mu.Unlock()55}5657// serverOptions are the SDK's options for the server: its instructions,58// its MCP Apps extension, and forgetRoots.59func (s *Server) serverOptions() *sdk.ServerOptions {60 o := &sdk.ServerOptions{Instructions: instructions, RootsListChangedHandler: s.forgetRoots,61 Capabilities: &sdk.ServerCapabilities{Extensions: map[string]any{uiExtension: map[string]any{}}}}62 if s.opts.Live != nil {63 // Clients subscribe to the suggestions resource to hear what64 // became of theirs (SuggestionsChanged).65 o.Instructions = liveInstructions66 o.SubscribeHandler = func(context.Context, *sdk.SubscribeRequest) error { return nil }67 o.UnsubscribeHandler = func(context.Context, *sdk.UnsubscribeRequest) error { return nil }68 }69 return o70}7172// rootsOf are the folders of a client's file:// roots that exist.73func rootsOf(list []*sdk.Root) []confine.Root {74 var out []confine.Root75 for _, r := range list {76 u, err := url.Parse(r.URI)77 if err != nil || u.Scheme != "file" {78 continue79 }80 if root, err := confine.New(filepath.FromSlash(u.Path)); err == nil {81 out = append(out, root)82 }83 }84 return out85}