From dccb4b46df32feba78a0aaab6d5988e73f8b1a91 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Tue, 16 Jun 2026 19:54:59 +0200 Subject: [PATCH 1/2] build(oauth): bake in default OAuth credentials via build-time ldflags Inject the public OAuth client credentials (stored as the OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET repo secrets) at build time via -ldflags so official binaries and images ship a working default app for zero-config login. Security relies on PKCE, not on the secret. Local/dev builds leave the values empty and continue to require an explicit token or --oauth-client-id. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/docker-publish.yml | 3 +++ .github/workflows/goreleaser.yml | 2 ++ .goreleaser.yaml | 2 +- Dockerfile | 7 ++++++- cmd/github-mcp-server/main.go | 13 ++++++++++++- internal/buildinfo/buildinfo.go | 19 +++++++++++++++++++ 6 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 internal/buildinfo/buildinfo.go diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 4f452aac41..ac667558f2 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -117,6 +117,9 @@ jobs: platforms: linux/amd64,linux/arm64 build-args: | VERSION=${{ github.ref_name }} + secrets: | + oauth_client_id=${{ secrets.OAUTH_CLIENT_ID }} + oauth_client_secret=${{ secrets.OAUTH_CLIENT_SECRET }} # Sign the resulting Docker image digest except on PRs. # This will only write to the public Rekor transparency log when the Docker diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 1004fc2747..32a6bffad9 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -38,6 +38,8 @@ jobs: workdir: . env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OAUTH_CLIENT_ID: ${{ secrets.OAUTH_CLIENT_ID }} + OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_CLIENT_SECRET }} - name: Generate signed build provenance attestations for workflow artifacts uses: actions/attest-build-provenance@v4 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 54f6b9f409..36dfc47bce 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,7 +9,7 @@ builds: - env: - CGO_ENABLED=0 ldflags: - - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X github.com/github/github-mcp-server/internal/buildinfo.OAuthClientID={{ .Env.OAUTH_CLIENT_ID }} -X github.com/github/github-mcp-server/internal/buildinfo.OAuthClientSecret={{ .Env.OAUTH_CLIENT_SECRET }} goos: - linux - windows diff --git a/Dockerfile b/Dockerfile index a4ea1d03b8..4138e6bcf6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,9 +24,14 @@ COPY . . COPY --from=ui-build /app/pkg/github/ui_dist/* ./pkg/github/ui_dist/ # Build the server +# OAuth credentials are injected via build secrets so they are not baked into image history; the values are public in practice but kept out of layers. RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ - CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${VERSION} -X main.commit=$(git rev-parse HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --mount=type=secret,id=oauth_client_id \ + --mount=type=secret,id=oauth_client_secret \ + export OAUTH_CLIENT_ID="$(cat /run/secrets/oauth_client_id 2>/dev/null || echo '')" && \ + export OAUTH_CLIENT_SECRET="$(cat /run/secrets/oauth_client_secret 2>/dev/null || echo '')" && \ + CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${VERSION} -X main.commit=$(git rev-parse HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ) -X github.com/github/github-mcp-server/internal/buildinfo.OAuthClientID=${OAUTH_CLIENT_ID} -X github.com/github/github-mcp-server/internal/buildinfo.OAuthClientSecret=${OAUTH_CLIENT_SECRET}" \ -o /bin/github-mcp-server ./cmd/github-mcp-server # Make a stage to run the app diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index b329b5012d..2717930def 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/github/github-mcp-server/internal/buildinfo" "github.com/github/github-mcp-server/internal/ghmcp" "github.com/github/github-mcp-server/internal/oauth" "github.com/github/github-mcp-server/pkg/github" @@ -37,6 +38,16 @@ var ( RunE: func(_ *cobra.Command, _ []string) error { token := viper.GetString("personal_access_token") oauthClientID := viper.GetString("oauth-client-id") + oauthClientSecret := viper.GetString("oauth-client-secret") + // Fall back to the build-time baked-in client (official releases) when none is + // configured explicitly. The baked-in app is registered on github.com, so it is + // only applied to the default host; GHES/ghe.com users must bring their own + // --oauth-client-id. The secret tracks the id, so an explicitly provided id with + // no secret never picks up the baked-in secret. + if oauthClientID == "" && viper.GetString("host") == "" { + oauthClientID = buildinfo.OAuthClientID + oauthClientSecret = buildinfo.OAuthClientSecret + } if token == "" && oauthClientID == "" { return errors.New("authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN, or pass --oauth-client-id to log in via OAuth") } @@ -112,7 +123,7 @@ var ( } oauthConfig := oauth.NewGitHubConfig( oauthClientID, - viper.GetString("oauth-client-secret"), + oauthClientSecret, scopes, viper.GetString("host"), viper.GetInt("oauth-callback-port"), diff --git a/internal/buildinfo/buildinfo.go b/internal/buildinfo/buildinfo.go new file mode 100644 index 0000000000..cd5084fa29 --- /dev/null +++ b/internal/buildinfo/buildinfo.go @@ -0,0 +1,19 @@ +// Package buildinfo contains variables that are set at build time via ldflags. +// These allow official releases to ship default OAuth credentials so users can +// log in without configuring their own OAuth app. The values are public in +// practice (security relies on PKCE, not on the client secret), but are kept out +// of source and injected at build time. +// +// Example: +// +// go build -ldflags="-X github.com/github/github-mcp-server/internal/buildinfo.OAuthClientID=xxx" +package buildinfo + +// OAuthClientID is the default OAuth client ID, set at build time. Empty in +// local/dev builds. +var OAuthClientID string + +// OAuthClientSecret is the default OAuth client secret, set at build time. For +// public OAuth clients it is not truly secret per OAuth 2.1 — PKCE provides the +// security — but it is still injected at build time rather than committed. +var OAuthClientSecret string From 7157db4f9d0d5638830842846bb601489f7e1948 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Thu, 18 Jun 2026 10:58:09 +0200 Subject: [PATCH 2/2] fix(oauth): recognize github.com host aliases for the baked-in client Match the default host via oauth.NormalizeHost instead of only an empty host string, so an explicit GITHUB_HOST=github.com (or api.github.com) still counts as the default and keeps zero-config baked-in login working. GHES and ghe.com users continue to bring their own --oauth-client-id. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cmd/github-mcp-server/main.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index 2717930def..231b0cf2c3 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -42,9 +42,11 @@ var ( // Fall back to the build-time baked-in client (official releases) when none is // configured explicitly. The baked-in app is registered on github.com, so it is // only applied to the default host; GHES/ghe.com users must bring their own - // --oauth-client-id. The secret tracks the id, so an explicitly provided id with - // no secret never picks up the baked-in secret. - if oauthClientID == "" && viper.GetString("host") == "" { + // --oauth-client-id. Recognizing the host via NormalizeHost means an explicit + // GITHUB_HOST=github.com (or api.github.com) still counts as the default and keeps + // zero-config login working. The secret tracks the id, so an explicitly provided + // id with no secret never picks up the baked-in secret. + if oauthClientID == "" && oauth.NormalizeHost(viper.GetString("host")) == "https://github.com" { oauthClientID = buildinfo.OAuthClientID oauthClientSecret = buildinfo.OAuthClientSecret }