0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code用にChrome Bridge v2を自作した話——複数プロファイル対応で生産性が変わる

0
Last updated at Posted at 2026-04-26

TL;DR

Claude Codeから複数Chromeプロファイルを同時操作したい開発者向けに、自作Chrome Bridge v2の設計と実装手順をまとめました。3層構造(拡張 → WebSocket → MCPサーバー)でプロファイル分離を実現し、安定した接続と30個の実用ツールを提供します。所要時間:約20分。


背景:既存OSSの限界

既存のchrome-mcp-bridgeは以下の欠陥を抱えていました。

  • 単一WebSocket接続:複数プロファイルからの同時接続が競合し、新しい接続が古い接続を上書き
  • プロファイル切り替え機能なし:1接続 = 1プロファイルに固定
  • 30秒でタイムアウト:offscreen APIのBLOBS reasonが安定しない
  • ツール数が限定的:スナップショット・コンソール・ネットワークモニタリング・ファイルアップロードなど、実務的なツールが不足(12個程度)
  • 拡張性が低い:アーキテクチャが固く、新機能追加が困難

これらの問題を根本的に解決するため、Chrome Bridge v2を自作することにしました。


3層構造の設計

v2は以下の3層で構成されています。

Chrome Profile 1 (拡張)
        ↓ WS接続 (プロファイルメタ付き)
Chrome Profile 2 (拡張) ─→ WebSocket Multiplexer → MCP Server
        ↓                    (Port 9239)
Chrome Profile 3 (拡張)

層1:Chrome拡張(フロントエンド)

  • 各Chromeプロファイルにロードされる
  • プロファイル情報を環境変数から読み込み、接続時にメタデータとして送信

層2:WebSocket(通信層)

  • 複数接続を許容し、メッセージにプロファイル識別子を含める
  • メッセージキューで順序を保証

層3:MCPサーバー(バックエンド)

  • プロファイル識別子を読み、正しいDevToolsプロセスに命令を振り分け
  • 30個のツールを統一フォーマットで実装

実装手順

ステップ1:拡張のmanifest.jsonとbackground.js

{
  "manifest_version": 3,
  "permissions": ["scripting", "tabs", "offscreen"],
  "background": {
    "service_worker": "background.js"
  }
}
// background.js
const PROFILE_EMAIL = process.env.PROFILE_EMAIL || "unknown";
const PROFILE_ALIAS = process.env.PROFILE_ALIAS || "Default";

let ws = null;

function connectWebSocket() {
  ws = new WebSocket("ws://localhost:9239");
  
  ws.onopen = () => {
    console.log(`Connected from profile: ${PROFILE_ALIAS}`);
    ws.send(JSON.stringify({
      type: "init",
      email: PROFILE_EMAIL,
      alias: PROFILE_ALIAS,
      timestamp: Date.now()
    }));
  };

  ws.onmessage = (event) => {
    handleCommand(JSON.parse(event.data));
  };

  ws.onerror = () => {
    console.error("WebSocket error, retrying...");
    setTimeout(connectWebSocket, 2000);
  };

  ws.onclose = () => {
    console.log("WebSocket closed, reconnecting...");
    exponentialBackoff();
  };
}

function exponentialBackoff(attempt = 0) {
  const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
  setTimeout(connectWebSocket, delay);
}

connectWebSocket();

// Ping/Pongハンドシェイク(30秒ごと)
setInterval(() => {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "ping" }));
  }
}, 30000);

ステップ2:プロファイル識別の仕組み

複数アカウント運用時は、メールアドレスだけでは不十分です。email + aliasで一意性を確保します。

// プロファイル初期化時
function initializeProfile() {
  const profilePath = chrome.runtime.getManifest().name; // または環境変数から読み込み
  const alias = extractAliasFromPath(profilePath);
  
  // ローカルストレージに保存
  chrome.storage.local.set({
    profileEmail: PROFILE_EMAIL,
    profileAlias: alias,
    connectedAt: Date.now()
  });

  return { email: PROFILE_EMAIL, alias };
}

function extractAliasFromPath(path) {
  // Windows: C:\Users\ishid\AppData\Local\Google\Chrome\User Data\Profile 1
  // macOS: ~/Library/Application Support/Google/Chrome/Profile 1
  return path.match(/Profile (\d+)$/)?.[1] || "Default";
}

ステップ3:MCPサーバーのプロファイル振り分け

// MCP Server side
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 9239 });

const profileConnections = new Map(); // profileId -> { ws, devtools, queue }

