Skip to content

mcp: support HTTPS redirect_uri for OAuth callback (TLS on loopback listener) #55965

Open

Description

@martinkihuha

Summary

OpenCode's built-in MCP OAuth flow cannot complete against authorization servers that only accept HTTPS redirect_uri values (e.g. iManage Cloud). Registration succeeds only with an https:// loopback, but the local callback server speaks plain HTTP, so the browser redirect fails the TLS handshake.

Environment

  • opencode version: 2.0.28 (channel latest, release build, local=false); also observed on 1.18.35
  • OS: Linux 7.2.5-3-omarchy (linux x64)
  • Terminal: TERM=xterm-256color, COLORTERM=truecolor
  • Shell: /usr/bin/zsh
  • Install/channel: mise, release channel latest
  • Active plugins: none found in config

Reproduction

  1. Configure a remote MCP server whose authorization server enforces HTTPS redirect URIs:
    { "mcp": { "imanage-work": { "type": "remote", "url": "https://cloudimanage.com/mcp/work", "oauth": {} } }
  2. Run opencode mcp auth imanage-work.
  3. Dynamic client registration posts redirect_uris: ["http://127.0.0.1:<port>/mcp/oauth/callback"] and the server rejects it:
    { "error": "invalid_client_metadata", "error_description": "All redirect_uris must be absolute URLs with allowed protocols" }
    (Verified by replaying the DCR request manually: http:// redirect → rejected; https://127.0.0.1:19876/mcp/oauth/callback → accepted with client_id + client_secret.)
  4. Workaround attempt: pin the DCR-issued client_id/client_secret plus "redirectUri": "https://127.0.0.1:19876/mcp/oauth/callback" in config. The browser then opens the IdP authorize URL fine, but the redirect to https://127.0.0.1:19876/... fails with ERR_SSL_PROTOCOL_ERROR, because the local callback server is plain HTTP.

Expected Behavior

There should be a supported way to complete built-in MCP OAuth against HTTPS-only authorization servers — e.g. serve TLS on the loopback callback (user-provided or auto-generated cert), or document a supported reverse-proxy/adapter pattern.

Actual Behavior

No overlap is possible: the IdP requires https:// redirect URIs while OpenCode only serves http:// on the callback. Relevant code in packages/core/src/mcp/oauth.ts (dev branch):

  • default redirect is http://127.0.0.1/callback (~line 152),
  • the callback listener is created via node:http with no TLS option (~line 358),
  • the code even notes "a mismatch strands the browser" (~line 384) when the redirect port/URL doesn't match the listener.

So HTTPS-only IdPs can never finish opencode mcp auth, and the failure surfaces as a raw browser TLS error with no actionable message.

Additional Context

  • Consistently reproducible against iManage Cloud (https://cloudimanage.com/mcp/work, scopes like imanage_work_mcp_scopes). RFC 8252 §7.3 recommends native apps use HTTP loopbacks, but some enterprise IdPs (iManage here) mandate HTTPS even for loopback — OpenCode has no answer for that class of server today.
  • Current workaround: an external helper script that performs DCR + PKCE itself, serves the callback over HTTPS with a self-signed cert, and writes tokens into the v1 mcp-auth.json store (which only works on v1 — see companion compat issue). Refresh-token rotation thereafter works without a browser, but any fresh login needs the helper.
  • Feature suggestion: accept oauth.tlsCert/oauth.tlsKey (or certFile/keyFile) for the callback listener, use them when redirect_uri is https://, and surface a clear error when an https:// redirect is configured without TLS material.

Activity

  1. opencode-agent commented on Oct 16, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed write-up and for checking the DCR behaviour by hand. You're asking for OpenCode's MCP OAuth flow to support authorization servers that only accept https:// redirect URIs, by serving TLS on the loopback callback listener (with a user-provided cert/key) and showing a clear error when an https:// redirect is configured without one. Maintainers will consider it.

    In the meantime, you might be able to get this working on v2 without the external helper script. The configured redirectUri is used as-is for client registration and the browser redirect. The local listener binds to callbackPort first and only uses the port from redirectUri if callbackPort isn't set. So you can point them at different ports and put a local TLS-terminating proxy in between:

    {
      "mcp": {
        "imanage-work": {
          "type": "remote",
          "url": "https://cloudimanage.com/mcp/work",
          "oauth": {
            "redirectUri": "https://127.0.0.1:19876/mcp/oauth/callback",
            "callbackPort": 19877
          }
        }
      }
    }

    Then, while opencode mcp auth imanage-work runs, forward https://127.0.0.1:19876 to http://127.0.0.1:19877 with any local TLS proxy. For example, with Caddy: caddy reverse-proxy --from https://127.0.0.1:19876 --to 127.0.0.1:19877, or use a self-signed cert with your proxy of choice. Because redirectUri is set, dynamic client registration sends the https:// URI, so you shouldn't need to pin clientId/clientSecret by hand. I haven't tested this against iManage, so please let us know whether it works.

    Related issues:

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