Skip to content

Commit aa9623c

Browse files
committed
Add HTTP+SSE server command with graceful shutdown and logging middleware
I'm not sure we want to merge this, but this allowed me to try dotcom chat action as an MCP host, using `mark3labs/mcp-go` in copilot-api, connecting to a localhost version of github-mcp-server via HTTP/SSE. ``` export GITHUB_PERSONAL_ACCESS_TOKEN=<TOKEN_FROM_DEV_ENVIRONMENT> go run cmd/github-mcp-server/main.go http --gh-host http://api.github.localhost --port 4567 ```
1 parent 5eb969e commit aa9623c

File tree

2 files changed

+160
-7
lines changed

2 files changed

+160
-7
lines changed

README.md

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ and set it as the GITHUB_PERSONAL_ACCESS_TOKEN environment variable.
237237

238238
### Repository Content
239239

240-
- **Get Repository Content**
240+
- **Get Repository Content**
241241
Retrieves the content of a repository at a specific path.
242242

243243
- **Template**: `repo://{owner}/{repo}/contents{/path*}`
@@ -246,7 +246,7 @@ and set it as the GITHUB_PERSONAL_ACCESS_TOKEN environment variable.
246246
- `repo`: Repository name (string, required)
247247
- `path`: File or directory path (string, optional)
248248

249-
- **Get Repository Content for a Specific Branch**
249+
- **Get Repository Content for a Specific Branch**
250250
Retrieves the content of a repository at a specific path for a given branch.
251251

252252
- **Template**: `repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}`
@@ -256,7 +256,7 @@ and set it as the GITHUB_PERSONAL_ACCESS_TOKEN environment variable.
256256
- `branch`: Branch name (string, required)
257257
- `path`: File or directory path (string, optional)
258258

259-
- **Get Repository Content for a Specific Commit**
259+
- **Get Repository Content for a Specific Commit**
260260
Retrieves the content of a repository at a specific path for a given commit.
261261

262262
- **Template**: `repo://{owner}/{repo}/sha/{sha}/contents{/path*}`
@@ -266,7 +266,7 @@ and set it as the GITHUB_PERSONAL_ACCESS_TOKEN environment variable.
266266
- `sha`: Commit SHA (string, required)
267267
- `path`: File or directory path (string, optional)
268268

269-
- **Get Repository Content for a Specific Tag**
269+
- **Get Repository Content for a Specific Tag**
270270
Retrieves the content of a repository at a specific path for a given tag.
271271

272272
- **Template**: `repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}`
@@ -276,7 +276,7 @@ and set it as the GITHUB_PERSONAL_ACCESS_TOKEN environment variable.
276276
- `tag`: Tag name (string, required)
277277
- `path`: File or directory path (string, optional)
278278

279-
- **Get Repository Content for a Specific Pull Request**
279+
- **Get Repository Content for a Specific Pull Request**
280280
Retrieves the content of a repository at a specific path for a given pull request.
281281

282282
- **Template**: `repo://{owner}/{repo}/refs/pull/{pr_number}/head/contents{/path*}`
@@ -319,6 +319,35 @@ GitHub MCP Server running on stdio
319319
320320
```
321321
322+
## HTTP+SSE server
323+
324+
> [!WARNING]
325+
> This version of the server works with the [2024-11-05 MCP Spec](https://spec.modelcontextprotocol.io/specification/2024-11-05/), which requires a stateful connection for SSE. We plan to add support for a stateless mode in the future, as allowed by the [2025-03-26 MCP Spec](https://spec.modelcontextprotocol.io/specification/2025-03-26/changelog).
326+
327+
Run the server in HTTP mode with Server-Sent Events (SSE):
328+
329+
```sh
330+
go run cmd/github-mcp-server/main.go http
331+
```
332+
333+
The server will start on port 8080 by default. You can specify a different port using the `--port` flag:
334+
335+
```sh
336+
go run cmd/github-mcp-server/main.go http --port 3000
337+
```
338+
339+
The server accepts connections at `http://localhost:<port>` and communicates using Server-Sent Events (SSE).
340+
341+
Like the stdio server, ensure your GitHub Personal Access Token is set in the `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable before starting the server.
342+
343+
You can use the same flags as the stdio server:
344+
345+
- `--read-only`: Restrict the server to read-only operations
346+
- `--log-file`: Path to log file
347+
- `--enable-command-logging`: Enable logging of all command requests and responses
348+
- `--export-translations`: Save translations to a JSON file
349+
- `--gh-host`: Specify the GitHub hostname (for GitHub Enterprise, localhost etc.)
350+
322351
## i18n / Overriding descriptions
323352
324353
The descriptions of the tools can be overridden by creating a github-mcp-server.json file in the same directory as the binary.
@@ -376,7 +405,7 @@ Run **Preferences: Open User Settings (JSON)**, and create or append to the `mcp
376405
"args": ["stdio"],
377406
"env": {
378407
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:githubpat}"
379-
},
408+
}
380409
}
381410
}
382411
}

cmd/github-mcp-server/main.go

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io"
77
stdlog "log"
8+
"net/http"
89
"os"
910
"os/signal"
1011
"syscall"
@@ -44,6 +45,26 @@ var (
4445
}
4546
},
4647
}
48+
49+
httpCmd = &cobra.Command{
50+
Use: "http",
51+
Short: "Start HTTP server",
52+
Long: `Start a server that communicates via HTTP using Server-Sent Events (SSE).`,
53+
Run: func(cmd *cobra.Command, args []string) {
54+
logFile := viper.GetString("log-file")
55+
readOnly := viper.GetBool("read-only")
56+
exportTranslations := viper.GetBool("export-translations")
57+
port := viper.GetString("port")
58+
logger, err := initLogger(logFile)
59+
if err != nil {
60+
stdlog.Fatal("Failed to initialize logger:", err)
61+
}
62+
logCommands := viper.GetBool("enable-command-logging")
63+
if err := runHTTPServer(readOnly, logger, logCommands, exportTranslations, port); err != nil {
64+
stdlog.Fatal("failed to run http server:", err)
65+
}
66+
},
67+
}
4768
)
4869

4970
func init() {
@@ -56,15 +77,20 @@ func init() {
5677
rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file")
5778
rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)")
5879

59-
// Bind flag to viper
80+
// Add HTTP specific flags
81+
httpCmd.Flags().String("port", "8080", "Port for the HTTP server")
82+
83+
// Bind flags to viper
6084
viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
6185
viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
6286
viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
6387
viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations"))
6488
viper.BindPFlag("gh-host", rootCmd.PersistentFlags().Lookup("gh-host"))
89+
viper.BindPFlag("port", httpCmd.Flags().Lookup("port"))
6590

6691
// Add subcommands
6792
rootCmd.AddCommand(stdioCmd)
93+
rootCmd.AddCommand(httpCmd)
6894
}
6995

7096
func initConfig() {
@@ -159,6 +185,104 @@ func runStdioServer(readOnly bool, logger *log.Logger, logCommands bool, exportT
159185
return nil
160186
}
161187

188+
func runHTTPServer(readOnly bool, logger *log.Logger, logCommands bool, exportTranslations bool, port string) error {
189+
// Create app context
190+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
191+
defer stop()
192+
193+
// Create GH client
194+
token := os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN")
195+
if token == "" {
196+
logger.Fatal("GITHUB_PERSONAL_ACCESS_TOKEN not set")
197+
}
198+
ghClient := gogithub.NewClient(nil).WithAuthToken(token)
199+
200+
// Check GH_HOST env var first, then fall back to viper config
201+
host := os.Getenv("GH_HOST")
202+
if host == "" {
203+
host = viper.GetString("gh-host")
204+
}
205+
206+
if host != "" {
207+
var err error
208+
ghClient, err = ghClient.WithEnterpriseURLs(host, host)
209+
if err != nil {
210+
return fmt.Errorf("failed to create GitHub client with host: %w", err)
211+
}
212+
}
213+
214+
t, dumpTranslations := translations.TranslationHelper()
215+
216+
// Create GitHub server
217+
ghServer := github.NewServer(ghClient, readOnly, t)
218+
219+
if exportTranslations {
220+
// Once server is initialized, all translations are loaded
221+
dumpTranslations()
222+
}
223+
224+
// Create SSE server
225+
sseServer := server.NewSSEServer(ghServer)
226+
227+
// Start listening for messages
228+
errC := make(chan error, 1)
229+
go func() {
230+
// Configure and start HTTP server
231+
mux := http.NewServeMux()
232+
233+
// Add SSE handler with logging middleware if enabled
234+
var handler http.Handler = sseServer
235+
if logCommands {
236+
handler = loggingMiddleware(handler, logger)
237+
}
238+
mux.Handle("/", handler)
239+
240+
srv := &http.Server{
241+
Addr: ":" + port,
242+
Handler: mux,
243+
}
244+
245+
// Graceful shutdown
246+
go func() {
247+
<-ctx.Done()
248+
if err := srv.Shutdown(context.Background()); err != nil {
249+
logger.Errorf("HTTP server shutdown error: %v", err)
250+
}
251+
}()
252+
253+
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
254+
errC <- err
255+
}
256+
}()
257+
258+
// Output github-mcp-server string
259+
_, _ = fmt.Fprintf(os.Stderr, "GitHub MCP Server running on http://localhost:%s\n", port)
260+
261+
// Wait for shutdown signal
262+
select {
263+
case <-ctx.Done():
264+
logger.Infof("shutting down server...")
265+
case err := <-errC:
266+
if err != nil {
267+
return fmt.Errorf("error running server: %w", err)
268+
}
269+
}
270+
271+
return nil
272+
}
273+
274+
// loggingMiddleware wraps an http.Handler and logs requests
275+
func loggingMiddleware(next http.Handler, logger *log.Logger) http.Handler {
276+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
277+
logger.WithFields(log.Fields{
278+
"method": r.Method,
279+
"path": r.URL.Path,
280+
}).Info("Received request")
281+
282+
next.ServeHTTP(w, r)
283+
})
284+
}
285+
162286
func main() {
163287
if err := rootCmd.Execute(); err != nil {
164288
fmt.Println(err)

0 commit comments

Comments
 (0)