Skip to content

fix(oauth): Windows browser auto-open drops URL query after & character #211

@liplus-lin-lay

Description

@liplus-lin-lay

目的

v0.11.1 の Windows 版 openBrowser() が、URL に含まれる & 文字を cmd.exe が command separator として解釈するために、authorize URL のクエリパラメータ(state など)を途中で切り落とした状態でブラウザに渡している。結果、ユーザが browser 自動オープンから到達する /oauth/authorize リクエストは state を失い、Worker は invalid_request: state parameter is required (min 8 chars) で弾く。手動で URL 全文をコピペすると通るが、v0.11.1 が謳う「ブラウザ自動オープン」が Windows 上で機能していない状態。

実機検証 2026-04-20(Master)で再現確認済み。

前提(検証済)

再現手順

  1. Windows(Claude Desktop / Claude Code CLI)で v0.11.1 mcpb を使用
  2. 未認証状態で webhook tool を呼び出す
  3. performOAuthFlow() が authorize URL を生成し openBrowser() を呼ぶ
  4. ブラウザのアドレスバーに ?client_id=... までが届き、&state=... 以降が落ちる
  5. Worker /oauth/authorize handler が state 欠落で 400 JSON エラー

根本原因(コード)

mcp-server/server/index.js の現行実装(local-mcp/src/index.ts も同構造):

if (plat === "win32") {
  command = "cmd.exe";
  args = ["/c", "start", "", url];  // ← url に & が入ると cmd が分断
}
  • cmd.exe /c start "" <URL> の形で渡しているが、cmd.exe は argv を再組み立てして GetCommandLine ベースで解釈するため & が command separator として働く
  • start "" https://.../authorize?client_id=X までがブラウザ起動に使われ、&state=Y 以降は別コマンドとして評価(通常は失敗)

類似症状の past history

  • v0.11.0 実機検証時に Master が観察した「?user_code=...?skip_account_picker=true に変わる」現象も、このバグ由来の可能性が高い(GitHub 側リダイレクトと重なって隠れていた)
  • v0.10.x 系の openBrowser() も同じ構造だったため、web OAuth flow + authorization_code で動いていた時代から潜在バグが存在していた。ただし authorization_code flow では redirect_uri に localhost が使われるため影響が違う形で出ていた可能性がある

制約

  • v0.11.2 は patch リリース(v0.11.1 の hotfix)
  • 挙動変更なし(クエリ付き URL を「正しく」Windows ブラウザに渡すだけ)
  • macOS (open) と Linux (xdg-open) は & を普通の引数として受け取るので現行実装で正しく動く(修正対象は Windows のみ)
  • ネイティブ依存を増やさない(Node.js 標準 node:child_process のみで解決)
  • mcp-server と local-mcp の両方を揃える

実装方針

推奨アプローチ: URL 全体をダブルクォートで括る

Windows では cmd.exe は "..." で括った文字列内の & をリテラル扱いする(command separator として解釈しない)。Node.js spawn は Windows で cmd.exe にコマンドラインを渡す際、各 arg を必要に応じて quote するが、URL はデフォルトで quote されない(& は quote 必要文字として Node 側に認識されていないため)。

明示的に URL を " で囲って渡す:

if (plat === "win32") {
  command = "cmd.exe";
  args = ["/c", "start", "", `"${url}"`];
}

ただし Node.js の Windows spawn は args 内の既存 " を自動エスケープするため、この書き方が素直に働くか要検証。動かない場合は shell: true を使って start "" "URL" を文字列としてコマンド渡し するのが確実:

if (plat === "win32") {
  command = `start "" "${url}"`;
  options.shell = true;
}

代替アプローチ: PowerShell 経由で Start-Process

if (plat === "win32") {
  command = "powershell.exe";
  args = ["-NoProfile", "-Command", "Start-Process", url];
}

PowerShell は argv を直接扱うので & エスケープ問題を回避できる。ただし PowerShell 起動オーバヘッドが cmd より大きい。

推奨順序

  1. まず shell: true + quoted URL を試す(最小の差分)
  2. 動かない場合は PowerShell フォールバック

既存コメント("start is a cmd.exe builtin, not a standalone executable. The empty "" argument is the window title placeholder expected by start when the URL is quoted.")に URL 自身の quote も明示する形でコメント更新。

テスト追加

mcp-server/test/ に unit-level テストを追加:

  • URL に & を含むケースで openBrowser が想定の引数列を組むこと(spawn をモック)
  • platform 別(win32 / darwin / linux)で分岐が正しいこと

実機テストは Master の Windows 環境で Master が行う。

target files

  • mcp-server/server/index.jsopenBrowser() Windows 分岐
  • local-mcp/src/index.ts — 同上(TypeScript)
  • mcp-server/test/open-browser.test.mjs — 新規テスト
  • docs/installation.ja.md / docs/installation.md — v0.11.2 の変更点として短い note を追加

version bump:

  • mcp-server/manifest.json 0.11.1 → 0.11.2
  • mcp-server/package.json 0.11.1 → 0.11.2
  • mcp-server/server.json 0.11.1 → 0.11.2(2 箇所)

関連

Metadata

Metadata

Labels

bugSomething isn't workingreadybody converged for implementation

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions