Skip to content

Commit 197c6c1

Browse files
committed
gopls/internal/mcp: more tuning of tools and prompts
After further testing, the go_file_metadata tool does not seem particularly useful replace it with a go_file_context tool, which summarizes APIs used within the file. For golang/go#73580 Change-Id: I8a41f916b7f2a71dac5b6b800088be595ecd8c21 Reviewed-on: https://go-review.googlesource.com/c/tools/+/687355 Reviewed-by: Madeline Kalil <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Hongxiang Jiang <[email protected]>
1 parent 9563af6 commit 197c6c1

File tree

9 files changed

+285
-89
lines changed

9 files changed

+285
-89
lines changed

gopls/internal/cmd/mcp_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ func MyFun() {}
231231
}()
232232

233233
var (
234-
tool = "go_file_metadata"
234+
tool = "go_file_context"
235235
args = map[string]any{"file": filepath.Join(tree, "a.go")}
236236
)
237237
res, err := mcpSession.CallTool(ctx, &mcp.CallToolParams{Name: tool, Arguments: args})

gopls/internal/mcp/context.go

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func (h *handler) contextHandler(ctx context.Context, _ *mcp.ServerSession, para
6666
{
6767
fmt.Fprintf(&result, "%s (current file):\n", pgf.URI.Base())
6868
result.WriteString("```go\n")
69-
if err := writeFileSummary(ctx, snapshot, pgf.URI, &result, false); err != nil {
69+
if err := writeFileSummary(ctx, snapshot, pgf.URI, &result, false, nil); err != nil {
7070
return nil, err
7171
}
7272
result.WriteString("```\n\n")
@@ -81,7 +81,7 @@ func (h *handler) contextHandler(ctx context.Context, _ *mcp.ServerSession, para
8181

8282
fmt.Fprintf(&result, "%s:\n", file.URI.Base())
8383
result.WriteString("```go\n")
84-
if err := writeFileSummary(ctx, snapshot, file.URI, &result, false); err != nil {
84+
if err := writeFileSummary(ctx, snapshot, file.URI, &result, false, nil); err != nil {
8585
return nil, err
8686
}
8787
result.WriteString("```\n\n")
@@ -153,7 +153,7 @@ func summarizePackage(ctx context.Context, snapshot *cache.Snapshot, md *metadat
153153
for _, f := range md.CompiledGoFiles {
154154
fmt.Fprintf(&buf, "%s:\n", f.Base())
155155
buf.WriteString("```go\n")
156-
if err := writeFileSummary(ctx, snapshot, f, &buf, true); err != nil {
156+
if err := writeFileSummary(ctx, snapshot, f, &buf, true, nil); err != nil {
157157
return "" // ignore error
158158
}
159159
buf.WriteString("```\n\n")
@@ -163,7 +163,7 @@ func summarizePackage(ctx context.Context, snapshot *cache.Snapshot, md *metadat
163163

164164
// writeFileSummary writes the file summary to the string builder based on
165165
// the input file URI.
166-
func writeFileSummary(ctx context.Context, snapshot *cache.Snapshot, f protocol.DocumentURI, out *strings.Builder, onlyExported bool) error {
166+
func writeFileSummary(ctx context.Context, snapshot *cache.Snapshot, f protocol.DocumentURI, out *strings.Builder, onlyExported bool, declsToSummarize map[string]bool) error {
167167
fh, err := snapshot.ReadFile(ctx, f)
168168
if err != nil {
169169
return err
@@ -173,52 +173,60 @@ func writeFileSummary(ctx context.Context, snapshot *cache.Snapshot, f protocol.
173173
return err
174174
}
175175

176-
// Copy everything before the first non-import declaration:
177-
// package decl, imports decl(s), and all comments (excluding copyright).
178-
{
179-
endPos := pgf.File.FileEnd
176+
// If we're summarizing specific declarations, we don't need to copy the header.
177+
if declsToSummarize == nil {
178+
// Copy everything before the first non-import declaration:
179+
// package decl, imports decl(s), and all comments (excluding copyright).
180+
{
181+
endPos := pgf.File.FileEnd
180182

181-
outerloop:
182-
for _, decl := range pgf.File.Decls {
183-
switch decl := decl.(type) {
184-
case *ast.FuncDecl:
185-
if decl.Doc != nil {
186-
endPos = decl.Doc.Pos()
187-
} else {
188-
endPos = decl.Pos()
189-
}
190-
break outerloop
191-
case *ast.GenDecl:
192-
if decl.Tok == token.IMPORT {
193-
continue
194-
}
195-
if decl.Doc != nil {
196-
endPos = decl.Doc.Pos()
197-
} else {
198-
endPos = decl.Pos()
183+
outerloop:
184+
for _, decl := range pgf.File.Decls {
185+
switch decl := decl.(type) {
186+
case *ast.FuncDecl:
187+
if decl.Doc != nil {
188+
endPos = decl.Doc.Pos()
189+
} else {
190+
endPos = decl.Pos()
191+
}
192+
break outerloop
193+
case *ast.GenDecl:
194+
if decl.Tok == token.IMPORT {
195+
continue
196+
}
197+
if decl.Doc != nil {
198+
endPos = decl.Doc.Pos()
199+
} else {
200+
endPos = decl.Pos()
201+
}
202+
break outerloop
199203
}
200-
break outerloop
201204
}
202-
}
203205

204-
startPos := pgf.File.FileStart
205-
if copyright := golang.CopyrightComment(pgf.File); copyright != nil {
206-
startPos = copyright.End()
207-
}
206+
startPos := pgf.File.FileStart
207+
if copyright := golang.CopyrightComment(pgf.File); copyright != nil {
208+
startPos = copyright.End()
209+
}
208210

209-
text, err := pgf.PosText(startPos, endPos)
210-
if err != nil {
211-
return err
212-
}
211+
text, err := pgf.PosText(startPos, endPos)
212+
if err != nil {
213+
return err
214+
}
213215

214-
out.Write(bytes.TrimSpace(text))
215-
out.WriteString("\n\n")
216+
out.Write(bytes.TrimSpace(text))
217+
out.WriteString("\n\n")
218+
}
216219
}
217220

218221
// Write func decl and gen decl.
219222
for _, decl := range pgf.File.Decls {
220223
switch decl := decl.(type) {
221224
case *ast.FuncDecl:
225+
if declsToSummarize != nil {
226+
if _, ok := declsToSummarize[decl.Name.Name]; !ok {
227+
continue
228+
}
229+
}
222230
if onlyExported {
223231
if !decl.Name.IsExported() {
224232
continue
@@ -251,6 +259,28 @@ func writeFileSummary(ctx context.Context, snapshot *cache.Snapshot, f protocol.
251259
continue
252260
}
253261

262+
// If we are summarizing specific decls, check if any of them are in this GenDecl.
263+
if declsToSummarize != nil {
264+
found := false
265+
for _, spec := range decl.Specs {
266+
switch spec := spec.(type) {
267+
case *ast.TypeSpec:
268+
if _, ok := declsToSummarize[spec.Name.Name]; ok {
269+
found = true
270+
}
271+
case *ast.ValueSpec:
272+
for _, name := range spec.Names {
273+
if _, ok := declsToSummarize[name.Name]; ok {
274+
found = true
275+
}
276+
}
277+
}
278+
}
279+
if !found {
280+
continue
281+
}
282+
}
283+
254284
// Dump the entire GenDecl (exported or unexported)
255285
// including doc comment without any filtering to the output.
256286
if !onlyExported {
@@ -301,6 +331,11 @@ func writeFileSummary(ctx context.Context, snapshot *cache.Snapshot, f protocol.
301331

302332
switch spec := spec.(type) {
303333
case *ast.TypeSpec:
334+
if declsToSummarize != nil {
335+
if _, ok := declsToSummarize[spec.Name.Name]; !ok {
336+
continue
337+
}
338+
}
304339
// TODO(hxjiang): only keep the exported field of
305340
// struct spec and exported method of interface spec.
306341
if !spec.Name.IsExported() {
@@ -323,6 +358,17 @@ func writeFileSummary(ctx context.Context, snapshot *cache.Snapshot, f protocol.
323358
}
324359

325360
case *ast.ValueSpec:
361+
if declsToSummarize != nil {
362+
found := false
363+
for _, name := range spec.Names {
364+
if _, ok := declsToSummarize[name.Name]; ok {
365+
found = true
366+
}
367+
}
368+
if !found {
369+
continue
370+
}
371+
}
326372
// TODO(hxjiang): only keep the exported identifier.
327373
if !slices.ContainsFunc(spec.Names, (*ast.Ident).IsExported) {
328374
continue

gopls/internal/mcp/file_context.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package mcp
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"go/ast"
11+
"go/types"
12+
"strings"
13+
14+
"golang.org/x/tools/gopls/internal/golang"
15+
"golang.org/x/tools/gopls/internal/protocol"
16+
"golang.org/x/tools/internal/mcp"
17+
)
18+
19+
type fileContextParams struct {
20+
File string `json:"file"`
21+
}
22+
23+
func (h *handler) fileContextTool() *mcp.ServerTool {
24+
return mcp.NewServerTool(
25+
"go_file_context",
26+
"Summarizes a file's cross-file dependencies",
27+
h.fileContextHandler,
28+
mcp.Input(
29+
mcp.Property("file", mcp.Description("the absolute path to the file")),
30+
),
31+
)
32+
}
33+
34+
func (h *handler) fileContextHandler(ctx context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[fileContextParams]) (*mcp.CallToolResultFor[any], error) {
35+
fh, snapshot, release, err := h.fileOf(ctx, params.Arguments.File)
36+
if err != nil {
37+
return nil, err
38+
}
39+
defer release()
40+
41+
pkg, pgf, err := golang.NarrowestPackageForFile(ctx, snapshot, fh.URI())
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
info := pkg.TypesInfo()
47+
if info == nil {
48+
return nil, fmt.Errorf("no types info for package %q", pkg.Metadata().PkgPath)
49+
}
50+
51+
// Group objects defined in other files by file URI.
52+
otherFiles := make(map[protocol.DocumentURI]map[string]bool)
53+
addObj := func(obj types.Object) {
54+
if obj == nil {
55+
return
56+
}
57+
pos := obj.Pos()
58+
if !pos.IsValid() {
59+
return
60+
}
61+
objFile := pkg.FileSet().File(pos)
62+
if objFile == nil {
63+
return
64+
}
65+
uri := protocol.URIFromPath(objFile.Name())
66+
if uri == fh.URI() {
67+
return
68+
}
69+
if _, ok := otherFiles[uri]; !ok {
70+
otherFiles[uri] = make(map[string]bool)
71+
}
72+
otherFiles[uri][obj.Name()] = true
73+
}
74+
75+
for cur := range pgf.Cursor.Preorder((*ast.Ident)(nil)) {
76+
id := cur.Node().(*ast.Ident)
77+
addObj(info.Uses[id])
78+
addObj(info.Defs[id])
79+
}
80+
81+
var result strings.Builder
82+
fmt.Fprintf(&result, "File `%s` is in package %q.\n", params.Arguments.File, pkg.Metadata().PkgPath)
83+
fmt.Fprintf(&result, "Below is a summary of the APIs it uses from other files.\n")
84+
fmt.Fprintf(&result, "To read the full API of any package, use go_package_api.\n")
85+
for uri, decls := range otherFiles {
86+
pkgPath := "UNKNOWN"
87+
md, err := snapshot.NarrowestMetadataForFile(ctx, uri)
88+
if err != nil {
89+
if ctx.Err() != nil {
90+
return nil, ctx.Err()
91+
}
92+
} else {
93+
pkgPath = string(md.PkgPath)
94+
}
95+
fmt.Fprintf(&result, "Referenced declarations from %s (package %q):\n", uri.Path(), pkgPath)
96+
result.WriteString("```go\n")
97+
if err := writeFileSummary(ctx, snapshot, uri, &result, false, decls); err != nil {
98+
return nil, err
99+
}
100+
result.WriteString("```\n\n")
101+
}
102+
103+
return textResult(result.String()), nil
104+
}

gopls/internal/mcp/instructions.md

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,46 @@
11
# The gopls MCP server
22

3-
These instructions describe how to efficiently work in the Go programming
4-
language using the gopls MCP server. They are intended to be provided as context
5-
for an interactive session using the gopls MCP tool: you can load this file
6-
directly into a session where the gopls MCP server is connected.
3+
These instructions describe how to efficiently work in the Go programming language using the gopls MCP server. You can load this file directly into a session where the gopls MCP server is connected.
74

85
## Detecting a Go workspace
96

10-
Use the `go_workspace` tool to learn about the Go workspace. These instructions
11-
apply whenever that tool indicates that the user is in a Go workspace.
12-
13-
## Go Programming Guidelines
14-
15-
These guidelines MUST be followed whenever working in a Go workspace. There
16-
are two workflows described below: the 'Read Workflow' must be followed when
17-
the user asks a question about a Go workspace. The 'Edit Workflow' must be
18-
followed when the user edits a Go workspace.
19-
20-
You may re-do parts of each workflow as necessary to recover from errors.
21-
However, you cannot skip any steps.
22-
23-
### Read Workflow
24-
25-
1. **Search the workspace:** When the user asks about a symbol, use
26-
`go_search` to search for the symbol in question. If you find no matches,
27-
search for a substring of the user's referenced symbol. If `go_search`
28-
fails, you may fall back to regular textual search.
29-
2. **Read files:** Read the relevant file(s). Use the `go_file_metadata` tool
30-
to get package information for the file.
31-
3. **Understand packages:** If the user is asking about the use of one or more Go
32-
package, use the `go_package_outline` command to summarize their API.
33-
34-
### Editing Workflow
35-
36-
1. **Read first:** Before making any edits, follow the Read Workflow to
37-
understand the user's request.
38-
2. **Find references:** Before modifying the definition of any symbol, use the
39-
`go_symbol_references` tool to find references to that identifier. These
40-
references may need to be updated after editing the symbol. Read files
41-
containing references to evaluate if any further edits are required.
42-
3. **Make edits:** Make the primary edit, as well as any edits to references.
43-
4. **Run diagnostics:** Every time, after making edits to one or more files,
44-
you must call the `go_diagnostics` tool, passing the paths to the edited
45-
files, to verify that the build is not broken. Apply edits to fix any
46-
relevant diagnostics, and re-run the `go_diagnostics` tool to verify the
47-
fixes. It is OK to ignore 'hint' or 'info' diagnostics if they are not
48-
relevant.
49-
5. **Run tests** run `go test` for any packages that were edited. Invoke `go
50-
test` with the package paths returned from `go_file_metadata`. Fix any test
51-
failures.
7+
At the start of every session, you MUST use the `go_workspace` tool to learn about the Go workspace. The rest of these instructions apply whenever that tool indicates that the user is in a Go workspace.
8+
9+
## Go programming workflows
10+
11+
These guidelines MUST be followed whenever working in a Go workspace. There are two workflows described below: the 'Read Workflow' must be followed when the user asks a question about a Go workspace. The 'Edit Workflow' must be followed when the user edits a Go workspace.
12+
13+
You may re-do parts of each workflow as necessary to recover from errors. However, you must not skip any steps.
14+
15+
### Read workflow
16+
17+
The goal of the read workflow is to understand the codebase.
18+
19+
1. **Understand the workspace layout**: Start by using `go_workspace` to understand the overall structure of the workspace, such as whether it's a module, a workspace, or a GOPATH project.
20+
21+
2. **Find relevant symbols**: If you're looking for a specific type, function, or variable, use `go_search`. This is a fuzzy search that will help you locate symbols even if you don't know the exact name or location.
22+
EXAMPLE: search for the 'Server' type: `go_search({"query":"server"})`
23+
24+
3. **Understand a file and its intra-package dependencies**: When you have a file path and want to understand its contents and how it connects to other files *in the same package*, use `go_file_context`. This tool will show you a summary of the declarations from other files in the same package that are used by the current file. `go_file_context` MUST be used immediately after reading any Go file for the first time, and MAY be re-used if dependencies have changed.
25+
EXAMPLE: to understand `server.go`'s dependencies on other files in its package: `go_file_context({"file":"/path/to/server.go"})`
26+
27+
4. **Understand a package's public API**: When you need to understand what a package provides to external code (i.e., its public API), use `go_package_api`. This is especially useful for understanding third-party dependencies or other packages in the same monorepo.
28+
EXAMPLE: to see the API of the `storage` package: `go_package_api({"packagePaths":["example.com/internal/storage"]})`
29+
30+
### Editing workflow
31+
32+
The editing workflow is iterative. You should cycle through these steps until the task is complete.
33+
34+
1. **Read first**: Before making any edits, follow the Read Workflow to understand the user's request and the relevant code.
35+
36+
2. **Find references**: Before modifying the definition of any exported symbol, use the `go_symbol_references` tool to find all references to that identifier. This is critical for understanding the impact of your change. Read the files containing references to evaluate if any further edits are required.
37+
EXAMPLE: `go_symbol_references({"file":"/path/to/server.go","symbol":"Server.Run"})`
38+
39+
3. **Make edits**: Make the primary edit, as well as any edits to references you identified in the previous step.
40+
41+
4. **Check for errors**: After every code modification, you MUST call the `go_diagnostics` tool. Pass the paths of the files you have edited. This tool will report any build or analysis errors.
42+
EXAMPLE: `go_diagnostics({"files":["/path/to/server.go"]})`
43+
44+
5. **Fix errors**: If `go_diagnostics` reports any errors, fix them. The tool may provide suggested quick fixes in the form of diffs. You should review these diffs and apply them if they are correct. Once you've applied a fix, re-run `go_diagnostics` to confirm that the issue is resolved. It is OK to ignore 'hint' or 'info' diagnostics if they are not relevant to the current task.
45+
46+
6. **Run tests**: Once `go_diagnostics` reports no errors (and ONLY once there are no errors), run the tests for the packages you have changed. You can do this with `go test [packagePath...]`. Don't run `go test ./...` unless the user explicitly requests it, as doing so may slow down the iteration loop.

0 commit comments

Comments
 (0)