Repository: github/github-mcp-server (Go)

Issue:
Use slog for logging instead of logrus
### Describe the bug

Logrus is in maintenance mode and is readily replaced by `slog` in go's standard library.

I am experimenting with running GitHub's MCP server as a Wasm component. Logrus does not compile but slog does.

### Steps to reproduce the behavior

```bash
GOOS=wasip1 GOARCH=wasm go build -o github-mcp-server.wasm ./cmd/github-mcp-server/main.go

# github.com/sirupsen/logrus
../../../go/pkg/mod/github.com/sirupsen/logrus@v1.9.3/terminal_check_notappengine.go:13:10: undefined: isTerminal
```

### Expected vs actual behavior

Replacing logrus with slog should result in no change in runtime behavior, however by replacing a third party dependency with the standard library, additional compilation targets like Wasm are available.



Discussion:

Patch:
diff --git a/go.mod b/go.mod
index 5f114825d..a2a86feaf 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,6 @@ require (
 	github.com/josephburnett/jd v1.9.2
 	github.com/mark3labs/mcp-go v0.32.0
 	github.com/migueleliasweb/go-github-mock v1.3.0
-	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/cobra v1.9.1
 	github.com/spf13/viper v1.20.1
 	github.com/stretchr/testify v1.10.0
diff --git a/go.sum b/go.sum
index 64ce05453..33395e6bb 100644
--- a/go.sum
+++ b/go.sum
@@ -66,8 +66,6 @@ github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkv
 github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
 github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
 github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
-github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
-github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
 github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
 github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
@@ -83,7 +81,6 @@ github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqj
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -98,7 +95,6 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0
 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
 golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go
index 5079ab847..5fb9582b9 100644
--- a/internal/ghmcp/server.go
+++ b/internal/ghmcp/server.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"io"
 	"log"
+	"log/slog"
 	"net/http"
 	"net/url"
 	"os"
@@ -21,7 +22,6 @@ import (
 	"github.com/mark3labs/mcp-go/mcp"
 	"github.com/mark3labs/mcp-go/server"
 	"github.com/shurcooL/githubv4"
-	"github.com/sirupsen/logrus"
 )
 
 type MCPServerConfig struct {
@@ -49,6 +49,8 @@ type MCPServerConfig struct {
 	Translator translations.TranslationHelperFunc
 }
 
+const stdioServerLogPrefix = "stdioserver"
+
 func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
 	apiHost, err := parseAPIHost(cfg.Host)
 	if err != nil {
@@ -203,17 +205,22 @@ func RunStdioServer(cfg StdioServerConfig) error {
 
 	stdioServer := server.NewStdioServer(ghServer)
 
-	logrusLogger := logrus.New()
+	var slogHandler slog.Handler
+	var logOutput io.Writer
 	if cfg.LogFilePath != "" {
 		file, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
 		if err != nil {
 			return fmt.Errorf("failed to open log file: %w", err)
 		}
-
-		logrusLogger.SetLevel(logrus.DebugLevel)
-		logrusLogger.SetOutput(file)
-	}
-	stdLogger := log.New(logrusLogger.Writer(), "stdioserver", 0)
+		logOutput = file
+		slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelDebug})
+	} else {
+		logOutput = os.Stderr
+		slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelInfo})
+	}
+	logger := slog.New(slogHandler)
+	logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "dynamicToolsets", cfg.DynamicToolsets, "readOnly", cfg.ReadOnly)
+	stdLogger := log.New(logOutput, stdioServerLogPrefix, 0)
 	stdioServer.SetErrorLogger(stdLogger)
 
 	if cfg.ExportTranslations {
@@ -227,7 +234,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
 		in, out := io.Reader(os.Stdin), io.Writer(os.Stdout)
 
 		if cfg.EnableCommandLogging {
-			loggedIO := mcplog.NewIOLogger(in, out, logrusLogger)
+			loggedIO := mcplog.NewIOLogger(in, out, logger)
 			in, out = loggedIO, loggedIO
 		}
 		// enable GitHub errors in the context
@@ -241,9 +248,10 @@ func RunStdioServer(cfg StdioServerConfig) error {
 	// Wait for shutdown signal
 	select {
 	case <-ctx.Done():
-		logrusLogger.Infof("shutting down server...")
+		logger.Info("shutting down server", "signal", "context done")
 	case err := <-errC:
 		if err != nil {
+			logger.Error("error running server", "error", err)
 			return fmt.Errorf("error running server: %w", err)
 		}
 	}
diff --git a/pkg/log/io.go b/pkg/log/io.go
index de2210278..44b8dc17a 100644
--- a/pkg/log/io.go
+++ b/pkg/log/io.go
@@ -3,7 +3,7 @@ package log
 import (
 	"io"
 
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 )
 
 // IOLogger is a wrapper around io.Reader and io.Writer that can be used
@@ -11,11 +11,11 @@ import (
 type IOLogger struct {
 	reader io.Reader
 	writer io.Writer
-	logger *log.Logger
+	logger *slog.Logger
 }
 
 // NewIOLogger creates a new IOLogger instance
-func NewIOLogger(r io.Reader, w io.Writer, logger *log.Logger) *IOLogger {
+func NewIOLogger(r io.Reader, w io.Writer, logger *slog.Logger) *IOLogger {
 	return &IOLogger{
 		reader: r,
 		writer: w,
@@ -30,7 +30,7 @@ func (l *IOLogger) Read(p []byte) (n int, err error) {
 	}
 	n, err = l.reader.Read(p)
 	if n > 0 {
-		l.logger.Infof("[stdin]: received %d bytes: %s", n, string(p[:n]))
+		l.logger.Info("[stdin]: received bytes", "count", n, "data", string(p[:n]))
 	}
 	return n, err
 }
@@ -40,6 +40,6 @@ 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))
+	l.logger.Info("[stdout]: sending bytes", "count", len(p), "data", string(p))
 	return l.writer.Write(p)
 }
diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md
index 2f6c0ecb8..88ee6a838 100644
--- a/third-party-licenses.darwin.md
+++ b/third-party-licenses.darwin.md
@@ -26,7 +26,6 @@ Some packages may only be included on certain architectures or operating systems
  - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
  - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
  - [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
- - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE))
  - [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/v0.3.0/LICENSE))
  - [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.14.0/LICENSE.txt))
  - [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.7.1/LICENSE))
diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md
index 2f6c0ecb8..88ee6a838 100644
--- a/third-party-licenses.linux.md
+++ b/third-party-licenses.linux.md
@@ -26,7 +26,6 @@ Some packages may only be included on certain architectures or operating systems
  - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
  - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
  - [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
- - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE))
  - [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/v0.3.0/LICENSE))
  - [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.14.0/LICENSE.txt))
  - [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.7.1/LICENSE))
diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md
index 63bf0cb69..bb742aeb0 100644
--- a/third-party-licenses.windows.md
+++ b/third-party-licenses.windows.md
@@ -27,7 +27,6 @@ Some packages may only be included on certain architectures or operating systems
  - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
  - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
  - [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
- - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE))
  - [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/v0.3.0/LICENSE))
  - [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.14.0/LICENSE.txt))
  - [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.7.1/LICENSE))
diff --git a/third-party/github.com/sirupsen/logrus/LICENSE b/third-party/github.com/sirupsen/logrus/LICENSE
deleted file mode 100644
index f090cb42f..000000000
--- a/third-party/github.com/sirupsen/logrus/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2014 Simon Eskildsen
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.


Test patch:
diff --git a/pkg/log/io_test.go b/pkg/log/io_test.go
index 0d0cd8959..2661de164 100644
--- a/pkg/log/io_test.go
+++ b/pkg/log/io_test.go
@@ -5,7 +5,8 @@ import (
 	"strings"
 	"testing"
 
-	log "github.com/sirupsen/logrus"
+	"log/slog"
+
 	"github.com/stretchr/testify/assert"
 )
 
@@ -17,11 +18,7 @@ func TestLoggedReadWriter(t *testing.T) {
 
 		// Create logger with buffer to capture output
 		var logBuffer bytes.Buffer
-		logger := log.New()
-		logger.SetOutput(&logBuffer)
-		logger.SetFormatter(&log.TextFormatter{
-			DisableTimestamp: true,
-		})
+		logger := slog.New(slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{ReplaceAttr: removeTimeAttr}))
 
 		lrw := NewIOLogger(reader, nil, logger)
 
@@ -44,11 +41,7 @@ func TestLoggedReadWriter(t *testing.T) {
 
 		// Create logger with buffer to capture output
 		var logBuffer bytes.Buffer
-		logger := log.New()
-		logger.SetOutput(&logBuffer)
-		logger.SetFormatter(&log.TextFormatter{
-			DisableTimestamp: true,
-		})
+		logger := slog.New(slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{ReplaceAttr: removeTimeAttr}))
 
 		lrw := NewIOLogger(nil, &writeBuffer, logger)
 
@@ -63,3 +56,10 @@ func TestLoggedReadWriter(t *testing.T) {
 		assert.Contains(t, logBuffer.String(), outputData)
 	})
 }
+
+func removeTimeAttr(groups []string, a slog.Attr) slog.Attr {
+	if a.Key == slog.TimeKey && len(groups) == 0 {
+		return slog.Attr{}
+	}
+	return a
+}