Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/github-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ var (
LockdownMode: viper.GetBool("lockdown-mode"),
RepoAccessCacheTTL: &ttl,
ScopeChallenge: viper.GetBool("scope-challenge"),
ReadOnly: viper.GetBool("read-only"),
}

return ghhttp.RunHTTPServer(httpConfig)
Expand Down
48 changes: 48 additions & 0 deletions pkg/http/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,3 +409,51 @@ func TestHTTPHandlerRoutes(t *testing.T) {
})
}
}

func TestWithGlobalReadonly(t *testing.T) {
tests := []struct {
name string
readonly bool
expectReadonly bool
}{
{
name: "readonly config enables readonly mode",
readonly: true,
expectReadonly: true,
},
{
name: "non-readonly config leaves readonly disabled",
readonly: false,
expectReadonly: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &ServerConfig{
Version: "test",
ReadOnly: tt.readonly,
}

middleware := withGlobalReadonly(cfg)

// Create a handler that will check the context
var contextReadonly bool
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
contextReadonly = ghcontext.IsReadonly(r.Context())
w.WriteHeader(http.StatusOK)
})

// Apply middleware
handler := middleware(next)

// Create request and execute
req := httptest.NewRequest(http.MethodGet, "/", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)

// Verify the readonly state matches the config
assert.Equal(t, tt.expectReadonly, contextReadonly, "readonly context should match config")
})
}
}
20 changes: 20 additions & 0 deletions pkg/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ type ServerConfig struct {
// ScopeChallenge indicates if we should return OAuth scope challenges, and if we should perform
// tool filtering based on token scopes.
ScopeChallenge bool

// ReadOnly indicates if the server should run in read-only mode globally
ReadOnly bool
}

func RunHTTPServer(cfg ServerConfig) error {
Expand Down Expand Up @@ -145,6 +148,9 @@ func RunHTTPServer(cfg ServerConfig) error {
// Register Middleware First, needs to be before route registration
handler.RegisterMiddleware(r)

// Apply global readonly setting from server config
r.Use(withGlobalReadonly(&cfg))

// Register MCP server routes
handler.RegisterRoutes(r)
})
Expand Down Expand Up @@ -219,3 +225,17 @@ func createHTTPFeatureChecker() inventory.FeatureFlagChecker {
return false, nil
}
}

// withGlobalReadonly is middleware that applies the server's global readonly setting
func withGlobalReadonly(cfg *ServerConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if cfg.ReadOnly {
ctx := ghcontext.WithReadonly(r.Context(), true)
next.ServeHTTP(w, r.WithContext(ctx))
} else {
next.ServeHTTP(w, r)
}
})
}
}
Loading