Skip to content

Commit db322bb

Browse files
committed
gopls/internal/mcp: add -logfile and -rpc.trace to the headless server
For debugging interactions with gopls' headless MCP server, add a -logfile and -rpc.trace flag. The names are chosen to align with gopls serve, though the RPCs in question are now MCP, not LSP. For golang/go#73580 Change-Id: I93525e8bd7ea3fe7c35445027961d430a6e75819 Reviewed-on: https://go-review.googlesource.com/c/tools/+/686815 Reviewed-by: Madeline Kalil <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 7cbfe75 commit db322bb

File tree

4 files changed

+97
-5
lines changed

4 files changed

+97
-5
lines changed

gopls/internal/cmd/mcp.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"flag"
1010
"fmt"
11+
"io"
1112
"log"
1213
"os"
1314
"time"
@@ -21,7 +22,9 @@ import (
2122
type headlessMCP struct {
2223
app *Application
2324

24-
Address string `flag:"listen" help:"the address on which to run the mcp server"`
25+
Address string `flag:"listen" help:"the address on which to run the mcp server"`
26+
Logfile string `flag:"logfile" help:"filename to log to; if unset, logs to stderr"`
27+
RPCTrace bool `flag:"rpc.trace" help:"print MCP rpc traces; cannot be used with -listen"`
2528
}
2629

2730
func (m *headlessMCP) Name() string { return "mcp" }
@@ -42,6 +45,21 @@ Examples:
4245
}
4346

4447
func (m *headlessMCP) Run(ctx context.Context, args ...string) error {
48+
if m.Address != "" && m.RPCTrace {
49+
// There's currently no way to plumb logging instrumentation into the SSE
50+
// transport that is created on connections to the HTTP handler, so we must
51+
// disallow the -rpc.trace flag when using -listen.
52+
return fmt.Errorf("-listen is incompatible with -rpc.trace")
53+
}
54+
if m.Logfile != "" {
55+
f, err := os.Create(m.Logfile)
56+
if err != nil {
57+
return fmt.Errorf("opening logfile: %v", err)
58+
}
59+
log.SetOutput(f)
60+
defer f.Close()
61+
}
62+
4563
// Start a new in-process gopls session and create a fake client
4664
// to connect to it.
4765
cli, sess, err := m.app.connect(ctx)
@@ -112,6 +130,11 @@ func (m *headlessMCP) Run(ctx context.Context, args ...string) error {
112130
return mcp.Serve(ctx, m.Address, eventChan, false)
113131
} else {
114132
countHeadlessMCPStdIO.Inc()
115-
return mcp.StartStdIO(ctx, sess, cli.server)
133+
var rpcLog io.Writer
134+
if m.RPCTrace {
135+
rpcLog = log.Writer() // possibly redirected by -logfile above
136+
}
137+
log.Printf("Listening for MCP messages on stdin...")
138+
return mcp.StartStdIO(ctx, sess, cli.server, rpcLog)
116139
}
117140
}

gopls/internal/cmd/mcp_test.go

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222

2323
func TestMCPCommandStdio(t *testing.T) {
2424
// Test that the headless MCP subcommand works, and recognizes file changes.
25-
2625
testenv.NeedsExec(t) // stdio transport uses execve(2)
2726
tree := writeTree(t, `
2827
-- go.mod --
@@ -93,6 +92,67 @@ const B = 2
9392
}
9493
}
9594

95+
func TestMCPCommandLogging(t *testing.T) {
96+
// Test that logging flags for headless MCP subcommand work as intended.
97+
testenv.NeedsExec(t) // stdio transport uses execve(2)
98+
99+
tests := []struct {
100+
logFile string // also the subtest name
101+
trace bool
102+
want string
103+
dontWant string
104+
}{
105+
{"notrace.log", false, "stdin", "initialized"},
106+
{"trace.log", true, "initialized", ""},
107+
}
108+
109+
dir := t.TempDir()
110+
for _, test := range tests {
111+
t.Run(test.logFile, func(t *testing.T) {
112+
tree := writeTree(t, `
113+
-- go.mod --
114+
module example.com
115+
go 1.18
116+
117+
-- a.go --
118+
package p
119+
`)
120+
121+
logFile := filepath.Join(dir, test.logFile)
122+
args := []string{"mcp", "-logfile", logFile}
123+
if test.trace {
124+
args = append(args, "-rpc.trace")
125+
}
126+
goplsCmd := exec.Command(os.Args[0], args...)
127+
goplsCmd.Env = append(os.Environ(), "ENTRYPOINT=goplsMain")
128+
goplsCmd.Dir = tree
129+
130+
ctx := t.Context()
131+
client := mcp.NewClient("client", "v0.0.1", nil)
132+
mcpSession, err := client.Connect(ctx, mcp.NewCommandTransport(goplsCmd))
133+
if err != nil {
134+
t.Fatal(err)
135+
}
136+
if err := mcpSession.Close(); err != nil {
137+
t.Errorf("closing MCP connection: %v", err)
138+
}
139+
logs, err := os.ReadFile(logFile)
140+
if err != nil {
141+
t.Fatal(err)
142+
}
143+
if test.want != "" && !bytes.Contains(logs, []byte(test.want)) {
144+
t.Errorf("logs do not contain expected %q", test.want)
145+
}
146+
if test.dontWant != "" && bytes.Contains(logs, []byte(test.dontWant)) {
147+
t.Errorf("logs contain unexpected %q", test.dontWant)
148+
}
149+
if t.Failed() {
150+
t.Logf("Logs:\n%s", string(logs))
151+
}
152+
})
153+
}
154+
}
155+
96156
func TestMCPCommandHTTP(t *testing.T) {
97157
testenv.NeedsExec(t)
98158
tree := writeTree(t, `

gopls/internal/cmd/usage/mcp.hlp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ Examples:
1111
$ gopls mcp //start over stdio
1212
-listen=string
1313
the address on which to run the mcp server
14+
-logfile=string
15+
filename to log to; if unset, logs to stderr
16+
-rpc.trace
17+
print MCP rpc traces; cannot be used with -listen

gopls/internal/mcp/mcp.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
_ "embed"
1010
"fmt"
11+
"io"
1112
"log"
1213
"net"
1314
"net/http"
@@ -67,8 +68,12 @@ func Serve(ctx context.Context, address string, eventChan <-chan lsprpc.SessionE
6768
}
6869

6970
// StartStdIO starts an MCP server over stdio.
70-
func StartStdIO(ctx context.Context, session *cache.Session, server protocol.Server) error {
71-
t := mcp.NewLoggingTransport(mcp.NewStdioTransport(), os.Stderr)
71+
func StartStdIO(ctx context.Context, session *cache.Session, server protocol.Server, rpcLog io.Writer) error {
72+
transport := mcp.NewStdioTransport()
73+
var t mcp.Transport = transport
74+
if rpcLog != nil {
75+
t = mcp.NewLoggingTransport(transport, rpcLog)
76+
}
7277
s := newServer(session, server)
7378
return s.Run(ctx, t)
7479
}

0 commit comments

Comments
 (0)