wss.on("connection", (ws) => {
  let profileId = null;

  ws.on("message", (data) => {
    const msg = JSON.parse(data);

    if (msg.type === "init") {
      profileId = `${msg.email}::${msg.alias}`;
      profileConnections.set(profileId, {
        ws,
        queue: [],
        lastPing: Date.now()
      });
      console.log(`Profile connected: ${profileId}`);
    } else if (msg.type === "ping") {
      profileConnections.get(profileId).lastPing = Date.now();
      ws.send(JSON.stringify({ type: "pong" }));
    } else {
      // コマンド処理
      executeCommand(profileId, msg);
    }
  });

  ws.on("close", () => {
    if (profileId) {
      profileConnections.delete(profileId);
      console.log(`Profile disconnected: ${profileId}`);
    }
  });
});

function executeCommand(profileId, cmd) {
  const profile = profileConnections.get(profileId);
  if (!profile) return;

  // メッセージキューで順序を保証
  profile.queue.push(cmd);
  processQueue(profileId);
}

function processQueue(profileId) {
  const profile = profileConnections.get(profileId);
  if (!profile || profile.processing || profile.queue.length === 0) return;

  profile.processing = true;
  const cmd = profile.queue.shift();

  routeCommand(profileId, cmd).then(() => {
    profile.processing = false;
    processQueue(profileId);
  });
}

ツール実装パターン

統一フォーマットで設計したため、新しいツール追加は単純作業です。

// ツール定義(共通フォーマット)
const tools = {
  snapshot: {
    name: "snapshot",
    handler: async (profileId, params) => {
      const session = getDevToolsSession(profileId);
      const result = await session.send("Page.captureScreenshot");
      return { data: result.data };
    }
  },
  
  console: {
    name: "console",
    handler: async (profileId, params) => {
      const session = getDevToolsSession(profileId);
      const logs = await session.send("Console.getMessages");
      return { messages: logs };
    }
  },

  network: {
    name: "network",
    handler: async (profileId, params) => {
      const session = getDevToolsSession(profileId);
      const requests = await session.send("Network.getAllRequests");
      return { requests };
    }
  },

  // 新しいツール追加は同じ構造で
  gifCapture: {
    name: "gifCapture",
    handler: async (profileId, params) => {
      const session = getDevToolsSession(profileId);
      // gifキャプチャのロジック
      return { gifData: "..." };
    }
  }
};

async function routeCommand(profileId, cmd) {
  const tool = tools[cmd.tool];
  if (!tool) throw new Error(`Unknown tool: ${cmd.tool}`);
  return tool.handler(profileId, cmd.params);
}

つまづきポイント

接続の安定性

問題:offscreen APIのBLOBS reasonが、ドキュメント未記載の理由で突然終了する

解決:WORKERS reasonに切り替え。サービスワーカーはより長寿命です。

// 避けるべき
chrome.offscreen.createDocument({
  url: "offscreen.html",
  reasons: ["BLOBS"]  // ❌ 不安定
});

// 推奨
// background.jsをサービスワーカーとして直接実行
// manifest_version 3では、background.service_worker が標準

プロファイル識別の一意性

問題:同じユーザーが複数プロファイルを持つ場合、メールアドレスだけでは区別できない

解決email + aliasを組み合わせ、プロファイルディレクトリ名を識別子として使用

// ❌ 不十分
const profileId = email; // 複数プロファイルで衝突

// ✅ 正解
const profileId = `${email}::${alias}`; // Profile 1, Profile 2と区別可能

メッセージの順序保証

複数接続からのコマンドが並行到着する場合、順序が乱れると状態不整合が発生します。

解決:各プロファイルに専用キューを持たせ、順序を保証


まとめ

自作Chrome Bridge v2は、複数Chromeプロファイル運用の課題を3層構造で根本的に解決しました。

アーキテクチャレベルで判断すべき:既存OSSを無理やり拡張するか、自作するか。単一接続を前提に設計されたシステムに複数接続機能を後付けするより、最初から複数接続を設計に含める方が、実装も簡潔で拡張も容易です。

複数のChromeプロファイルを運用している開発者や、Claude Code等のAIツールで自動化スクリプトを走らせながら別アカウントを操作したい方にとって、この設計思想は参考になるはずです。


さらに詳しい実装手順はnoteで公開中

この記事では概要のみ紹介しました。実装の完全手順・プロンプト全文・運用ノウハウ(本番環境での細かい落とし穴、30個ツールの詳細実装)は以下のnoteで公開しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?