[tools] gopls/internal/mcp: only register watch for roots

0 views
Skip to first unread message

Gopher Robot (Gerrit)

unread,
Mar 17, 2026, 2:13:43 PM (yesterday) Mar 17
to Hongxiang Jiang, goph...@pubsubhelper.golang.org, golang-...@googlegroups.com, Go LUCI, Alan Donovan, Hyang-Ah Hana Kim, golang-co...@googlegroups.com

Gopher Robot submitted the change with unreviewed changes

Unreviewed changes

8 is the latest approved patch-set.
The change was submitted with unreviewed changes in the following files:

```
The name of the file: gopls/internal/cmd/mcp.go
Insertions: 4, Deletions: 3.

@@ -14,6 +14,7 @@
"sync"
"time"

+ "github.com/modelcontextprotocol/go-sdk/mcp"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/filewatcher"
internalmcp "golang.org/x/tools/gopls/internal/mcp"
@@ -155,14 +156,14 @@
// goroutine for it because WatchDir performs OS-level filesystem operations
// which can be slow. Blocking this callback would block the MCP server's
// JSON-RPC message loop and stall the entireconnection.
- watchRoots := func(roots []string, err error) {
+ watchRoots := func(res *mcp.ListRootsResult, err error) {
if err != nil {
errHandler(err)
return
}
watchQueueMu.Lock()
- for _, r := range roots {
- watchQueue = append(watchQueue, protocol.DocumentURI(r).Path())
+ for _, r := range res.Roots {
+ watchQueue = append(watchQueue, protocol.DocumentURI(r.URI).Path())
}
watchQueueMu.Unlock()

```
```
The name of the file: gopls/internal/mcp/mcp.go
Insertions: 9, Deletions: 18.

@@ -47,11 +47,11 @@
// caller is responsible for closing. The server runs until the context is
// canceled.
//
-// The rootsHandler callback is invoked immediately after initialization
-// and subsequently whenever the MCP client signals a change to the workspace
-// roots. It is passed the current list of roots returned by the MCP client,
-// or an error if the roots could not be retrieved.
-func Serve(ctx context.Context, address string, sessions Sessions, isDaemon bool, rootsHandler func([]string, error)) error {
+// The rootsHandler callback is invoked immediately after initialization and
+// subsequently whenever the MCP client signals a change to the workspace roots.
+// It is passed the list roots result returned by the MCP client, or an error
+// if the roots could not be retrieved. rootsHandler may be called concurrently.
+func Serve(ctx context.Context, address string, sessions Sessions, isDaemon bool, rootsHandler func(*mcp.ListRootsResult, error)) error {
log.Printf("Gopls MCP server: starting up on http")
listener, err := net.Listen("tcp", address)
if err != nil {
@@ -82,7 +82,7 @@
}

// StartStdIO starts an MCP server over stdio.
-func StartStdIO(ctx context.Context, session *cache.Session, server protocol.Server, rpcLog io.Writer, rootsHandler func([]string, error)) error {
+func StartStdIO(ctx context.Context, session *cache.Session, server protocol.Server, rpcLog io.Writer, rootsHandler func(*mcp.ListRootsResult, error)) error {
s := NewServer(session, server, rootsHandler)
if rpcLog != nil {
return s.Run(ctx, &mcp.LoggingTransport{
@@ -95,7 +95,7 @@

}

-func HTTPHandler(sessions Sessions, isDaemon bool, rootsHandler func([]string, error)) http.Handler {
+func HTTPHandler(sessions Sessions, isDaemon bool, rootsHandler func(*mcp.ListRootsResult, error)) http.Handler {
var (
mu sync.Mutex // lock for mcpHandlers.
mcpHandlers = make(map[string]*mcp.SSEHandler) // map from lsp session ids to MCP sse handlers.
@@ -160,7 +160,7 @@
return mux
}

-func NewServer(session *cache.Session, lspServer protocol.Server, rootsHandler func([]string, error)) *mcp.Server {
+func NewServer(session *cache.Session, lspServer protocol.Server, rootsHandler func(*mcp.ListRootsResult, error)) *mcp.Server {
h := handler{
session: session,
lspServer: lspServer,
@@ -252,16 +252,7 @@
}

roots, err := session.ListRoots(context.Background(), &mcp.ListRootsParams{})
- if err != nil {
- rootsHandler(nil, err)
- return
- }
-
- var uris []string
- for _, v := range roots.Roots {
- uris = append(uris, v.URI)
- }
- rootsHandler(uris, nil)
+ rootsHandler(roots, err)
}()
}

```
```
The name of the file: gopls/internal/mcp/mcp_test.go
Insertions: 5, Deletions: 5.

@@ -77,7 +77,7 @@

var callCount int

- server := internalmcp.NewServer(nil, nil, func(roots []string, err error) {
+ server := internalmcp.NewServer(nil, nil, func(res *mcp.ListRootsResult, err error) {
if err != nil {
foundError <- err
return
@@ -90,13 +90,13 @@

expected := wantRoots[callCount]

- if len(roots) != len(expected) {
- t.Errorf("Phase %d: expected %d roots, got %d", callCount+1, len(expected), len(roots))
+ if len(res.Roots) != len(expected) {
+ t.Errorf("Phase %d: expected %d roots, got %d", callCount+1, len(expected), len(res.Roots))
return
}

- for _, r := range roots {
- if _, ok := expected[r]; !ok {
+ for _, r := range res.Roots {
+ if _, ok := expected[r.URI]; !ok {
t.Errorf("Phase %d: unexpected root %s", callCount+1, r)
}
}
```

