Skip to content

add iologger for debugging purposes #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package main
import (
"context"
"fmt"
"io"
stdlog "log"
"os"
"os/signal"
"syscall"

"github.com/github/github-mcp-server/pkg/github"
iolog "github.com/github/github-mcp-server/pkg/log"
gogithub "github.com/google/go-github/v69/github"
"github.com/mark3labs/mcp-go/server"
log "github.com/sirupsen/logrus"
Expand All @@ -33,7 +35,8 @@ var (
if err != nil {
stdlog.Fatal("Failed to initialize logger:", err)
}
if err := runStdioServer(logger); err != nil {
logCommands := viper.GetBool("enable-command-logging")
if err := runStdioServer(logger, logCommands); err != nil {
stdlog.Fatal("failed to run stdio server:", err)
}
},
Expand All @@ -45,9 +48,11 @@ func init() {

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

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

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

logger := log.New()
logger.SetLevel(log.DebugLevel)
logger.SetOutput(file)

return logger, nil
}

func runStdioServer(logger *log.Logger) error {
func runStdioServer(logger *log.Logger, logCommands bool) error {
// Create app context
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
Expand All @@ -97,7 +103,14 @@ func runStdioServer(logger *log.Logger) error {
// Start listening for messages
errC := make(chan error, 1)
go func() {
errC <- stdioServer.Listen(ctx, os.Stdin, os.Stdout)
in, out := io.Reader(os.Stdin), io.Writer(os.Stdout)

if logCommands {
loggedIO := iolog.NewIOLogger(in, out, logger)
in, out = loggedIO, loggedIO
}

errC <- stdioServer.Listen(ctx, in, out)
}()

// Output github-mcp-server string
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
)

require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
Expand All @@ -21,6 +23,7 @@ require (
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
Expand Down
45 changes: 45 additions & 0 deletions pkg/log/io.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package log

import (
"io"

log "github.com/sirupsen/logrus"
)

// IOLogger is a wrapper around io.Reader and io.Writer that can be used
// to log the data being read and written from the underlying streams
type IOLogger struct {
reader io.Reader
writer io.Writer
logger *log.Logger
}

// NewIOLogger creates a new IOLogger instance
func NewIOLogger(r io.Reader, w io.Writer, logger *log.Logger) *IOLogger {
return &IOLogger{
reader: r,
writer: w,
logger: logger,
}
}

// Read reads data from the underlying io.Reader and logs it.
func (l *IOLogger) Read(p []byte) (n int, err error) {
if l.reader == nil {
return 0, io.EOF
}
n, err = l.reader.Read(p)
if n > 0 {
l.logger.Infof("[stdin]: received %d bytes: %s", n, string(p[:n]))
}
return n, err
}

// Write writes data to the underlying io.Writer and logs it.
func (l *IOLogger) Write(p []byte) (n int, err error) {
if l.writer == nil {
return 0, io.ErrClosedPipe
}
l.logger.Infof("[stdout]: sending %d bytes: %s", len(p), string(p))
return l.writer.Write(p)
}
65 changes: 65 additions & 0 deletions pkg/log/io_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package log

import (
"bytes"
"strings"
"testing"

log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)

func TestLoggedReadWriter(t *testing.T) {
t.Run("Read method logs and passes data", func(t *testing.T) {
// Setup
inputData := "test input data"
reader := strings.NewReader(inputData)

// Create logger with buffer to capture output
var logBuffer bytes.Buffer
logger := log.New()
logger.SetOutput(&logBuffer)
logger.SetFormatter(&log.TextFormatter{
DisableTimestamp: true,
})

lrw := NewIOLogger(reader, nil, logger)

// Test Read
buf := make([]byte, 100)
n, err := lrw.Read(buf)

// Assertions
assert.NoError(t, err)
assert.Equal(t, len(inputData), n)
assert.Equal(t, inputData, string(buf[:n]))
assert.Contains(t, logBuffer.String(), "[stdin]")
assert.Contains(t, logBuffer.String(), inputData)
})

t.Run("Write method logs and passes data", func(t *testing.T) {
// Setup
outputData := "test output data"
var writeBuffer bytes.Buffer

// Create logger with buffer to capture output
var logBuffer bytes.Buffer
logger := log.New()
logger.SetOutput(&logBuffer)
logger.SetFormatter(&log.TextFormatter{
DisableTimestamp: true,
})

lrw := NewIOLogger(nil, &writeBuffer, logger)

// Test Write
n, err := lrw.Write([]byte(outputData))

// Assertions
assert.NoError(t, err)
assert.Equal(t, len(outputData), n)
assert.Equal(t, outputData, writeBuffer.String())
assert.Contains(t, logBuffer.String(), "[stdout]")
assert.Contains(t, logBuffer.String(), outputData)
})
}