Skip to content

Commit 2386301

Browse files
committed
add iologging for debugging purposes
1 parent 09366fa commit 2386301

File tree

4 files changed

+129
-3
lines changed

4 files changed

+129
-3
lines changed

cmd/server/main.go

+16-3
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ package main
33
import (
44
"context"
55
"fmt"
6+
"io"
67
stdlog "log"
78
"os"
89
"os/signal"
910
"syscall"
1011

1112
"github.com/github/github-mcp-server/pkg/github"
13+
iolog "github.com/github/github-mcp-server/pkg/log"
1214
gogithub "github.com/google/go-github/v69/github"
1315
"github.com/mark3labs/mcp-go/server"
1416
log "github.com/sirupsen/logrus"
@@ -33,7 +35,8 @@ var (
3335
if err != nil {
3436
stdlog.Fatal("Failed to initialize logger:", err)
3537
}
36-
if err := runStdioServer(logger); err != nil {
38+
logCommands := viper.GetBool("log-commands")
39+
if err := runStdioServer(logger, logCommands); err != nil {
3740
stdlog.Fatal("failed to run stdio server:", err)
3841
}
3942
},
@@ -45,9 +48,11 @@ func init() {
4548

4649
// Add global flags that will be shared by all commands
4750
rootCmd.PersistentFlags().String("log-file", "", "Path to log file")
51+
rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file")
4852

4953
// Bind flag to viper
5054
viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
55+
viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
5156

5257
// Add subcommands
5358
rootCmd.AddCommand(stdioCmd)
@@ -70,12 +75,13 @@ func initLogger(outPath string) (*log.Logger, error) {
7075
}
7176

7277
logger := log.New()
78+
logger.SetLevel(log.DebugLevel)
7379
logger.SetOutput(file)
7480

7581
return logger, nil
7682
}
7783

78-
func runStdioServer(logger *log.Logger) error {
84+
func runStdioServer(logger *log.Logger, logCommands bool) error {
7985
// Create app context
8086
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
8187
defer stop()
@@ -97,7 +103,14 @@ func runStdioServer(logger *log.Logger) error {
97103
// Start listening for messages
98104
errC := make(chan error, 1)
99105
go func() {
100-
errC <- stdioServer.Listen(ctx, os.Stdin, os.Stdout)
106+
in, out := io.Reader(os.Stdin), io.Writer(os.Stdout)
107+
108+
if logCommands {
109+
loggedIO := iolog.NewIOLogger(in, out, logger)
110+
in, out = loggedIO, loggedIO
111+
}
112+
113+
errC <- stdioServer.Listen(ctx, in, out)
101114
}()
102115

103116
// Output github-mcp-server string

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ require (
99
github.com/sirupsen/logrus v1.9.3
1010
github.com/spf13/cobra v1.9.1
1111
github.com/spf13/viper v1.19.0
12+
github.com/stretchr/testify v1.9.0
1213
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
1314
)
1415

1516
require (
17+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
1618
github.com/fsnotify/fsnotify v1.7.0 // indirect
1719
github.com/google/go-querystring v1.1.0 // indirect
1820
github.com/google/uuid v1.6.0 // indirect
@@ -21,6 +23,7 @@ require (
2123
github.com/magiconair/properties v1.8.7 // indirect
2224
github.com/mitchellh/mapstructure v1.5.0 // indirect
2325
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
26+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
2427
github.com/sagikazarmark/locafero v0.4.0 // indirect
2528
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
2629
github.com/sourcegraph/conc v0.3.0 // indirect

pkg/log/io.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package log
2+
3+
import (
4+
"io"
5+
6+
log "github.com/sirupsen/logrus"
7+
)
8+
9+
// IOLogger is a wrapper around io.Reader and io.Writer that can be used
10+
// to log the data being read and written from the underlying streams
11+
type IOLogger struct {
12+
reader io.Reader
13+
writer io.Writer
14+
logger *log.Logger
15+
}
16+
17+
// NewIOLogger creates a new IOLogger instance
18+
func NewIOLogger(r io.Reader, w io.Writer, logger *log.Logger) *IOLogger {
19+
return &IOLogger{
20+
reader: r,
21+
writer: w,
22+
logger: logger,
23+
}
24+
}
25+
26+
// Read reads data from the underlying io.Reader and logs it.
27+
func (l *IOLogger) Read(p []byte) (n int, err error) {
28+
if l.reader == nil {
29+
return 0, io.EOF
30+
}
31+
n, err = l.reader.Read(p)
32+
if n > 0 {
33+
l.logger.Infof("[stdin]: received %d bytes: %s", n, string(p[:n]))
34+
}
35+
return n, err
36+
}
37+
38+
// Write writes data to the underlying io.Writer and logs it.
39+
func (l *IOLogger) Write(p []byte) (n int, err error) {
40+
if l.writer == nil {
41+
return 0, io.ErrClosedPipe
42+
}
43+
l.logger.Infof("[stdout]: sending %d bytes: %s", len(p), string(p))
44+
return l.writer.Write(p)
45+
}

pkg/log/io_test.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package log
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
8+
log "github.com/sirupsen/logrus"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestLoggedReadWriter(t *testing.T) {
13+
t.Run("Read method logs and passes data", func(t *testing.T) {
14+
// Setup
15+
inputData := "test input data"
16+
reader := strings.NewReader(inputData)
17+
18+
// Create logger with buffer to capture output
19+
var logBuffer bytes.Buffer
20+
logger := log.New()
21+
logger.SetOutput(&logBuffer)
22+
logger.SetFormatter(&log.TextFormatter{
23+
DisableTimestamp: true,
24+
})
25+
26+
lrw := NewIOLogger(reader, nil, logger)
27+
28+
// Test Read
29+
buf := make([]byte, 100)
30+
n, err := lrw.Read(buf)
31+
32+
// Assertions
33+
assert.NoError(t, err)
34+
assert.Equal(t, len(inputData), n)
35+
assert.Equal(t, inputData, string(buf[:n]))
36+
assert.Contains(t, logBuffer.String(), "[stdin]")
37+
assert.Contains(t, logBuffer.String(), inputData)
38+
})
39+
40+
t.Run("Write method logs and passes data", func(t *testing.T) {
41+
// Setup
42+
outputData := "test output data"
43+
var writeBuffer bytes.Buffer
44+
45+
// Create logger with buffer to capture output
46+
var logBuffer bytes.Buffer
47+
logger := log.New()
48+
logger.SetOutput(&logBuffer)
49+
logger.SetFormatter(&log.TextFormatter{
50+
DisableTimestamp: true,
51+
})
52+
53+
lrw := NewIOLogger(nil, &writeBuffer, logger)
54+
55+
// Test Write
56+
n, err := lrw.Write([]byte(outputData))
57+
58+
// Assertions
59+
assert.NoError(t, err)
60+
assert.Equal(t, len(outputData), n)
61+
assert.Equal(t, outputData, writeBuffer.String())
62+
assert.Contains(t, logBuffer.String(), "[stdout]")
63+
assert.Contains(t, logBuffer.String(), outputData)
64+
})
65+
}

0 commit comments

Comments
 (0)