Skip to content

mcp-publisher: device-flow login fails with incorrect_device_code at the authorize step (1.7.9 and 1.8.1); pending-poll and payload verified healthy #1543

Closed

Description

@john-broadway

Summary

mcp-publisher login github fails with incorrect_device_code at the authorization step. Observed three times across two days and two publisher versions (1.7.9 twice on 2026-08-17, 1.8.1 once on 2026-08-18), same host. Polling while the code is still pending is healthy; the error appears only in runs where the user proceeds to authorize in the browser, well inside the 15-minute expiry window.

Logging in with github...

To authenticate, please:
1. Go to: https://github.com/login/device
2. Enter code: XXXX-XXXX
3. Authorize this application
Waiting for authorization...
Error: login failed: error polling for token: token request failed: incorrect_device_code

Per RFC 8628 and GitHub's device-flow docs, a pending poll should return authorization_pending, and incorrect_device_code means the polled device_code does not match one issued for that client_id. Getting it after a successful code issuance, from the same process that requested the code, is the puzzle.

Environment

What we ruled out

  1. The binary version. 1.7.9 and 1.8.1 fail identically.
  2. The pending-poll path. Ran mcp-publisher login github (1.8.1) and let it poll for 40 seconds without authorizing anything: no error, it polls quietly. So the loop, the payload it sends, and the network path are all fine while the code is pending.
  3. The wire and the payload shape. Reproduced the publisher's exact requests manually from the same host: POST https://github.com/login/device/code with {"client_id":"Iv23liUydBbI7Z2Q9bOZ","scope":"read:org read:user"} (JSON, same headers) issues a device code, and a single poll of POST https://github.com/login/oauth/access_token with the publisher's exact payload returns authorization_pending as expected.
  4. The account/namespace. The MCP_GITHUB_TOKEN provided-token path logs in and publishes fine with the same GitHub account (that is how we shipped in the end).

What we could not capture

Browser-side behavior at the authorize step. The failures correlate with the user completing (or attempting) authorization at github.com/login/device, but we did not record what GitHub displayed there. One of the three failures arrived within seconds of a rerun.

Related

#1289 is the same poll loop failing terminally on slow_down. Two observations about pollForToken in cmd/publisher/auth/github-at.go compound whatever the underlying GitHub-side condition is:

  • every non-authorization_pending error is treated as terminal, so a transient or ambiguous response kills the whole login irrecoverably;
  • the loop hardcodes interval = 5 and ignores the interval the device-code response actually returns.

Ask

  • Is the device flow for the Iv23liUydBbI7Z2Q9bOZ GitHub App known-healthy right now? Has anyone else reported incorrect_device_code after authorizing?
  • Independent of the root cause, could the poll loop distinguish terminal from transient errors per RFC 8628 (which would also close mcp-publisher: device-flow login treats GitHub slow_down as fatal instead of backing off #1289)? A login that dies on the last hop after the user has authorized is the most expensive place to fail.

Activity

  1. JosephDoUrden commented on Aug 27, 2026

    @JosephDoUrden
    Contributor

    Looked into this. I can reproduce the exact failure text in a test harness, and I've ruled out the obvious client side causes so far, the client id is fetched once per login and stays stable, and the poll payload is marshalled once so it can't drift between polls. Writing up the full analysis, will post it here.

  2. JosephDoUrden commented on Aug 28, 2026

    @JosephDoUrden
    Contributor

    Full analysis, as promised in the claim comment. Short version: I could not make any client-side cause survive testing; the evidence points at a transient on GitHub's side at the claim step, for which there is direct precedent. What the client does control is that it turns one transient bad answer into a dead login, and that part is fixable regardless of root cause.

    Reproducing the failure path

    The exact symptom string — login failed: error polling for token: token request failed: incorrect_device_code — reproduces byte-for-byte in an httptest harness the moment the token endpoint answers incorrect_device_code once. Every non-authorization_pending error in pollForToken is terminal on first sight, so a single anomalous response at the very moment the user authorises kills the whole login. That matches your observation that pending-polling is healthy and the failure correlates with the authorise step: pending is the only error the loop tolerates.

    Client-side hypotheses, falsified

    • client_id changing mid-run (registry health serving a different app id between code issuance and poll): the publisher fetches the id once and reuses it for both requests — pinned by a test — and the live health endpoint served the same Iv23liUydBbI7Z2Q9bOZ on every probe today.
    • Payload drift between polls: the token-request body is marshalled once, before the loop; the same bytes are re-sent every iteration.
    • Stale local state: the publisher keeps no on-disk device-flow state that could go stale between runs.
    • Version regression: git diff 35b20719 f52dc852 -- cmd/publisher/auth/ is empty. The auth code is byte-identical between 1.7.9 and 1.8.1, which is why they fail identically — independently confirms your ruling-out of the binary.
    • Go HTTP internals replaying the request: nothing in this path retries a POST on its own.

    Precedent

    cli/cli#9302 is the same shape — device-flow logins intermittently failing at the claim step with device-code errors — and was closed by a cli maintainer with: "I believe this was a race condition in the platform which is now fixed." (Jul 2024.) So GitHub's device flow has produced exactly this class of transient before, platform-side. Whether what you hit is a recurrence or a sibling race, it is consistent with everything you captured, including the failure arriving within seconds of a rerun.

    What the client should do regardless

    I have the loop hardened on top of #1290's branch (it touches the same code, and #1290 should land first — its four tests pass unmodified under these changes, credit to its author):

    • grace-retry incorrect_device_code a couple of times before declaring the login dead — a genuinely invalid code fails identically a few seconds later, a transient recovers;
    • surface error_description/error_uri, so the next report of this is not opaque;
    • honour the interval/expires_in the device-code response actually returns (RFC 8628 §3.2) instead of the hardcoded 5s/900s;
    • wait one interval before the first poll — polling at t=0 can land inside the window where the just-issued code has not propagated to the token endpoint (cli/oauth waits the same way);
    • bounded retry on 5xx from the token endpoint, with the overall deadline still ending the loop.

    77+/4− plus seven tests, including one that pins the exact symptom string from this issue. Happy to open it as a PR once #1290 lands, or fold whichever parts are wanted into a different shape.

    Open questions for maintainers: (1) any other reports of incorrect_device_code after authorise against the registry's app? (2) is on-top-of-#1290 the right shape, or should the resilience changes go separately? (3) any objection to grace-retrying incorrect_device_code specifically? (4) worth taking error_description surfacing on its own even if the rest waits?

  3. john-broadway commented on Sep 1, 2026

    @john-broadway
    Author

    Retested today: same 1.8.1 binary (commit f52dc85, the checksum-verified release build that failed on 2026-08-18), same host, full device flow. It succeeded end to end, including the authorize step where all three original failures fired:

    Waiting for authorization...
    Successfully authenticated!
    ✓ Successfully logged in
    

    That lands exactly where @JosephDoUrden's analysis pointed: no client-side cause survives testing, and the precedent says a transient on GitHub's side at the claim step. Thank you for that work, it is a thorough piece of elimination and it made this retest the only step left.

    Closing as resolved-by-transient. The two pollForToken observations stand on their own regardless: treating every non-authorization_pending response as terminal is what turned a one-poll blip into a dead login, and the hardcoded 5s interval ignores what the device-code response returns. #1289 tracks the slow_down half of that. A GitHub hiccup should cost one poll, not the whole flow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions