Skip to content

Commit b2bc8a1

Browse files
h9jianggopherbot
authored andcommitted
gopls/internal/mcp: add diagnostics tool
The diagnostics tool returns the error for the given file. This helps LLM to self-correct the code change it made. The input parameter's URI is required but range is marked as optional. For golang/go#73580 Change-Id: Ia1b944036497991108c3bf458beeb11148f41203 Reviewed-on: https://go-review.googlesource.com/c/tools/+/682515 Reviewed-by: Alan Donovan <[email protected]> Auto-Submit: Hongxiang Jiang <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent b021ce4 commit b2bc8a1

File tree

6 files changed

+124
-6
lines changed

6 files changed

+124
-6
lines changed

gopls/internal/mcp/context.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ import (
2727
)
2828

2929
type ContextParams struct {
30-
// TODO(hxjiang): experiment if the LLM can correctly provide the right
31-
// location information.
3230
Location protocol.Location `json:"location"`
3331
}
3432

gopls/internal/mcp/diagnostics.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
// This file defines the "diagnostics" operation, which is responsible for
8+
// returning diagnostics for the input file.
9+
10+
import (
11+
"context"
12+
"fmt"
13+
"strings"
14+
15+
"golang.org/x/tools/gopls/internal/cache"
16+
"golang.org/x/tools/gopls/internal/golang"
17+
"golang.org/x/tools/gopls/internal/protocol"
18+
"golang.org/x/tools/internal/mcp"
19+
)
20+
21+
type DiagnosticsParams struct {
22+
Location protocol.Location `json:"location"`
23+
}
24+
25+
func diagnosticsHandler(ctx context.Context, session *cache.Session, params *mcp.CallToolParamsFor[DiagnosticsParams]) (*mcp.CallToolResultFor[struct{}], error) {
26+
fh, snapshot, release, err := session.FileOf(ctx, params.Arguments.Location.URI)
27+
if err != nil {
28+
return nil, err
29+
}
30+
defer release()
31+
32+
diagnostics, err := golang.DiagnoseFile(ctx, snapshot, fh.URI())
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
var builder strings.Builder
38+
if len(diagnostics) == 0 {
39+
builder.WriteString("No diagnostics")
40+
} else {
41+
for _, d := range diagnostics {
42+
fmt.Fprintf(&builder, "%d:%d-%d:%d: [%s] %s\n", d.Range.Start.Line, d.Range.Start.Character, d.Range.End.Line, d.Range.End.Character, d.Severity, d.Message)
43+
}
44+
}
45+
46+
return &mcp.CallToolResultFor[struct{}]{
47+
Content: []*mcp.Content{
48+
mcp.NewTextContent(builder.String()),
49+
},
50+
}, nil
51+
}

gopls/internal/mcp/mcp.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,37 @@ func newServer(session *cache.Session) *mcp.Server {
136136
mcp.Property("uri", mcp.Description("URI of the text document")),
137137
mcp.Property("range",
138138
mcp.Description("range within text document"),
139+
mcp.Required(false),
140+
mcp.Property(
141+
"start",
142+
mcp.Description("start position of range"),
143+
mcp.Property("line", mcp.Description("line number (zero-based)")),
144+
mcp.Property("character", mcp.Description("column number (zero-based, UTF-16 encoding)")),
145+
),
146+
mcp.Property(
147+
"end",
148+
mcp.Description("end position of range"),
149+
mcp.Property("line", mcp.Description("line number (zero-based)")),
150+
mcp.Property("character", mcp.Description("column number (zero-based, UTF-16 encoding)")),
151+
),
152+
),
153+
),
154+
),
155+
),
156+
mcp.NewTool(
157+
"diagnostics",
158+
"Provide diagnostics for a region within a Go file",
159+
func(ctx context.Context, _ *mcp.ServerSession, request *mcp.CallToolParamsFor[DiagnosticsParams]) (*mcp.CallToolResultFor[struct{}], error) {
160+
return diagnosticsHandler(ctx, session, request)
161+
},
162+
mcp.Input(
163+
mcp.Property(
164+
"location",
165+
mcp.Description("location inside of a text file"),
166+
mcp.Property("uri", mcp.Description("URI of the text document")),
167+
mcp.Property("range",
168+
mcp.Description("range within text document"),
169+
mcp.Required(false),
139170
mcp.Property(
140171
"start",
141172
mcp.Description("start position of range"),
@@ -153,6 +184,5 @@ func newServer(session *cache.Session) *mcp.Server {
153184
),
154185
),
155186
)
156-
157187
return s
158188
}

gopls/internal/server/diagnostics.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ func (s *server) diagnoseSnapshot(ctx context.Context, snapshot *cache.Snapshot,
212212
// The second phase runs after the delay, and does everything.
213213

214214
if len(changedURIs) > 0 {
215-
diagnostics, err := s.diagnoseChangedFiles(ctx, snapshot, changedURIs)
215+
diagnostics, err := diagnoseChangedFiles(ctx, snapshot, changedURIs)
216216
if err != nil {
217217
if ctx.Err() == nil {
218218
event.Error(ctx, "warning: while diagnosing changed files", err, snapshot.Labels()...)
@@ -239,7 +239,7 @@ func (s *server) diagnoseSnapshot(ctx context.Context, snapshot *cache.Snapshot,
239239
s.updateDiagnostics(ctx, snapshot, diagnostics, true)
240240
}
241241

242-
func (s *server) diagnoseChangedFiles(ctx context.Context, snapshot *cache.Snapshot, uris []protocol.DocumentURI) (diagMap, error) {
242+
func diagnoseChangedFiles(ctx context.Context, snapshot *cache.Snapshot, uris []protocol.DocumentURI) (diagMap, error) {
243243
ctx, done := event.Start(ctx, "server.diagnoseChangedFiles", snapshot.Labels()...)
244244
defer done()
245245

gopls/internal/test/marker/testdata/mcptools/context.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
This test exercises mcp tool context.
1+
This test exercises the "context" MCP tool.
22

33
-- flags --
44
-mcp
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
This test exercises the "diagnostics" MCP tool.
2+
3+
-- flags --
4+
-mcp
5+
6+
-- go.mod --
7+
module example.com
8+
9+
-- a/main.go --
10+
package main
11+
12+
func foo() {} //@loc(foo, "foo")
13+
14+
//@mcptool("diagnostics", `{}`, foo, output=unused)
15+
//@diag(foo, re"unused")
16+
17+
-- @unused --
18+
2:5-2:8: [Information] function "foo" is unused
19+
-- b/main.go --
20+
package main
21+
22+
func _() {
23+
_ = deprecated([]string{"a"}, "a") //@loc(inline, "deprecated")
24+
}
25+
26+
//go:fix inline
27+
func deprecated(slice []string, s string) bool {
28+
return proposed(slice, s, true)
29+
}
30+
31+
func proposed(_ []string, _ string, _ bool) bool {
32+
return false // fake
33+
}
34+
35+
//@mcptool("diagnostics", `{}`, inline, output=bloop)
36+
//@diag(inline, re"inline")
37+
38+
-- @bloop --
39+
3:5-3:35: [Hint] Call of main.deprecated should be inlined

0 commit comments

Comments
 (0)