Change information

Commit message:
gopls/internal/mcp: only register watch for roots

Some of the MCP client spins up MCP server from the dir "/".
Previously, the gopls mcp server watch for the dir "os.Getwd"
so the gopls mcp server will try to watch for the entire file
system.

Instead, this CL change the behavior from registering the
watch for "os.Getwd" to registering the watch for roots from
MCP methods "roots/list". The gopls mcp server will request
the roots again after "roots/list_changed" notifications
and re-watch all the roots available.

For golang/go#77814 golang/go#76291
Change-Id: I934a6095bcad875ca569d343d0e9c4d08ce2b630
Reviewed-by: Alan Donovan <adon...@google.com>
Auto-Submit: Hongxiang Jiang <hxj...@golang.org>
Files:
  • M gopls/internal/cmd/mcp.go
  • M gopls/internal/cmd/serve.go
  • M gopls/internal/filewatcher/filewatcher.go
  • M gopls/internal/mcp/mcp.go
  • M gopls/internal/mcp/mcp_test.go
  • M gopls/internal/test/marker/marker_test.go
Change size: L
Delta: 6 files changed, 229 insertions(+), 24 deletions(-)
Branch: refs/heads/master
Submit Requirements:
  • requirement satisfiedCode-Review: +2 by Alan Donovan
  • requirement satisfiedTryBots-Pass: LUCI-TryBot-Result+1 by Go LUCI
Open in Gerrit
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: merged
Gerrit-Project: tools
Gerrit-Branch: master
Gerrit-Change-Id: I934a6095bcad875ca569d343d0e9c4d08ce2b630
Gerrit-Change-Number: 755020
Gerrit-PatchSet: 10
Gerrit-Owner: Hongxiang Jiang <hxj...@golang.org>
Gerrit-Reviewer: Alan Donovan <adon...@google.com>
Gerrit-Reviewer: Gopher Robot <go...@golang.org>
Gerrit-Reviewer: Hongxiang Jiang <hxj...@golang.org>
Gerrit-Reviewer: Hyang-Ah Hana Kim <hya...@gmail.com>
open
diffy
satisfied_requirement
Reply all
Reply to author
Forward
0 new messages