Skip to content

opencode mcp auth fails to open browser for OAuth flow (Google Drive MCP) #26195

Closed as not planned

Description

@JulioBuscer

Description

When running opencode mcp auth gdrive to authenticate with the Google Drive MCP server, the command outputs "Authentication successful!" but the OAuth flow never completes. The browser never opens, and no tokens are saved.

  1. opencode mcp auth gdrive starts the OAuth flow but fails to open the browser (mcp.browser.open.failed in logs)
  2. Without the browser opening, the user can't authorize, so no tokens are received
  3. The command incorrectly says "Authentication successful!" even though it failed
  4. All GDrive operations return "Unauthorized"

Expected behavior:

  • Browser should open with Google OAuth authorization URL
  • After authorizing, tokens should be saved to ~/.local/share/opencode/mcp-auth.json
  • opencode mcp auth list should show "authenticated"
    Actual behavior:
  • Logs show mcp.browser.open.failed event
  • No browser window opens
  • Command prints "Authentication successful!" incorrectly
  • Tokens file ends up as {"gdrive": {}} (empty)
  • All GDrive operations return "Unauthorized"

Environment:

Logs:
INFO service=mcp.oauth-callback port=19876 path=/mcp/oauth/callback oauth callback server started
...
INFO service=bus type=mcp.browser.open.failed subscribing
...
◇ Authentication successful!

Additional context:
The opencode mcp debug gdrive command reports "Server responded successfully (no auth required or already authenticated)" because the Drive MCP server returns HTTP 200 even without authentication, so OpenCode doesn't detect that OAuth is needed until tools are actually called.

Plugins

