Skip to content

mcp: ?codemode=false is appended to remote MCP URLs; Google's Workspace MCP servers return 404 for it #55336

Closed

Description

@jethac

Description

For every remote MCP server without codemode: false, opencode adds ?codemode=false to the configured URL before it connects. Google's hosted Workspace MCP servers (gmailmcp.googleapis.com, drivemcp.googleapis.com, calendarmcp.googleapis.com) return HTTP 404 for any URL with that query string. On /mcp/v1, the 404 is Google's HTML error page.

The connect path catches 400/404 and retries the original URL, so the servers do connect. But:

  • every connect and reconnect makes an extra failing request first, and logs a large mcp transport error with the full HTML 404 page for each Google server;
  • the fallback covers only 400 and 404. A server that answers an unknown query string differently, for example with 401/403 or a redirect, gets the modified URL with no fallback;
  • the URL the user configured is not the URL opencode calls. This is surprising and is not in the MCP docs;
  • the only opt-out is codemode: false, which changes how the server's tools are exposed. MCP codemode: false is ignored: server tools stay behind execute (2.0.25–2.0.28, opencode acp) #55079 reports that codemode: false is ignored in some paths.

Steps to reproduce

  1. Configure a remote server: "google-gmail": { "type": "remote", "url": "https://gmailmcp.googleapis.com/mcp/v1", "oauth": { ... } }
  2. Start opencode and check ~/.local/share/opencode/log/opencode.log:
    message="mcp transport error" server=google-gmail error="Error POSTing to endpoint: <!DOCTYPE html> … The requested URL <code>/mcp/v1?codemode=false</code> was not found on this server. … (HTTP 404)"
    
  3. Direct check with a valid token: POST https://gmailmcp.googleapis.com/mcp/v1 (initialize) returns 200, and POST https://gmailmcp.googleapis.com/mcp/v1?codemode=false returns 404.

Expected

Do not change user-configured URLs. If servers need the Code Mode hint, send it in a header, which servers ignore when they do not know it, or as an MCP capability. At minimum, do not log the full HTML body for the expected fallback.

OpenCode version

2.0.28 (CLI, @opencode/cli), macOS (Apple Silicon)

Related

Activity

  1. opencode-agent commented on Oct 14, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this with a local mock MCP server standing in for Google's: it serves /mcp/v1 normally and rejects any URL with a query string.

    • Mock returns 404: opencode's first request is POST /mcp/v1?codemode=false. The log gets a mcp transport error with the whole HTML 404 page, then opencode retries the configured /mcp/v1 and connects.
    • Mock returns 403: only POST /mcp/v1?codemode=false is sent and the connection fails (mcp connect failed ... HTTP 403). The configured URL is never tried.

    This happens on 2.0.28 and still happens on the latest dev (3a69bae). The script below reproduces both cases.

    Related:

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #55336: opencode rewrites remote MCP URLs to add ?codemode=false.
    # A mock Streamable HTTP MCP server stands in for Google's Workspace MCP servers:
    # it serves MCP normally at /mcp/v1 but answers any request with a query string
    # with an HTML 404 (MODE=404) or a 403 (MODE=403).
    # Usage: OPENCODE="opencode" ./repro.sh   (or OPENCODE="bun ... packages/cli/src/index.ts")
    set -u
    OPENCODE=${OPENCODE:-opencode}
    DIR=$(mktemp -d /tmp/opencode/repro.XXXX)
    cat > "$DIR/server.ts" <<'EOF'
    const mode = process.env.MODE ?? "404"
    Bun.serve({
      port: Number(process.env.PORT),
      async fetch(req) {
        const url = new URL(req.url)
        console.error(`[mock] ${req.method} ${url.pathname}${url.search}`)
        if (url.search) {
          if (mode === "403") return new Response("forbidden", { status: 403 })
          return new Response(`<!DOCTYPE html><html><body><p>The requested URL <code>${url.pathname}${url.search}</code> was not found on this server.</p>${"<!-- padding -->".repeat(50)}</body></html>`, { status: 404, headers: { "content-type": "text/html" } })
        }
        if (req.method !== "POST") return new Response(null, { status: 405 })
        const msg = await req.json()
        if (msg.id === undefined) return new Response(null, { status: 202 })
        const result =
          msg.method === "initialize"
            ? { protocolVersion: msg.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "mock", version: "1" } }
            : msg.method === "tools/list" ? { tools: [{ name: "ping", inputSchema: { type: "object" } }] } : {}
        return Response.json({ jsonrpc: "2.0", id: msg.id, result })
      },
    })
    EOF
    LOGDIR=$DIR/data/opencode/log
    run() {
      local mode=$1 port=$2
      echo "=== mock answers query-string URLs with HTTP $mode ==="
      MODE=$mode PORT=$port bun "$DIR/server.ts" > "$DIR/server-$mode.log" 2>&1 &
      local pid=$!
      sleep 1
      mkdir -p "$DIR/proj-$mode"
      echo "{ \"mcp\": { \"servers\": { \"google-gmail\": { \"type\": \"remote\", \"url\": \"http://127.0.0.1:$port/mcp/v1\" } } } }" > "$DIR/proj-$mode/opencode.json"
      # Running from source: bun's re-spawn drops --conditions/--preload, so start the service ourselves.
      if [ -n "${START_SERVICE:-}" ]; then
        (cd "$DIR/proj-$mode" && XDG_DATA_HOME=$DIR/data $OPENCODE serve --service > "$DIR/service-$mode.log" 2>&1 &)
        sleep 8
      fi
      (cd "$DIR/proj-$mode" && XDG_DATA_HOME=$DIR/data timeout 60 $OPENCODE mcp list 2>&1)
      sleep 3
      kill $pid
      echo "--- requests seen by the mock server:"
      cat "$DIR/server-$mode.log"
      echo "--- opencode log lines for the server:"
      grep -h "server=google-gmail" "$LOGDIR"/*.log | grep "proj-$mode\|codemode\|mcp" | grep -v "^$" | cut -c1-300 | tail -n 6
      for p in $(pgrep -f '[s]erve --service'); do [ "$p" != "$$" ] && kill $p 2>/dev/null; done; true
      sleep 1
    }
    for p in $(pgrep -f '[s]erve --service'); do [ "$p" != "$$" ] && kill $p 2>/dev/null; done; true
    run 404 18414
    run 403 18413

    Run it: bash repro.sh

  2. jethac commented on Oct 15, 2026

    @jethac
    Author

    The triage comment above shows the 403 failure. When the server sends 403 for the ?codemode=false request, opencode does not retry the configured URL, and the connection fails. When the server sends 400 or 404, opencode retries the configured URL and connects.

    Two fixes are possible:

    1. Add 403 to the retry statuses for the configured URL.
    2. Keep the configured URL unchanged. Send the Code Mode hint in a different way, for example in a request header. Servers ignore unknown headers.

    The test "does not retry MCP beyond the query fallback" in packages/core/test/mcp.test.ts sets 403 to one attempt. Thus, the current behavior can be intentional. Which fix is preferred? A PR can follow the decision.

  3. rekram1-node commented on Oct 15, 2026

    @rekram1-node
    Collaborator

    Yeah we tested quite a few mcp servers (30+ of major ones) and didn't find any that had issues w/ the extra param. We send that param because some servers actually respect it, it's not quite a standard yet tho, so for now for best experience (unless we can show that majority of major servers actually reject this param) we will do explicit blacklisting to prevent it from being sent and wasting time. So for now I think not sending it to servers w/ .google.apis is sufficient.

    Ofc you can completely prevent it by adding codemode: false to your mcp server config (which DOESNT send the parameter) it makes it so the mcp server tools are available as is. The codemode=false query param is to tell mcp servers DO NOT implement codemode on your end, let the client own codemode instead.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions