Skip to content

Commit 3b96e1c

Browse files
committed
WIP: Support logging API requests
1 parent 4ccedee commit 3b96e1c

File tree

4 files changed

+72
-14
lines changed

4 files changed

+72
-14
lines changed

cmd/github-mcp-server/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ var (
5353
ExportTranslations: viper.GetBool("export-translations"),
5454
EnableCommandLogging: viper.GetBool("enable-command-logging"),
5555
LogFilePath: viper.GetString("log-file"),
56+
LogAPIRequests: viper.GetBool("log_api_requests"),
5657
}
5758

5859
return ghmcp.RunStdioServer(stdioServerConfig)
@@ -71,6 +72,7 @@ func init() {
7172
rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations")
7273
rootCmd.PersistentFlags().String("log-file", "", "Path to log file")
7374
rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file")
75+
rootCmd.PersistentFlags().Bool("log-api-requests", false, "When enabled, the server will log all API requests to the log location (default stderr)")
7476
rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file")
7577
rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)")
7678

@@ -80,6 +82,7 @@ func init() {
8082
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
8183
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
8284
_ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
85+
_ = viper.BindPFlag("log_api_requests", rootCmd.PersistentFlags().Lookup("log-api-requests"))
8386
_ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations"))
8487
_ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host"))
8588

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/google/go-querystring v1.1.0 // indirect
2121
github.com/google/uuid v1.6.0 // indirect
2222
github.com/gorilla/mux v1.8.0 // indirect
23+
github.com/henvic/httpretty v0.1.4
2324
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2425
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
2526
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
2222
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
2323
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
2424
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
25+
github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU=
26+
github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM=
2527
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
2628
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
2729
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=

internal/ghmcp/server.go

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import (
99
"net/url"
1010
"os"
1111
"os/signal"
12+
"regexp"
1213
"strings"
1314
"syscall"
1415

1516
"github.com/github/github-mcp-server/pkg/github"
1617
mcplog "github.com/github/github-mcp-server/pkg/log"
1718
"github.com/github/github-mcp-server/pkg/translations"
1819
gogithub "github.com/google/go-github/v69/github"
20+
"github.com/henvic/httpretty"
1921
"github.com/mark3labs/mcp-go/mcp"
2022
"github.com/mark3labs/mcp-go/server"
2123
"github.com/shurcooL/githubv4"
@@ -45,6 +47,12 @@ type MCPServerConfig struct {
4547

4648
// Translator provides translated text for the server tooling
4749
Translator translations.TranslationHelperFunc
50+
51+
// HTTPLogWriter is the writer for HTTP request/response logs, nil means no logging
52+
// TODO: In future, this should probably log back to the MCP server via notifications/message so that
53+
// remote servers are able to deliver debug logs to the client. For the moment, this is a very
54+
// effective solution for debugging stdio.
55+
HTTPLogWriter io.Writer
4856
}
4957

5058
func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
@@ -53,8 +61,32 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
5361
return nil, fmt.Errorf("failed to parse API host: %w", err)
5462
}
5563

64+
baseTransport := http.DefaultTransport
65+
66+
if cfg.HTTPLogWriter != nil {
67+
httpLogger := &httpretty.Logger{
68+
Time: true,
69+
TLS: true,
70+
RequestHeader: true,
71+
RequestBody: true,
72+
ResponseHeader: true,
73+
ResponseBody: true,
74+
Colors: false, // erase line if you don't like colors
75+
Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}},
76+
MaxResponseBody: 10000,
77+
}
78+
httpLogger.SetOutput(cfg.HTTPLogWriter)
79+
httpLogger.SetBodyFilter(func(h http.Header) (skip bool, err error) {
80+
return !inspectableMIMEType(h.Get("Content-Type")), nil
81+
})
82+
baseTransport = httpLogger.RoundTripper(http.DefaultTransport)
83+
}
84+
5685
// Construct our REST client
57-
restClient := gogithub.NewClient(nil).WithAuthToken(cfg.Token)
86+
restHTTPClient := &http.Client{
87+
Transport: baseTransport,
88+
}
89+
restClient := gogithub.NewClient(restHTTPClient).WithAuthToken(cfg.Token)
5890
restClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", cfg.Version)
5991
restClient.BaseURL = apiHost.baseRESTURL
6092
restClient.UploadURL = apiHost.uploadURL
@@ -64,7 +96,7 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
6496
// did the necessary API host parsing so that github.com will return the correct URL anyway.
6597
gqlHTTPClient := &http.Client{
6698
Transport: &bearerAuthTransport{
67-
transport: http.DefaultTransport,
99+
transport: baseTransport,
68100
token: cfg.Token,
69101
},
70102
} // We're going to wrap the Transport later in beforeInit
@@ -169,6 +201,9 @@ type StdioServerConfig struct {
169201

170202
// Path to the log file if not stderr
171203
LogFilePath string
204+
205+
// LogAPIRequests indicates if we should log API requests to stderr
206+
LogAPIRequests bool
172207
}
173208

174209
// RunStdioServer is not concurrent safe.
@@ -179,6 +214,26 @@ func RunStdioServer(cfg StdioServerConfig) error {
179214

180215
t, dumpTranslations := translations.TranslationHelper()
181216

217+
logrusLogger := logrus.New()
218+
logLocation := os.Stderr
219+
if cfg.LogFilePath != "" {
220+
file, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
221+
if err != nil {
222+
return fmt.Errorf("failed to open log file: %w", err)
223+
}
224+
defer func() { _ = file.Close() }()
225+
226+
logLocation = file
227+
logrusLogger.SetLevel(logrus.DebugLevel)
228+
}
229+
logrusLogger.SetOutput(logLocation)
230+
stdLogger := log.New(logrusLogger.Writer(), "stdioserver", 0)
231+
232+
var httpLogWriter io.Writer
233+
if cfg.LogAPIRequests {
234+
httpLogWriter = logLocation
235+
}
236+
182237
ghServer, err := NewMCPServer(MCPServerConfig{
183238
Version: cfg.Version,
184239
Host: cfg.Host,
@@ -187,24 +242,13 @@ func RunStdioServer(cfg StdioServerConfig) error {
187242
DynamicToolsets: cfg.DynamicToolsets,
188243
ReadOnly: cfg.ReadOnly,
189244
Translator: t,
245+
HTTPLogWriter: httpLogWriter,
190246
})
191247
if err != nil {
192248
return fmt.Errorf("failed to create MCP server: %w", err)
193249
}
194250

195251
stdioServer := server.NewStdioServer(ghServer)
196-
197-
logrusLogger := logrus.New()
198-
if cfg.LogFilePath != "" {
199-
file, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
200-
if err != nil {
201-
return fmt.Errorf("failed to open log file: %w", err)
202-
}
203-
204-
logrusLogger.SetLevel(logrus.DebugLevel)
205-
logrusLogger.SetOutput(file)
206-
}
207-
stdLogger := log.New(logrusLogger.Writer(), "stdioserver", 0)
208252
stdioServer.SetErrorLogger(stdLogger)
209253

210254
if cfg.ExportTranslations {
@@ -378,3 +422,11 @@ func (t *bearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, erro
378422
req.Header.Set("Authorization", "Bearer "+t.token)
379423
return t.transport.RoundTrip(req)
380424
}
425+
426+
var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`)
427+
428+
func inspectableMIMEType(t string) bool {
429+
return strings.HasPrefix(t, "text/") ||
430+
strings.HasPrefix(t, "application/x-www-form-urlencoded") ||
431+
jsonTypeRE.MatchString(t)
432+
}

0 commit comments

Comments
 (0)