MCP server: Google Drive (https://drivemcp.googleapis.com/mcp/v1)

OpenCode version

1.14.39

Steps to reproduce

  1. Configure Google Drive MCP in opencode.json:
    {
    mcp: {
    gdrive: {
    type: remote,
    url: https://drivemcp.googleapis.com/mcp/v1,
    oauth: {
    clientId: ...,
    clientSecret: ...,
    scope: https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/drive.file
    }
    }
    }
    }

  2. Run opencode mcp auth gdrive --print-logs

Screenshot and/or share link

Image Image Image

Operating System

Ubuntu 24.04.4 LTS (Noble Numbat)

Terminal

ps (bash)

Activity

  1. github-actions commented on May 7, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If your issue is indeed distinct (e.g., specific to the mcp auth CLI command rather than automatic OAuth triggering, or specific to the "Authentication successful!" false positive message), please add context clarifying how it differs.

  2. yanniznik commented on May 11, 2026

    @yanniznik

    I investigated this from the OpenCode binary and MCP protocol level. The core issue is that Google's MCP servers don't expose OAuth discovery, so OpenCode has no way to initiate the OAuth flow.

    How OAuth works for servers that DO work (e.g., Slack)

    Slack exposes /.well-known/oauth-authorization-server at its MCP URL:

    $ curl https://mcp.slack.com/.well-known/oauth-authorization-server
    {
      "issuer": "https://slack.com",
      "authorization_endpoint": "https://slack.com/oauth/v2_user/authorize",
      "token_endpoint": "https://slack.com/api/oauth.v2.user.access",
      ...
    }
    

    OpenCode discovers this → constructs the authorization URL → opens browser → user consents → callback delivers code → token exchange completes. This flow works reliably.

    Why Google MCP servers fail

    Google's MCP endpoints return 404 for OAuth discovery:

    $ curl https://people.googleapis.com/mcp/v1/.well-known/oauth-authorization-server
    404 Not Found
    
    $ curl https://people.googleapis.com/.well-known/oauth-authorization-server
    404 Not Found
    

    Additionally, Google's servers respond 200 to initialize and tools/list without any authentication. Auth is only enforced on tools/call. So OpenCode:

    1. Connects via StreamableHTTP → 200 OK
    2. Lists tools → gets toolCount (e.g., 3 for People, 10 for Gmail)
    3. Considers the connection "successful" → reports "Authentication successful!"
    4. Never triggers OAuth because the server never challenged it

    What OpenCode has available but doesn't use

    The opencode.json config already has the OAuth client config:

    "google-people": {
      "type": "remote",
      "url": "https://people.googleapis.com/mcp/v1",
      "oauth": {
        "clientId": "...",
        "clientSecret": "...",
        "scope": "..."
      }
    }

    Proposed fix

    When a server doesn't expose /.well-known/oauth-authorization-server but the config has an oauth block with clientId/clientSecret/scope, OpenCode should fall back to client-side OAuth configuration using Google's standard endpoints:

    • Authorization: https://accounts.google.com/o/oauth2/v2/auth
    • Token: https://oauth2.googleapis.com/token

    This would also fix BigQuery MCP and any other Google MCP server.

    Confirmed working services (once tokens are manually injected)

    Using manually obtained tokens written to mcp-auth.json, all Google MCP servers work:

    • ✅ People (3 tools)
    • ✅ Gmail (10 tools)
    • ✅ Calendar (8 tools)
    • ✅ Drive (8 tools)

    Environment

    • OpenCode 1.14.48
    • macOS 15.5 (darwin/arm64)
    • Google Workspace MCP (Developer Preview)
  3. romybompart commented on May 25, 2026

    @romybompart

    I have similar issue:

    (base) PS C:\Users\Romy1> opencode mcp auth drive

    T MCP OAuth Authentication
    |
    o Authentication successful!
    |
    — Done

    (base) PS C:\Users\Romy1> opencode mcp auth list

    T MCP OAuth Status
    |
    • ✗ drive not authenticated
    | https://drivemcp.googleapis.com/mcp/v1
    |
    — 1 OAuth-capable server(s)

  4. romybompart commented on May 25, 2026

    @romybompart

    I have similar issue:

    (base) PS C:\Users\Romy1> opencode mcp auth drive

    T MCP OAuth Authentication | o Authentication successful! | — Done

    (base) PS C:\Users\Romy1> opencode mcp auth list

    T MCP OAuth Status | • ✗ drive not authenticated | https://drivemcp.googleapis.com/mcp/v1 | — 1 OAuth-capable server(s)

    I was able to fix this by using the following script.
    prior to that I make sure I logout from the mcp using;
    opencode mcp logout google-drive-mcp

    Script:
    ``
    <#
    .SYNOPSIS
    Configure and authenticate OpenCode's Google Drive MCP server.

    .DESCRIPTION
    OpenCode currently can report Google Drive MCP as connected before OAuth tokens
    are stored, because the Drive MCP endpoint allows the initial MCP handshake
    without user authorization. This script performs the Google OAuth flow directly,
    then writes tokens to OpenCode's MCP auth store in the format OpenCode expects.

    Required environment variables:
    GOOGLE_DRIVE_MCP_CLIENT_ID
    GOOGLE_DRIVE_MCP_CLIENT_SECRET

    After this script succeeds, restart OpenCode so the running MCP client reloads
    the token from ~/.local/share/opencode/mcp-auth.json.
    #>

    [CmdletBinding()]
    param(
    [string]$ServerName = "drive",
    [string]$ServerUrl = "https://drivemcp.googleapis.com/mcp/v1",
    [int]$CallbackPort = 19876,
    [string]$CallbackPath = "/mcp/oauth/callback",
    [string]$ConfigPath = (Join-Path $HOME ".config\opencode\opencode.json"),
    [string]$AuthPath = (Join-Path $HOME ".local\share\opencode\mcp-auth.json"),
    [switch]$SkipConfigUpdate
    )

    $ErrorActionPreference = "Stop"

    $Scopes = @(
    "https://www.googleapis.com/auth/drive.readonly",
    "https://www.googleapis.com/auth/drive.file"
    )
    $Scope = $Scopes -join " "

    function New-Base64UrlRandomString {
    param([int]$ByteCount = 32)

    $bytes = New-Object byte[] $ByteCount
    [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
    return [Convert]::ToBase64String($bytes).TrimEnd("=").Replace("+", "-").Replace("/", "_")
    

    }

    function ConvertTo-Base64UrlSha256 {
    param([Parameter(Mandatory = $true)][string]$Value)

    $sha = [System.Security.Cryptography.SHA256]::Create()
    $hash = $sha.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($Value))
    return [Convert]::ToBase64String($hash).TrimEnd("=").Replace("+", "-").Replace("/", "_")
    

    }

    function ConvertTo-UrlEncoded {
    param([Parameter(Mandatory = $true)][string]$Value)

    return [Uri]::EscapeDataString($Value)
    

    }

    function Ensure-Directory {
    param([Parameter(Mandatory = $true)][string]$Path)

    if (-not (Test-Path -LiteralPath $Path)) {
        New-Item -ItemType Directory -Path $Path | Out-Null
    }
    

    }

    function Add-OrSetProperty {
    param(
    [Parameter(Mandatory = $true)]$Object,
    [Parameter(Mandatory = $true)][string]$Name,
    [Parameter(Mandatory = $true)]$Value
    )

    if ($Object.PSObject.Properties.Name -contains $Name) {
        $Object.$Name = $Value
    }
    else {
        $Object | Add-Member -NotePropertyName $Name -NotePropertyValue $Value
    }
    

    }

    function Write-JsonFile {
    param(
    [Parameter(Mandatory = $true)][string]$Path,
    [Parameter(Mandatory = $true)]$Value
    )

    Ensure-Directory -Path (Split-Path -Parent $Path)
    $Value | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $Path -Encoding UTF8
    

    }

    function Ensure-DriveMcpConfig {
    param(
    [Parameter(Mandatory = $true)][string]$Path,
    [Parameter(Mandatory = $true)][string]$Name,
    [Parameter(Mandatory = $true)][string]$Url,
    [Parameter(Mandatory = $true)][string]$ScopeValue
    )

    if (Test-Path -LiteralPath $Path) {
        $config = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
    }
    else {
        $config = [pscustomobject]@{
            '$schema' = "https://opencode.ai/config.json"
        }
    }
    
    if (-not ($config.PSObject.Properties.Name -contains "`$schema")) {
        Add-OrSetProperty -Object $config -Name '$schema' -Value "https://opencode.ai/config.json"
    }
    
    if (-not ($config.PSObject.Properties.Name -contains "mcp") -or $null -eq $config.mcp) {
        Add-OrSetProperty -Object $config -Name "mcp" -Value ([pscustomobject]@{})
    }
    
    $oauth = [pscustomobject]@{
        clientId     = "{env:GOOGLE_DRIVE_MCP_CLIENT_ID}"
        clientSecret = "{env:GOOGLE_DRIVE_MCP_CLIENT_SECRET}"
        scope        = $ScopeValue
    }
    
    $drive = [pscustomobject]@{
        type    = "remote"
        url     = $Url
        enabled = $true
        oauth   = $oauth
        timeout = 30000
    }
    
    Add-OrSetProperty -Object $config.mcp -Name $Name -Value $drive
    Write-JsonFile -Path $Path -Value $config
    

    }

    function Update-McpAuthFile {
    param(
    [Parameter(Mandatory = $true)][string]$Path,
    [Parameter(Mandatory = $true)][string]$Name,
    [Parameter(Mandatory = $true)][string]$Url,
    [Parameter(Mandatory = $true)]$TokenResponse
    )

    if (Test-Path -LiteralPath $Path) {
        $auth = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
    }
    else {
        $auth = [pscustomobject]@{}
    }
    
    $tokens = [ordered]@{
        accessToken = $TokenResponse.access_token
        expiresAt   = [int]([DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + $TokenResponse.expires_in)
        scope       = $TokenResponse.scope
    }
    
    if ($TokenResponse.refresh_token) {
        $tokens.refreshToken = $TokenResponse.refresh_token
    }
    else {
        Write-Warning "Google did not return a refresh token. If auth expires later, revoke app access in Google Account settings and rerun this script."
    }
    
    $entry = [pscustomobject]@{
        tokens    = ([pscustomobject]$tokens)
        serverUrl = $Url
    }
    
    Add-OrSetProperty -Object $auth -Name $Name -Value $entry
    Write-JsonFile -Path $Path -Value $auth
    

    }

    function Invoke-OpenCodeAuthStatus {
    $command = Get-Command opencode -ErrorAction SilentlyContinue
    if (-not $command) {
    Write-Warning "opencode was not found on PATH. Tokens were written, but auth status could not be verified."
    return
    }

    & opencode mcp auth list
    

    }

    $clientId = $env:GOOGLE_DRIVE_MCP_CLIENT_ID
    $clientSecret = $env:GOOGLE_DRIVE_MCP_CLIENT_SECRET

    if (-not $clientId) {
    throw "Missing GOOGLE_DRIVE_MCP_CLIENT_ID environment variable."
    }
    if (-not $clientSecret) {
    throw "Missing GOOGLE_DRIVE_MCP_CLIENT_SECRET environment variable."
    }

    if (-not $SkipConfigUpdate) {
    Ensure-DriveMcpConfig -Path $ConfigPath -Name $ServerName -Url $ServerUrl -ScopeValue $Scope
    Write-Host "Updated OpenCode MCP config: $ConfigPath"
    }

    $redirectUri = "http://127.0.0.1:$CallbackPort$CallbackPath"
    $state = New-Base64UrlRandomString
    $codeVerifier = New-Base64UrlRandomString
    $codeChallenge = ConvertTo-Base64UrlSha256 -Value $codeVerifier

    $listener = [System.Net.HttpListener]::new()

    try {
    $listener.Prefixes.Add("http://127.0.0.1:$CallbackPort/")
    $listener.Start()

    $authUrl = "https://accounts.google.com/o/oauth2/v2/auth" +
        "?client_id=$(ConvertTo-UrlEncoded $clientId)" +
        "&redirect_uri=$(ConvertTo-UrlEncoded $redirectUri)" +
        "&response_type=code" +
        "&scope=$(ConvertTo-UrlEncoded $Scope)" +
        "&access_type=offline" +
        "&prompt=consent" +
        "&state=$(ConvertTo-UrlEncoded $state)" +
        "&code_challenge=$(ConvertTo-UrlEncoded $codeChallenge)" +
        "&code_challenge_method=S256"
    
    Start-Process $authUrl
    Write-Host "Opened browser for Google OAuth. Approve access, then wait for this script to finish."
    
    $deadline = [DateTime]::UtcNow.AddMinutes(5)
    $authorizationCode = $null
    $oauthError = $null
    
    while ([DateTime]::UtcNow -lt $deadline -and -not $authorizationCode -and -not $oauthError) {
        $remaining = [int]([Math]::Max(1000, ($deadline - [DateTime]::UtcNow).TotalMilliseconds))
        $async = $listener.BeginGetContext($null, $null)
    
        if (-not $async.AsyncWaitHandle.WaitOne($remaining)) {
            break
        }
    
        $context = $listener.EndGetContext($async)
        $request = $context.Request
        $query = $request.QueryString
    
        if ($query -and ($query["code"] -or $query["error"])) {
            if ($query["state"] -ne $state) {
                $context.Response.StatusCode = 400
                $body = "<html><body><h1>OAuth state mismatch</h1></body></html>"
                $buffer = [System.Text.Encoding]::UTF8.GetBytes($body)
                $context.Response.OutputStream.Write($buffer, 0, $buffer.Length)
                $context.Response.Close()
                throw "OAuth state mismatch."
            }
    
            $authorizationCode = $query["code"]
            $oauthError = $query["error"]
            $body = "<html><body><h1>Authentication received</h1><p>You can close this tab and return to OpenCode.</p></body></html>"
        }
        else {
            $body = "<html><body><p>Waiting for Google OAuth callback...</p></body></html>"
        }
    
        $responseBytes = [System.Text.Encoding]::UTF8.GetBytes($body)
        $context.Response.ContentType = "text/html"
        $context.Response.OutputStream.Write($responseBytes, 0, $responseBytes.Length)
        $context.Response.Close()
    }
    
    if ($oauthError) {
        throw "OAuth error: $oauthError"
    }
    if (-not $authorizationCode) {
        throw "Timed out waiting for OAuth authorization code."
    }
    
    $tokenBody = "code=$(ConvertTo-UrlEncoded $authorizationCode)" +
        "&client_id=$(ConvertTo-UrlEncoded $clientId)" +
        "&client_secret=$(ConvertTo-UrlEncoded $clientSecret)" +
        "&redirect_uri=$(ConvertTo-UrlEncoded $redirectUri)" +
        "&grant_type=authorization_code" +
        "&code_verifier=$(ConvertTo-UrlEncoded $codeVerifier)"
    
    $tokenResponse = Invoke-RestMethod -Method Post -Uri "https://oauth2.googleapis.com/token" -ContentType "application/x-www-form-urlencoded" -Body $tokenBody
    
    Update-McpAuthFile -Path $AuthPath -Name $ServerName -Url $ServerUrl -TokenResponse $tokenResponse
    Write-Host "Stored Drive OAuth tokens for OpenCode: $AuthPath"
    Invoke-OpenCodeAuthStatus
    Write-Host "Restart OpenCode so the active MCP client reloads the new token."
    

    }
    finally {
    if ($listener.IsListening) {
    $listener.Stop()
    }
    $listener.Close()
    }

  5. aloussase commented on Jun 23, 2026

    @aloussase

    Hi, is there any workaround?

    I am having the same issue.

    I am logged in to the GMAIL MCP but opencode always says it's unauthorized.

    This is the output of debug:

    ┌  MCP OAuth Debug
    │
    ●  Server: gmail
    │
    ●  URL: https://gmailmcp.googleapis.com/mcp/v1
    │
    ●  Auth status: ✗ not authenticated
    │
    ◇  HTTP response: 200 OK
    │
    ◆  Server responded successfully (no auth required or already authenticated)
    │
    ●  Server info: {"name":"StatelessServer","version":"ESF"}
    │
    └  Debug complete
    
  6. kebab-casee commented on Jul 27, 2026

    @kebab-casee

    Try composio

  7. EgilSandfeld commented on Aug 6, 2026

    @EgilSandfeld

    Still reproducible on OpenCode 1.18.10 on Windows 25H2, build 26200.8875.

    I tested Google's official hosted MCP servers:

    • Drive: https://drivemcp.googleapis.com/mcp/v1
    • Docs: https://docsmcp.googleapis.com/mcp/v1
    • Sheets: https://sheetsmcp.googleapis.com/mcp/v1

    Each uses a pre-registered Google Desktop OAuth client with explicit clientId, clientSecret, read-only scopes, and callback http://127.0.0.1:19876/mcp/oauth/callback.

    For all three:

    1. opencode mcp auth <server> immediately reports Authentication successful!
    2. No browser opens and no Google consent flow starts.
    3. opencode mcp auth list reports not authenticated.
    4. mcp-auth.json contains an entry for each server, but no token or server URL is stored.
    5. opencode mcp list still reports the servers as connected because the unauthenticated MCP handshake returns HTTP 200.

    This confirms the issue remains present in 1.18.10 and is not Drive-specific; Docs and Sheets exhibit the same false-success behavior.

  8. dr-kristau commented on Aug 6, 2026

    @dr-kristau

    This is also a problem with Google GMail access. The following bug report was automatically created:

    Bug: opencode mcp auth gmail OAuth flow succeeds but token is not persisted

    Environment

    • opencode version: 1.18.14
    • OS: Linux
    • MCP server: gmail (remote, Google MCP — https://gmailmcp.googleapis.com/mcp/v1)

    Config (~/.config/opencode/opencode.json)

    "gmail": {
        "enabled": true,
        "type": "remote",
        "url": "https://gmailmcp.googleapis.com/mcp/v1",
        "oauth": {
          "clientId": "XXXX",
          "clientSecret": "XXXX",
          "scope": "https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.compose"
        }
    }

    Steps to reproduce

    1. Run opencode mcp debug gmail — shows Auth status: ✗ not authenticated
    2. Run opencode mcp auth gmail — reports Authentication successful! and Done
    3. Run opencode mcp debug gmail again — still shows Auth status: ✗ not authenticated
    4. Any Gmail tool call (e.g. gmail_search_threads) returns Unauthorized
    5. No OAuth tokens or credentials files appear in ~/.config/opencode/

    Expected behavior

    After opencode mcp auth gmail reports success, the OAuth token should be persisted so that:

    • opencode mcp debug gmail reports Auth status: ✓ authenticated
    • Gmail MCP tool calls succeed (e.g. gmail_search_threads returns data, not Unauthorized)

    Actual behavior

    The OAuth flow completes successfully on the browser side, and opencode prints Authentication successful!, but no token is persisted. The auth state remains not authenticated and all tool calls are rejected with Unauthorized.

    Additional notes

    • The server identifies as StatelessServer (debug output: {"name":"StatelessServer","version":"ESF"}), so it requires an auth token on every request — there is no session-level auth handshake.
    • The connection itself succeeds (HTTP 200 OK from https://gmailmcp.googleapis.com/mcp/v1).
    • Other remote MCP servers (e.g. mcp-atlassian) work correctly, so the issue is specific to the OAuth flow persistence.
  9. jonhydetitanbay commented on Sep 10, 2026

    @jonhydetitanbay

    Reproduced with Gmail on macOS

    Environment

    • OpenCode CLI: 1.18.25, installed globally from the opencode-ai npm package
    • OpenCode beta runtime: 0.0.0-beta-19192
    • OS: Darwin 25.5.0, arm64
    • Terminal: xterm-256color
    • Shell: /bin/zsh
    • Active plugins: none found
    • MCP server: https://gmailmcp.googleapis.com/mcp/v1

    Reproduction

    1. Configure the Gmail remote MCP server. In this case the server has no explicit OAuth client ID or secret, so OpenCode attempts dynamic client registration.
    2. Run opencode mcp auth gmail.
    3. OpenCode prints Authentication successful!, although no Google consent page is completed and the Gmail entry in ~/.local/share/opencode/mcp-auth.json is empty.
    4. Run opencode mcp auth list; Gmail remains not authenticated.
    5. Call any Gmail tool, including list_labels or get_thread.

    Actual behaviour

    Every Gmail tool call fails consistently with:

    Incompatible auth server: does not support dynamic client registration
    

    The MCP extensions screen shows only an enable/disable toggle, so it offers no route to enter the pre-registered OAuth client credentials that Google's hosted MCP servers require.

    Google Calendar and Google Drive show the same unauthenticated state. Other remote MCP servers, including Notion, authenticate and execute correctly.

    Expected behaviour

    OpenCode should not report authentication success when no token was obtained or persisted. For an authorization server that does not support dynamic client registration, it should give an actionable error before claiming success, such as requiring oauth.client_id and oauth.client_secret and pointing the user to configuration.

    Google documents that its hosted Workspace MCP servers do not support dynamic client registration, so the current success message leaves the user unable to distinguish a missing static OAuth client configuration from a completed login.

  10. jethac commented on Oct 6, 2026

    @jethac

    Hit this today setting up Google's official MCP connectors — the first-party Gmail, Drive, and Calendar servers at gmailmcp/drivemcp/calendarmcp.googleapis.com — using an OAuth client created in Google Cloud exactly as Google's own MCP setup docs instruct. So anyone following the official path lands on this bug. The mechanism, for whoever picks it up:

    Google's MCP endpoints accept initialize — and ping and tools/list — without credentials (HTTP 200). Only tools/call returns 401, with a proper WWW-Authenticate: Bearer resource_metadata=... challenge. Since opencode only triggers the OAuth flow on a connect-time 401, startAuth()'s client.connect() succeeds, commit() stores nothing, and the CLI reports "Authentication successful!" — while every tool call dead-ends as Unauthorized.

    Repro (no opencode needed):

    curl -si -X POST https://gmailmcp.googleapis.com/mcp/v1 -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'
    # → 200
    # same request with method "ping" or "tools/list" → 200
    # same request with method "tools/call" → 401 + WWW-Authenticate resource_metadata
    

    I've opened #53468 with a fix: persist the discovery state from 401 challenges via the SDK's discoveryState/saveDiscoveryState hooks, explicitly start the flow in startAuth() when there's evidence OAuth is required, flip request-time 401s to needs_auth, and add an authorizationServerUrl config escape hatch. Approach follows the one proposed in #26912.

  11. 0xuwin commented on Oct 9, 2026

    @0xuwin

    Sharing a Python script that runs the OAuth PKCE flow directly against Google's standard endpoints and prints an entry in mcp-auth.json format. Note that you should add http://127.0.0.1:19876/mcp/oauth/callback (opencode's default callback, which the script listens on) to the Authorized redirect URIs of your Google OAuth client, otherwise Google rejects with redirect_uri_mismatch.

    #!/usr/bin/env python3
    """Google MCP OAuth helper for opencode.
    
    Usage:
      GOOGLE_MCP_CLIENT_ID=xxx GOOGLE_MCP_CLIENT_SECRET=xxx \
      python3 google_mcp_oauth.py --name drive --url https://drivemcp.googleapis.com/mcp/v1 \
          --scope https://www.googleapis.com/auth/drive.readonly
    """
    
    import argparse
    import base64
    import hashlib
    import json
    import os
    import secrets
    import threading
    import time
    import urllib.parse
    import urllib.request
    from http.server import BaseHTTPRequestHandler, HTTPServer
    
    AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth"
    TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
    CALLBACK_PATH = "/mcp/oauth/callback"
    
    
    def b64url(data: bytes) -> str:
        return base64.urlsafe_b64encode(data).decode().rstrip("=")
    
    
    class CallbackHandler(BaseHTTPRequestHandler):
        result = {}
    
        def do_GET(self):
            parsed = urllib.parse.urlparse(self.path)
            if parsed.path != CALLBACK_PATH:
                self.send_error(404)
                return
            CallbackHandler.result = {k: v[0] for k, v in urllib.parse.parse_qs(parsed.query).items()}
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.end_headers()
            self.wfile.write(b"<html><body><h1>OK</h1><p>You can close this tab.</p></body></html>")
    
        def log_message(self, *args):
            pass
    
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument("--name", required=True, help="MCP server name in opencode.json")
        ap.add_argument("--url", required=True, help="MCP server URL")
        ap.add_argument("--scope", action="append", required=True)
        ap.add_argument("--port", type=int, default=19876)
        args = ap.parse_args()
    
        client_id = os.environ.get("GOOGLE_MCP_CLIENT_ID")
        client_secret = os.environ.get("GOOGLE_MCP_CLIENT_SECRET")
        if not client_id or not client_secret:
            raise SystemExit("Set GOOGLE_MCP_CLIENT_ID and GOOGLE_MCP_CLIENT_SECRET")
    
        redirect_uri = f"http://127.0.0.1:{args.port}{CALLBACK_PATH}"
        scope = " ".join(args.scope)
        state = b64url(secrets.token_bytes(32))
        verifier = b64url(secrets.token_bytes(64))
        challenge = b64url(hashlib.sha256(verifier.encode()).digest())
    
        server = HTTPServer(("127.0.0.1", args.port), CallbackHandler)
        print(f"Callback server listening on {redirect_uri}")
    
        auth_url = AUTH_ENDPOINT + "?" + urllib.parse.urlencode({
            "client_id": client_id,
            "redirect_uri": redirect_uri,
            "response_type": "code",
            "scope": scope,
            "access_type": "offline",
            "prompt": "consent",
            "state": state,
            "code_challenge": challenge,
            "code_challenge_method": "S256",
        })
        print(f"Open this URL in your browser if it doesn't open automatically:\n{auth_url}\n")
        try:
            import webbrowser
            webbrowser.open(auth_url)
        except Exception:
            pass
    
        t = threading.Thread(target=server.handle_request)
        t.start()
        t.join(timeout=300)
        server.server_close()
    
        res = CallbackHandler.result
        if not res:
            raise SystemExit("Timed out waiting for OAuth callback")
        if "error" in res:
            raise SystemExit(f"OAuth error: {res['error']}")
        if res.get("state") != state:
            raise SystemExit("State mismatch")
        if "code" not in res:
            raise SystemExit(f"No code in callback: {res}")
    
        body = urllib.parse.urlencode({
            "code": res["code"],
            "client_id": client_id,
            "client_secret": client_secret,
            "redirect_uri": redirect_uri,
            "grant_type": "authorization_code",
            "code_verifier": verifier,
        }).encode()
        req = urllib.request.Request(TOKEN_ENDPOINT, data=body,
                                     headers={"Content-Type": "application/x-www-form-urlencoded"})
        with urllib.request.urlopen(req) as resp:
            tok = json.loads(resp.read())
    
        tokens = {
            "accessToken": tok["access_token"],
            "expiresAt": int(time.time()) + tok.get("expires_in", 3600),
            "scope": tok.get("scope", scope),
        }
        if "refresh_token" in tok:
            tokens["refreshToken"] = tok["refresh_token"]
        else:
            print("Warning: no refresh_token returned. Revoke app access in your Google "
                  "account and rerun to get one.")
    
        print("\nPaste this entry into ~/.local/share/opencode/mcp-auth.json "
              f'(key "{args.name}"), then restart opencode:\n')
        print(json.dumps({args.name: {"tokens": tokens, "serverUrl": args.url}}, indent=2))
    
    
    if __name__ == "__main__":
        main()
  12. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

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

Metadata

Metadata

Assignees

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