From c6750c54c28a47901b0dad5fc2ff09d7e432cef1 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 21 May 2025 16:54:05 +0200 Subject: [PATCH] WIP: Support logging API requests --- cmd/github-mcp-server/main.go | 3 + go.mod | 3 + go.sum | 4 + internal/ghmcp/server.go | 80 +++++++++++++++---- third-party-licenses.darwin.md | 1 + third-party-licenses.linux.md | 1 + third-party-licenses.windows.md | 1 + .../github.com/henvic/httpretty/LICENSE.md | 21 +++++ 8 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 third-party/github.com/henvic/httpretty/LICENSE.md diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index fb716f78..212c22ea 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -53,6 +53,7 @@ var ( ExportTranslations: viper.GetBool("export-translations"), EnableCommandLogging: viper.GetBool("enable-command-logging"), LogFilePath: viper.GetString("log-file"), + LogAPIRequests: viper.GetBool("log_api_requests"), } return ghmcp.RunStdioServer(stdioServerConfig) @@ -71,6 +72,7 @@ func init() { rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations") 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") + rootCmd.PersistentFlags().Bool("log-api-requests", false, "When enabled, the server will log all API requests to the log location (default stderr)") rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file") rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)") @@ -80,6 +82,7 @@ func init() { _ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only")) _ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file")) _ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging")) + _ = viper.BindPFlag("log_api_requests", rootCmd.PersistentFlags().Lookup("log-api-requests")) _ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations")) _ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host")) diff --git a/go.mod b/go.mod index 5c9bc081..3ddc584e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,8 @@ require ( github.com/stretchr/testify v1.10.0 ) +require golang.org/x/tools v0.22.0 // indirect + require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect @@ -20,6 +22,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.0 // indirect + github.com/henvic/httpretty v0.1.4 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/go.sum b/go.sum index 6d3d2976..59b26dea 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU= +github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -82,6 +84,8 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index a75a9e0c..03380959 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "os/signal" + "regexp" "strings" "syscall" @@ -16,6 +17,7 @@ import ( mcplog "github.com/github/github-mcp-server/pkg/log" "github.com/github/github-mcp-server/pkg/translations" gogithub "github.com/google/go-github/v69/github" + "github.com/henvic/httpretty" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/shurcooL/githubv4" @@ -45,6 +47,12 @@ type MCPServerConfig struct { // Translator provides translated text for the server tooling Translator translations.TranslationHelperFunc + + // HTTPLogWriter is the writer for HTTP request/response logs, nil means no logging + // TODO: In future, this should probably log back to the MCP server via notifications/message so that + // remote servers are able to deliver debug logs to the client. For the moment, this is a very + // effective solution for debugging stdio. + HTTPLogWriter io.Writer } func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { @@ -53,8 +61,32 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { return nil, fmt.Errorf("failed to parse API host: %w", err) } + baseTransport := http.DefaultTransport + + if cfg.HTTPLogWriter != nil { + httpLogger := &httpretty.Logger{ + Time: true, + TLS: true, + RequestHeader: true, + RequestBody: true, + ResponseHeader: true, + ResponseBody: true, + Colors: false, // erase line if you don't like colors + Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}}, + MaxResponseBody: 10000, + } + httpLogger.SetOutput(cfg.HTTPLogWriter) + httpLogger.SetBodyFilter(func(h http.Header) (skip bool, err error) { + return !inspectableMIMEType(h.Get("Content-Type")), nil + }) + baseTransport = httpLogger.RoundTripper(http.DefaultTransport) + } + // Construct our REST client - restClient := gogithub.NewClient(nil).WithAuthToken(cfg.Token) + restHTTPClient := &http.Client{ + Transport: baseTransport, + } + restClient := gogithub.NewClient(restHTTPClient).WithAuthToken(cfg.Token) restClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", cfg.Version) restClient.BaseURL = apiHost.baseRESTURL restClient.UploadURL = apiHost.uploadURL @@ -64,7 +96,7 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { // did the necessary API host parsing so that github.com will return the correct URL anyway. gqlHTTPClient := &http.Client{ Transport: &bearerAuthTransport{ - transport: http.DefaultTransport, + transport: baseTransport, token: cfg.Token, }, } // We're going to wrap the Transport later in beforeInit @@ -169,6 +201,9 @@ type StdioServerConfig struct { // Path to the log file if not stderr LogFilePath string + + // LogAPIRequests indicates if we should log API requests to stderr + LogAPIRequests bool } // RunStdioServer is not concurrent safe. @@ -179,6 +214,26 @@ func RunStdioServer(cfg StdioServerConfig) error { t, dumpTranslations := translations.TranslationHelper() + logrusLogger := logrus.New() + logLocation := os.Stderr + 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) + } + defer func() { _ = file.Close() }() + + logLocation = file + logrusLogger.SetLevel(logrus.DebugLevel) + } + logrusLogger.SetOutput(logLocation) + stdLogger := log.New(logrusLogger.Writer(), "stdioserver", 0) + + var httpLogWriter io.Writer + if cfg.LogAPIRequests { + httpLogWriter = logLocation + } + ghServer, err := NewMCPServer(MCPServerConfig{ Version: cfg.Version, Host: cfg.Host, @@ -187,24 +242,13 @@ func RunStdioServer(cfg StdioServerConfig) error { DynamicToolsets: cfg.DynamicToolsets, ReadOnly: cfg.ReadOnly, Translator: t, + HTTPLogWriter: httpLogWriter, }) if err != nil { return fmt.Errorf("failed to create MCP server: %w", err) } stdioServer := server.NewStdioServer(ghServer) - - logrusLogger := logrus.New() - 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) stdioServer.SetErrorLogger(stdLogger) if cfg.ExportTranslations { @@ -378,3 +422,11 @@ func (t *bearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, erro req.Header.Set("Authorization", "Bearer "+t.token) return t.transport.RoundTrip(req) } + +var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`) + +func inspectableMIMEType(t string) bool { + return strings.HasPrefix(t, "text/") || + strings.HasPrefix(t, "application/x-www-form-urlencoded") || + jsonTypeRE.MatchString(t) +} diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index cdb2af5b..a7577d9b 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -13,6 +13,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/google/go-github/v69/github](https://pkg.go.dev/github.com/google/go-github/v69/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v69.2.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) + - [github.com/henvic/httpretty](https://pkg.go.dev/github.com/henvic/httpretty) ([MIT](https://github.com/henvic/httpretty/blob/v0.1.4/LICENSE.md)) - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.28.0/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index cdb2af5b..a7577d9b 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -13,6 +13,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/google/go-github/v69/github](https://pkg.go.dev/github.com/google/go-github/v69/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v69.2.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) + - [github.com/henvic/httpretty](https://pkg.go.dev/github.com/henvic/httpretty) ([MIT](https://github.com/henvic/httpretty/blob/v0.1.4/LICENSE.md)) - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.28.0/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index 74d13898..95a68b01 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -13,6 +13,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/google/go-github/v69/github](https://pkg.go.dev/github.com/google/go-github/v69/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v69.2.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) + - [github.com/henvic/httpretty](https://pkg.go.dev/github.com/henvic/httpretty) ([MIT](https://github.com/henvic/httpretty/blob/v0.1.4/LICENSE.md)) - [github.com/inconshreveable/mousetrap](https://pkg.go.dev/github.com/inconshreveable/mousetrap) ([Apache-2.0](https://github.com/inconshreveable/mousetrap/blob/v1.1.0/LICENSE)) - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.28.0/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) diff --git a/third-party/github.com/henvic/httpretty/LICENSE.md b/third-party/github.com/henvic/httpretty/LICENSE.md new file mode 100644 index 00000000..426f2a87 --- /dev/null +++ b/third-party/github.com/henvic/httpretty/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Henrique Vicente + +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.