Skip to content

Instantly share code, notes, and snippets.

@kentcdodds
Created September 7, 2026 16:21
Show Gist options
  • Select an option

  • Save kentcdodds/491cea8146dbe3458f023f5f5a6096ec to your computer and use it in GitHub Desktop.

Select an option

Save kentcdodds/491cea8146dbe3458f023f5f5a6096ec to your computer and use it in GitHub Desktop.
Devin + Kody issue

Right—the clone won’t have production request logs. I was considering a live Cloudflare log stream, but I don’t have that tooling set up here.

Instead, I can use a temporary localhost OAuth diagnostic proxy with Devin’s CLI. It will forward the exchange to kody.codes and report only the HTTP status and OAuth error fields—not codes, headers, or tokens. That should reveal the error Devin is currently hiding, without changing Kody or your existing MCP configuration.

The diagnostic proxy is running on localhost. I’ll start one more sign-in; please authorize it in the browser. This uses a separate temporary kody-diagnostic entry and leaves your existing kody entry unchanged.

The diagnostic attempt hit Cloudflare 403 / error 1010 (browser_signature_banned) before reaching Kody’s OAuth handler. That happened on the proxy’s registration request, so it does not yet prove the cause of Devin’s original token-exchange failure.

I’ll compare requests with and without a User-Agent using deliberately invalid, non-sensitive OAuth inputs. This needs no browser sign-in and may tell us whether Cloudflare’s browser checks are interfering.

The requests without a User-Agent reached Kody normally. The Cloudflare block was caused by Python’s default User-Agent in my proxy, which I’ve removed so it preserves Devin’s behavior. That was a diagnostic artifact, not an explanation of the original failure.

The proxy check now passes. I’ll retry the diagnostic sign-in.

The instrumented flow reproduced Devin’s error, but no request reached the token endpoint.

I found a plausible explanation: Kody advertises RFC 9207 issuer validation, and Rust MCP clients must preserve the callback’s iss parameter when handing it to the SDK. I’ll test that behavior with a fully local mock OAuth server. This requires no real credentials or further Kody sign-ins.

I have a deterministic client-side reproduction:

Local mock server setting Callback includes the correct iss Token requests sent
Issuer validation advertised: true Yes 0
Issuer validation advertised: false Yes 1

Only that metadata flag changed. Kody advertises true.

This strongly points to Devin’s callback handling—likely dropping iss before passing the callback to the Rust MCP SDK. I can’t confirm that internal detail without Devin’s source, but the failing behavior is isolated. I won’t disable issuer validation on Kody as a workaround.

The connection is still blocked, but I isolated a reproducible Devin-side OAuth failure. Here’s a report you can forward.


Devin MCP OAuth fails when RFC 9207 issuer validation is advertised

Environment

  • Devin bundled CLI: 3000.5.20 (2d902011)
  • macOS, ARM64, Darwin 25.6
  • MCP endpoint: https://kody.codes/mcp
  • Tested September 7, 2026

User-visible behavior

  1. Browser authorization completes.
  2. The localhost callback page displays “Authentication complete.”
  3. Devin remains in “Needs auth”, and MCP tools are unavailable.
  4. Running devin mcp login kody directly reports:
    Error: Failed to exchange authorization code for tokens
    

Logout/login did not repair it. Logout initially reported no OAuth credentials stored for kody.

Decisive reproduction: no Kody or Cloudflare required

A standalone localhost OAuth mock implements discovery, public-client registration, and an authorization redirect containing:

  • An authorization code
  • The unchanged request state
  • An iss value exactly matching the discovery document’s issuer

Two runs changed only authorization_response_iss_parameter_supported:

Metadata flag Registrations Callbacks Token endpoint requests
true 1 1 0
false 1 1 1

With the flag enabled, Devin fails before sending the token request, despite receiving a correct issuer.

The mock deliberately rejects any token request with HTTP 400, so both runs end with the generic exchange error. The decisive difference is whether Devin reaches the token endpoint.

Why this affects Kody

Kody’s live authorization-server metadata includes:

{
  "issuer": "https://kody.codes",
  "authorization_response_iss_parameter_supported": true
}

Discovery endpoints return HTTP 200. The observed registration flow uses public-client authentication (none) and PKCE S256.

Likely cause — source inspection needed

Please inspect whether Devin’s loopback callback handler preserves iss and passes it into the MCP SDK.

The Rust MCP SDK exposes issuer-aware methods:

  • handle_callback_with_issuer(code, state, issuer)
  • handle_callback_url(callback_url)

Calling the issuer-less callback method, or dropping iss while parsing the redirect, would explain this result. The behavioral failure is reproduced; the exact internal cause remains unconfirmed.

SDK callback API documentation

Suggested fixes and regression coverage

  1. Preserve and validate the authorization-response issuer end to end.
  2. Test advertised issuer support with correct, missing, and mismatched callback issuers.
  3. Surface the underlying validation/exchange error instead of only the generic wrapper.
  4. Do not show browser authentication success until token exchange and credential persistence succeed—or label the page as “Authorization received” while those steps remain pending.

Standalone reproducer

Attach these files:

  • Mock OAuth server: <ref_file file="/tmp/kody-devin-oauth-diagnostic.Jc5mlR/mock_oauth.py" />
  • Isolated MCP configuration: <ref_file file="/tmp/kody-devin-oauth-diagnostic.Jc5mlR/.devin/mcp_config.local.json" />

Start the mock with ISSUER_SUPPORTED=true, run devin mcp login mock-issuer-test from the configuration’s project directory, then inspect http://127.0.0.1:18466/stats. Restart with ISSUER_SUPPORTED=false and repeat. No real credentials are needed.


All diagnostic servers are stopped. Your repository is clean, existing MCP configuration is unchanged, and I did not weaken Kody’s issuer validation.

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