Skip to content

Commit 5813625

Browse files
committed
Switch to cobra for command-line
1 parent e103f6b commit 5813625

18 files changed

Lines changed: 439 additions & 357 deletions

‎README.md‎

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ go install github.com/bigkevmcd/githubstatus/cmd/githubstatus@latest
1616
```
1717

1818
```sh
19-
githubstatus -overall major \
20-
-component "Actions=partial_outage" \
21-
-component "Git Operations=degraded_performance"
19+
githubstatus --overall major \
20+
--component "Actions=partial_outage" \
21+
--component "Git Operations=degraded_performance"
2222
```
2323

2424
The tool prints the current status and any violations, and sets its exit code
@@ -33,18 +33,18 @@ from the result:
3333

3434
| Flag | Default | Description |
3535
|------------------------|---------|----------------------------------------------------------------------|
36-
| `-overall` | `major` | Lowest overall indicator that counts as unhealthy; `""` disables it |
37-
| `-component Name=status` | | Threshold for one component; repeat the flag for more. Without any, the tool fetches only the small `status.json` |
38-
| `-fail-on-maintenance` | `false` | Treat planned maintenance as unhealthy |
39-
| `-fail-on-missing` | `false` | Exit 1 if a `-component` name doesn't exist |
40-
| `-base-url` | githubstatus.com | Statuspage API base URL |
41-
| `-timeout` | `10s` | Timeout for fetching the status |
36+
| `--overall` | `major` | Lowest overall indicator that counts as unhealthy; `""` disables it |
37+
| `--component Name=status` | | Threshold for one component; repeat the flag for more. Without any, the tool fetches only the small `status.json` |
38+
| `--fail-on-maintenance` | `false` | Treat planned maintenance as unhealthy |
39+
| `--fail-on-missing` | `false` | Exit 1 if a `--component` name doesn't exist |
40+
| `--base-url` | githubstatus.com | Statuspage API base URL |
41+
| `--timeout` | `10s` | Timeout for fetching the status |
4242
| `-q` | `false` | Print nothing; only set the exit code |
4343

4444
For example, to skip a deploy step while GitHub Actions is having problems:
4545

4646
```sh
47-
githubstatus -q -component "Actions=partial_outage" && ./deploy.sh
47+
githubstatus -q --component "Actions=partial_outage" && ./deploy.sh
4848
```
4949

5050
## MCP server
@@ -73,21 +73,21 @@ Other MCP clients use a config like this:
7373
}
7474
```
7575

76-
To run it as a shared server instead, use `-http`. This serves the MCP
76+
To run it as a shared server instead, use `--http`. This serves the MCP
7777
streamable HTTP transport at `/mcp`:
7878

7979
```sh
80-
githubstatus-mcp -http localhost:8080 # endpoint: http://localhost:8080/mcp
80+
githubstatus-mcp --http localhost:8080 # endpoint: http://localhost:8080/mcp
8181
```
8282

8383
The HTTP server rejects cross-origin browser requests and DNS-rebinding
8484
attempts, but it has no authentication. Only expose it on a trusted network.
8585

8686
| Flag | Default | Description |
8787
|--------------|------------------|----------------------------------------------------------|
88-
| `-http` | | Serve over HTTP on this address instead of stdio |
89-
| `-cache-ttl` | `30s` | How long to reuse API responses; `0` disables caching |
90-
| `-base-url` | githubstatus.com | Statuspage API base URL |
88+
| `--http` | | Serve over HTTP on this address instead of stdio |
89+
| `--cache-ttl` | `30s` | How long to reuse API responses; `0` disables caching |
90+
| `--base-url` | githubstatus.com | Statuspage API base URL |
9191

9292
### Tools
9393

‎client.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func (c *Client) get(ctx context.Context, path string, v any) error {
127127
if err != nil {
128128
return fmt.Errorf("githubstatus: %w", err)
129129
}
130-
defer resp.Body.Close()
130+
defer func() { _ = resp.Body.Close() }()
131131

132132
if resp.StatusCode != http.StatusOK {
133133
body, _ := io.ReadAll(io.LimitReader(resp.Body, maxErrorBodyBytes))

‎cmd/githubstatus-mcp/main.go‎

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
// exposes GitHub's status from githubstatus.com.
33
//
44
// By default it communicates over stdio, so it can be launched directly by
5-
// an MCP client. With -http it instead serves the MCP streamable HTTP
5+
// an MCP client. With --http it instead serves the MCP streamable HTTP
66
// transport at /mcp on the given address:
77
//
8-
// githubstatus-mcp -http localhost:8080
8+
// githubstatus-mcp --http localhost:8080
99
//
1010
// The HTTP server has no authentication; only expose it on a trusted
1111
// network.
@@ -14,7 +14,6 @@ package main
1414
import (
1515
"context"
1616
"errors"
17-
"flag"
1817
"fmt"
1918
"io"
2019
"log"
@@ -27,6 +26,7 @@ import (
2726
"time"
2827

2928
"github.com/modelcontextprotocol/go-sdk/mcp"
29+
"github.com/spf13/cobra"
3030

3131
"github.com/bigkevmcd/githubstatus"
3232
"github.com/bigkevmcd/githubstatus/mcpserver"
@@ -41,10 +41,6 @@ func main() {
4141

4242
stop()
4343

44-
if errors.Is(err, flag.ErrHelp) {
45-
return
46-
}
47-
4844
if err != nil {
4945
os.Exit(1)
5046
}
@@ -56,44 +52,55 @@ func main() {
5652
func run(ctx context.Context, args []string, stderr io.Writer) error {
5753
logger := log.New(stderr, "githubstatus-mcp: ", 0)
5854

59-
fs := flag.NewFlagSet("githubstatus-mcp", flag.ContinueOnError)
60-
fs.SetOutput(stderr)
61-
baseURL := fs.String("base-url", githubstatus.DefaultBaseURL, "Statuspage API base URL")
62-
httpAddr := fs.String("http", "", `serve MCP over streamable HTTP at `+httpPath+` on this address (e.g. "localhost:8080") instead of stdio`)
63-
cacheTTL := fs.Duration("cache-ttl", mcpserver.DefaultCacheTTL, "how long to reuse API responses; negative disables caching")
64-
65-
fs.Usage = func() {
66-
fmt.Fprintf(stderr, "Usage: githubstatus-mcp [flags]\n\nRuns an MCP server exposing GitHub's status, over stdio by default.\n\nFlags:\n")
67-
fs.PrintDefaults()
68-
}
69-
if err := fs.Parse(args); err != nil {
70-
return err
71-
}
72-
73-
if fs.NArg() > 0 {
74-
err := fmt.Errorf("unexpected arguments: %v", fs.Args())
75-
logger.Print(err)
76-
77-
return err
78-
}
79-
80-
if *cacheTTL == 0 {
81-
*cacheTTL = -1 // the flag's 0 means "off"; Options' 0 means "default"
55+
baseURL := githubstatus.DefaultBaseURL
56+
57+
var httpAddr string
58+
59+
cacheTTL := mcpserver.DefaultCacheTTL
60+
cmd := &cobra.Command{
61+
Use: "githubstatus-mcp",
62+
Short: "Expose GitHub status over MCP",
63+
SilenceUsage: true,
64+
SilenceErrors: true,
65+
Args: func(_ *cobra.Command, args []string) error {
66+
if len(args) > 0 {
67+
return fmt.Errorf("unexpected arguments: %v", args)
68+
}
69+
70+
return nil
71+
},
72+
RunE: func(_ *cobra.Command, _ []string) error {
73+
if cacheTTL == 0 {
74+
cacheTTL = -1 // the flag's 0 means "off"; Options' 0 means "default"
75+
}
76+
77+
server := mcpserver.New(&githubstatus.Client{BaseURL: baseURL}, &mcpserver.Options{
78+
Version: version(),
79+
CacheTTL: cacheTTL,
80+
})
81+
82+
var err error
83+
if httpAddr != "" {
84+
err = serveHTTP(ctx, server, httpAddr, logger)
85+
} else {
86+
err = server.Run(ctx, &mcp.StdioTransport{})
87+
}
88+
89+
if err != nil && ctx.Err() == nil {
90+
return err
91+
}
92+
93+
return nil
94+
},
8295
}
83-
84-
server := mcpserver.New(&githubstatus.Client{BaseURL: *baseURL}, &mcpserver.Options{
85-
Version: version(),
86-
CacheTTL: *cacheTTL,
87-
})
88-
89-
var err error
90-
if *httpAddr != "" {
91-
err = serveHTTP(ctx, server, *httpAddr, logger)
92-
} else {
93-
err = server.Run(ctx, &mcp.StdioTransport{})
94-
}
95-
96-
if err != nil && ctx.Err() == nil {
96+
cmd.Flags().StringVar(&baseURL, "base-url", baseURL, "Statuspage API base URL")
97+
cmd.Flags().StringVar(&httpAddr, "http", "", `serve MCP over streamable HTTP at `+httpPath+` on this address (e.g. "localhost:8080") instead of stdio`)
98+
cmd.Flags().DurationVar(&cacheTTL, "cache-ttl", cacheTTL, "how long to reuse API responses; negative disables caching")
99+
cmd.SetArgs(append([]string{}, args...))
100+
cmd.SetOut(stderr)
101+
cmd.SetErr(stderr)
102+
103+
if err := cmd.ExecuteContext(ctx); err != nil {
97104
logger.Print(err)
98105
return err
99106
}

‎cmd/githubstatus-mcp/main_test.go‎

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import (
44
"bufio"
55
"bytes"
66
"context"
7-
"errors"
8-
"flag"
97
"io"
108
"net/http"
119
"net/http/httptest"
@@ -27,10 +25,12 @@ const runMainEnv = "GITHUBSTATUS_MCP_TEST_RUN_MAIN"
2725
func TestMain(m *testing.M) {
2826
if os.Getenv(runMainEnv) == "1" {
2927
main()
28+
// The test binary is acting as the command process in this branch.
29+
//revive:disable-next-line:redundant-test-main-exit
3030
os.Exit(0)
3131
}
3232

33-
os.Exit(m.Run())
33+
m.Run()
3434
}
3535

3636
// newStatusAPI serves the summary fixture and returns its base URL.
@@ -89,15 +89,21 @@ func checkSession(t *testing.T, cs *mcp.ClientSession) {
8989
}
9090

9191
out, ok := res.StructuredContent.(map[string]any)
92-
if !ok || out["healthy"] != false || out["description"] != "Minor Service Outage" {
92+
if !ok {
93+
t.Errorf("check_github_health = %#v", res.StructuredContent)
94+
return
95+
}
96+
97+
healthy, healthyOK := out["healthy"].(bool)
98+
if !healthyOK || healthy || out["description"] != "Minor Service Outage" {
9399
t.Errorf("check_github_health = %#v", res.StructuredContent)
94100
}
95101
}
96102

97103
func TestStdio(t *testing.T) {
98104
baseURL := newStatusAPI(t)
99105

100-
cmd := exec.Command(os.Args[0], "-base-url", baseURL)
106+
cmd := exec.Command(os.Args[0], "--base-url", baseURL)
101107

102108
cmd.Env = append(os.Environ(), runMainEnv+"=1")
103109

@@ -136,7 +142,7 @@ func TestHTTP(t *testing.T) {
136142

137143
done := make(chan error, 1)
138144
go func() {
139-
done <- run(ctx, []string{"-http", "127.0.0.1:0", "-base-url", baseURL}, pw)
145+
done <- run(ctx, []string{"--http", "127.0.0.1:0", "--base-url", baseURL}, pw)
140146

141147
_ = pw.Close()
142148
}()
@@ -205,25 +211,25 @@ func TestRunErrors(t *testing.T) {
205211
tests := []struct {
206212
name string
207213
args []string
208-
wantErr error
214+
wantError bool
209215
wantStderr string
210216
}{
211-
{"help", []string{"-h"}, flag.ErrHelp, "Usage: githubstatus-mcp"},
212-
{"unknown flag", []string{"-bogus"}, nil, "flag provided but not defined: -bogus"},
213-
{"unexpected argument", []string{"extra"}, nil, "unexpected arguments: [extra]"},
214-
{"bad listen address", []string{"-http", "256.0.0.1:bad"}, nil, "githubstatus-mcp: listen tcp"},
217+
{"help", []string{"--help"}, false, "Usage:\n githubstatus-mcp [flags]"},
218+
{"unknown flag", []string{"--bogus"}, true, "unknown flag: --bogus"},
219+
{"unexpected argument", []string{"extra"}, true, "unexpected arguments: [extra]"},
220+
{"bad listen address", []string{"--http", "256.0.0.1:bad"}, true, "githubstatus-mcp: listen tcp"},
215221
}
216222
for _, tt := range tests {
217223
t.Run(tt.name, func(t *testing.T) {
218224
var stderr bytes.Buffer
219225

220226
err := run(context.Background(), tt.args, &stderr)
221-
if err == nil {
227+
if err == nil && tt.wantError {
222228
t.Fatal("expected an error")
223229
}
224230

225-
if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
226-
t.Errorf("err = %v, want %v", err, tt.wantErr)
231+
if err != nil && !tt.wantError {
232+
t.Fatalf("err = %v, want nil", err)
227233
}
228234

229235
if !strings.Contains(stderr.String(), tt.wantStderr) {

0 commit comments

Comments
 (0)