Skip to content

Commit 7cbfe75

Browse files
findleyrgopherbot
authored andcommitted
gopls/internal/mcp: tune tools for editing
Make some changes to the gopls MCP server a result of experimentation with our latest set of tools: - Add instructions in instructions.md, and embed them in the MCP server. - Prefer the 'tuned' version of references, context (outline), and diagnostics. Leave the original versions for now, for further experimentation. In some cases, refactor to share more code with the originals. - Update 'go_outline' to just accept an array of package paths. This is how Gemini wanted to use it. - More description for the 'go_search' tool. - Accept an optional array of files for workspace_diagnostics, rather than a single file. - Fix empty output when there were no diagnostics. - Use fenced code blocks in more places. - Allow configuring the set of tools in tests. - Other minor test improvements. For golang/go#73580 Change-Id: I47baf7f3c06904f0d016ac2d9bd5be68c5178b34 Reviewed-on: https://go-review.googlesource.com/c/tools/+/686755 Reviewed-by: Madeline Kalil <[email protected]> Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Robert Findley <[email protected]>
1 parent 4724ef9 commit 7cbfe75

20 files changed

+400
-163
lines changed

gopls/internal/cache/metadata/graph.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
package metadata
66

77
import (
8+
"cmp"
89
"iter"
910
"maps"
11+
"slices"
1012
"sort"
1113
"strings"
1214

@@ -23,6 +25,14 @@ type Graph struct {
2325
// ImportedBy maps package IDs to the list of packages that import them.
2426
ImportedBy map[PackageID][]PackageID
2527

28+
// ByPackagePath maps package by their package path to their package ID.
29+
// Non-test packages appear before test packages, and within each of those
30+
// categories, packages with fewer CompiledGoFiles appear first.
31+
//
32+
// TODO(rfindley): there's no reason for ImportedBy and IDs to not also hold
33+
// pointers, rather than IDs, and pointers are more convenient.
34+
ByPackagePath map[PackagePath][]*Package
35+
2636
// IDs maps file URIs to package IDs, sorted by (!valid, cli, packageID).
2737
// A single file may belong to multiple packages due to tests packages.
2838
//
@@ -84,10 +94,12 @@ func (g *Graph) Update(updates map[PackageID]*Package) *Graph {
8494
func newGraph(pkgs map[PackageID]*Package) *Graph {
8595
// Build the import graph.
8696
importedBy := make(map[PackageID][]PackageID)
97+
byPackagePath := make(map[PackagePath][]*Package)
8798
for id, mp := range pkgs {
8899
for _, depID := range mp.DepsByPkgPath {
89100
importedBy[depID] = append(importedBy[depID], id)
90101
}
102+
byPackagePath[mp.PkgPath] = append(byPackagePath[mp.PkgPath], mp)
91103
}
92104

93105
// Collect file associations.
@@ -139,10 +151,26 @@ func newGraph(pkgs map[PackageID]*Package) *Graph {
139151
}
140152
}
141153

154+
for _, mps := range byPackagePath {
155+
slices.SortFunc(mps, func(a, b *Package) int {
156+
if (a.ForTest == "") != (b.ForTest == "") {
157+
if a.ForTest == "" {
158+
return -1
159+
}
160+
return 1
161+
}
162+
if c := cmp.Compare(len(a.CompiledGoFiles), len(b.CompiledGoFiles)); c != 0 {
163+
return c
164+
}
165+
return cmp.Compare(a.ID, b.ID)
166+
})
167+
}
168+
142169
return &Graph{
143-
Packages: pkgs,
144-
ImportedBy: importedBy,
145-
IDs: uriIDs,
170+
Packages: pkgs,
171+
ImportedBy: importedBy,
172+
ByPackagePath: byPackagePath,
173+
IDs: uriIDs,
146174
}
147175
}
148176

gopls/internal/cache/snapshot.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
"golang.org/x/tools/gopls/internal/util/bug"
3939
"golang.org/x/tools/gopls/internal/util/constraints"
4040
"golang.org/x/tools/gopls/internal/util/immutable"
41+
"golang.org/x/tools/gopls/internal/util/moremaps"
4142
"golang.org/x/tools/gopls/internal/util/pathutil"
4243
"golang.org/x/tools/gopls/internal/util/persistent"
4344
"golang.org/x/tools/gopls/internal/vulncheck"
@@ -1001,19 +1002,13 @@ func (s *Snapshot) IsWorkspacePackage(id PackageID) bool {
10011002
// It may also contain ad-hoc packages for standalone files.
10021003
// It includes all test variants.
10031004
//
1004-
// TODO(rfindley): Replace this with s.MetadataGraph().
1005+
// TODO(rfindley): Replace usage of function this with s.LoadMetadataGraph().
10051006
func (s *Snapshot) AllMetadata(ctx context.Context) ([]*metadata.Package, error) {
1006-
if err := s.awaitLoaded(ctx); err != nil {
1007+
g, err := s.LoadMetadataGraph(ctx)
1008+
if err != nil {
10071009
return nil, err
10081010
}
1009-
1010-
g := s.MetadataGraph()
1011-
1012-
meta := make([]*metadata.Package, 0, len(g.Packages))
1013-
for _, mp := range g.Packages {
1014-
meta = append(meta, mp)
1015-
}
1016-
return meta, nil
1011+
return moremaps.ValueSlice(g.Packages), nil
10171012
}
10181013

10191014
// GoModForFile returns the URI of the go.mod file for the given URI.
@@ -1171,6 +1166,14 @@ func (s *Snapshot) MetadataGraph() *metadata.Graph {
11711166
return s.meta
11721167
}
11731168

1169+
// LoadMetadataGraph is like [Snapshot.MetadataGraph], but awaits snapshot reloading.
1170+
func (s *Snapshot) LoadMetadataGraph(ctx context.Context) (*metadata.Graph, error) {
1171+
if err := s.awaitLoaded(ctx); err != nil {
1172+
return nil, err
1173+
}
1174+
return s.MetadataGraph(), nil
1175+
}
1176+
11741177
// InitializationError returns the last error from initialization.
11751178
func (s *Snapshot) InitializationError() *InitializationError {
11761179
s.mu.Lock()

gopls/internal/cmd/mcp_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const B = 2
5757
}()
5858
var (
5959
tool = "go_diagnostics"
60-
args = map[string]any{"file": filepath.Join(tree, "a.go")}
60+
args = map[string]any{"files": []string{filepath.Join(tree, "a.go")}}
6161
)
6262
// On the first diagnostics call, there should be no diagnostics.
6363
{
@@ -157,15 +157,15 @@ func MyFun() {}
157157
}()
158158

159159
var (
160-
tool = "go_context"
160+
tool = "go_file_metadata"
161161
args = map[string]any{"file": filepath.Join(tree, "a.go")}
162162
)
163163
res, err := mcpSession.CallTool(ctx, &mcp.CallToolParams{Name: tool, Arguments: args})
164164
if err != nil {
165165
t.Fatal(err)
166166
}
167167
got := resultText(t, res)
168-
want := "The imported packages declare the following symbols"
168+
want := "example.com"
169169
if !strings.Contains(got, want) {
170170
t.Errorf("CallTool(%s, %v) = %+v, want containing %q", tool, args, got, want)
171171
}

gopls/internal/mcp/context.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func (h *handler) contextHandler(ctx context.Context, _ *mcp.ServerSession, para
9393
// Write import decls of the current file.
9494
{
9595
fmt.Fprintf(&result, "Current file %q contains this import declaration:\n", pgf.URI.Base())
96-
result.WriteString("--->\n")
96+
result.WriteString("```go\n")
9797
// Add all import decl to output including all floating comment by
9898
// using GenDecl's start and end position.
9999
for _, decl := range pgf.File.Decls {
@@ -110,7 +110,7 @@ func (h *handler) contextHandler(ctx context.Context, _ *mcp.ServerSession, para
110110
result.Write(text)
111111
result.WriteString("\n")
112112
}
113-
result.WriteString("<---\n\n")
113+
result.WriteString("```\n\n")
114114
}
115115

116116
var toSummarize []*ast.ImportSpec
@@ -137,7 +137,7 @@ func (h *handler) contextHandler(ctx context.Context, _ *mcp.ServerSession, para
137137
if md == nil {
138138
continue // ignore error
139139
}
140-
if summary := summarizePackage(ctx, snapshot, path, md); summary != "" {
140+
if summary := summarizePackage(ctx, snapshot, md); summary != "" {
141141
result.WriteString(summary)
142142
}
143143
}
@@ -147,16 +147,16 @@ func (h *handler) contextHandler(ctx context.Context, _ *mcp.ServerSession, para
147147
return textResult(result.String()), nil
148148
}
149149

150-
func summarizePackage(ctx context.Context, snapshot *cache.Snapshot, path metadata.ImportPath, md *metadata.Package) string {
150+
func summarizePackage(ctx context.Context, snapshot *cache.Snapshot, md *metadata.Package) string {
151151
var buf strings.Builder
152-
fmt.Fprintf(&buf, "%q (package %s)\n", path, md.Name)
152+
fmt.Fprintf(&buf, "%q (package %s)\n", md.PkgPath, md.Name)
153153
for _, f := range md.CompiledGoFiles {
154154
fmt.Fprintf(&buf, "%s:\n", f.Base())
155-
buf.WriteString("--->\n")
155+
buf.WriteString("```go\n")
156156
if err := writeFileSummary(ctx, snapshot, f, &buf, true); err != nil {
157157
return "" // ignore error
158158
}
159-
buf.WriteString("<---\n\n")
159+
buf.WriteString("```\n\n")
160160
}
161161
return buf.String()
162162
}

gopls/internal/mcp/diagnostics.go renamed to gopls/internal/mcp/file_diagnostics.go

Lines changed: 57 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,76 +27,89 @@ type diagnosticsParams struct {
2727
File string `json:"file"`
2828
}
2929

30-
func (h *handler) diagnosticsTool() *mcp.ServerTool {
30+
func (h *handler) fileDiagnosticsTool() *mcp.ServerTool {
3131
return mcp.NewServerTool(
32-
"go_diagnostics",
32+
"go_file_diagnostics",
3333
"Provides diagnostics for a Go file",
34-
h.diagnoseFileHandler,
34+
h.fileDiagnosticsHandler,
3535
mcp.Input(
3636
mcp.Property("file", mcp.Description("the absolute path to the file to diagnose")),
3737
),
3838
)
3939
}
4040

41-
func (h *handler) diagnoseFileHandler(ctx context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[diagnosticsParams]) (*mcp.CallToolResultFor[any], error) {
41+
func (h *handler) fileDiagnosticsHandler(ctx context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[diagnosticsParams]) (*mcp.CallToolResultFor[any], error) {
4242
fh, snapshot, release, err := h.fileOf(ctx, params.Arguments.File)
4343
if err != nil {
4444
return nil, err
4545
}
4646
defer release()
4747

48-
diagnostics, err := golang.DiagnoseFile(ctx, snapshot, fh.URI())
48+
diagnostics, fixes, err := h.diagnoseFile(ctx, snapshot, fh.URI())
4949
if err != nil {
5050
return nil, err
5151
}
5252

5353
var builder strings.Builder
5454
if len(diagnostics) == 0 {
55-
builder.WriteString("No diagnostics")
56-
} else {
57-
// LSP [protocol.Diagnostic]s do not carry code edits directly.
58-
// Instead, gopls provides associated [protocol.CodeAction]s with their
59-
// diagnostics field populated.
60-
// Ignore errors. It is still valuable to provide only the diagnostic
61-
// without any text edits.
62-
// TODO(hxjiang): support code actions that returns call back command.
63-
actions, _ := h.lspServer.CodeAction(ctx, &protocol.CodeActionParams{
64-
TextDocument: protocol.TextDocumentIdentifier{
65-
URI: fh.URI(),
66-
},
67-
Context: protocol.CodeActionContext{
68-
Only: []protocol.CodeActionKind{protocol.QuickFix},
69-
Diagnostics: cache.ToProtocolDiagnostics(diagnostics...),
70-
},
71-
})
72-
73-
type key struct {
74-
Message string
75-
Range protocol.Range
76-
}
55+
return textResult("No diagnostics"), nil
56+
}
7757

78-
fixes := make(map[key]*protocol.CodeAction)
79-
for _, action := range actions {
80-
for _, d := range action.Diagnostics {
81-
k := key{d.Message, d.Range}
82-
if alt, ok := fixes[k]; !ok || !alt.IsPreferred && action.IsPreferred {
83-
fixes[k] = &action
84-
}
85-
}
86-
}
58+
if err := summarizeDiagnostics(ctx, snapshot, &builder, diagnostics, fixes); err != nil {
59+
return nil, err
60+
}
61+
62+
return textResult(builder.String()), nil
63+
}
64+
65+
// diagnoseFile diagnoses a single file, including go/analysis and quick fixes.
66+
func (h *handler) diagnoseFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) ([]*cache.Diagnostic, map[*cache.Diagnostic]*protocol.CodeAction, error) {
67+
diagnostics, err := golang.DiagnoseFile(ctx, snapshot, uri)
68+
if err != nil {
69+
return nil, nil, err
70+
}
71+
if len(diagnostics) == 0 {
72+
return nil, nil, nil
73+
}
74+
75+
// LSP [protocol.Diagnostic]s do not carry code edits directly.
76+
// Instead, gopls provides associated [protocol.CodeAction]s with their
77+
// diagnostics field populated.
78+
// Ignore errors. It is still valuable to provide only the diagnostic
79+
// without any text edits.
80+
// TODO(hxjiang): support code actions that returns call back command.
81+
actions, _ := h.lspServer.CodeAction(ctx, &protocol.CodeActionParams{
82+
TextDocument: protocol.TextDocumentIdentifier{
83+
URI: uri,
84+
},
85+
Context: protocol.CodeActionContext{
86+
Only: []protocol.CodeActionKind{protocol.QuickFix},
87+
Diagnostics: cache.ToProtocolDiagnostics(diagnostics...),
88+
},
89+
})
90+
91+
type key struct {
92+
Message string
93+
Range protocol.Range
94+
}
8795

88-
fixesByDiagnostic := make(map[*cache.Diagnostic]*protocol.CodeAction)
89-
for _, d := range diagnostics {
90-
if fix, ok := fixes[key{d.Message, d.Range}]; ok {
91-
fixesByDiagnostic[d] = fix
96+
actionMap := make(map[key]*protocol.CodeAction)
97+
for _, action := range actions {
98+
for _, d := range action.Diagnostics {
99+
k := key{d.Message, d.Range}
100+
if alt, ok := actionMap[k]; !ok || !alt.IsPreferred && action.IsPreferred {
101+
actionMap[k] = &action
92102
}
93103
}
94-
if err := summarizeDiagnostics(ctx, snapshot, &builder, diagnostics, fixesByDiagnostic); err != nil {
95-
return nil, err
96-
}
97104
}
98105

99-
return textResult(builder.String()), nil
106+
fixes := make(map[*cache.Diagnostic]*protocol.CodeAction)
107+
for _, d := range diagnostics {
108+
if fix, ok := actionMap[key{d.Message, d.Range}]; ok {
109+
fixes[d] = fix
110+
}
111+
}
112+
return diagnostics, fixes, nil
100113
}
101114

102115
func summarizeDiagnostics(ctx context.Context, snapshot *cache.Snapshot, w io.Writer, diagnostics []*cache.Diagnostic, fixes map[*cache.Diagnostic]*protocol.CodeAction) error {

gopls/internal/mcp/instructions.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# The gopls MCP server
2+
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.
7+
8+
## Detecting a Go workspace
9+
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.

0 commit comments

Comments
 (0)