Madogiwa Blog

主に技術系の学習メモに使っていきます。

vite_rubyからrails_viteへ移行したのでハマりどころとかもメモ📝

はじめに

個人サービスでVue.js 3 + TypeScriptのフロントエンドをVite経由でビルドしており、RailsとViteの統合にはvite_railsvite_rubyベース)を使っていましたが、rails_vite(README曰く"No proxy, no config duplication, no magic")へ移行したので、その時のハマりどころ等をメモ📝

なお、rails_vite作者本人が書いたEvil Martiansの解説記事によると、rails_viteにはapp/assets/builds/にスタブファイルを置いてPropshaftと共存する「jsbundling mode」と、rails_vite gem自体をインストールしてvite_tagsヘルパーを使う「gem mode」の2パターンがありますが、今回は後者のgem modeを採用しています。

本題

vite_railsとrails_viteの設計思想の違い

移行にあたって最初に整理したのが、両者の設計思想の違いです。

主な違いは以下の3点です。

観点 vite_rails rails_vite
開発サーバー接続 Rackプロキシ経由(同一オリジン) プロキシなし(ブラウザが直接devサーバーへ接続、別オリジン)
設定ファイル config/vite.json(Ruby側)+ vite.config.ts(JS側)の二重管理 vite.config.tsのみ。Ruby側は起動時に書き出されるtmp/rails-vite.jsonを読んで自動同期
依存gem rack-proxy / dry-cli / mutex_mなど railtiesのみ

設定ファイルの二重管理がなくなる点は、地味ですがメンテナンスコストの削減として大きいポイントでした。

移行作業自体はGemfileの入れ替えとvite.config.tsの設定移植が中心で、ローカルでの動作確認では大きな問題は起きませんでした。ただ、View側とCSP周りで2つほど設計差分に起因する対応が必要になりました。

1つ目はViewヘルパーの実装方式の違いです。vite_railsViteRails::TagHelpersはRails標準のjavascript_include_tag/stylesheet_link_tagAssetTagHelper)に処理を委譲していましたが、rails_viteRailsVite::TagHelpertag.script/tag.linkを自前で組み立てる実装になっています。そのため、Rails標準のnonce: trueという糖衣構文(AssetTagHelper内部でcontent_security_policy_nonceを自動解決する仕組み)が効かなくなりました。

先述のEvil Martiansの記事では「gem modeはCSP nonce対応がある」と書かれていて、実際vite_tagsはnonceの値を渡せば正しく<script nonce="...">を出力してくれます。ただしそれは「nonceの値を明示的に渡せば効く」という意味であって、Railsのnonce: trueAssetTagHelperが内部でcontent_security_policy_nonceを自動解決してくれる糖衣構文)がそのまま動くわけではありませんでした。このニュアンスの違いに気づかず最初はnonce: trueのまま動かしてハマったので、地味に注意が必要なポイントです。呼び出し側の記法を変えたくなかったので、自前のVitePackHelperラッパー内でnonce: trueを検知して実際のnonce値に変換する形で吸収しています(詳細は実装例セクション参照)。

2つ目はCSPです。プロキシがなくなり、開発環境ではViteのdevサーバーが別オリジンになったことで、devサーバーから配信される画像がCSPのimg-srcでブロックされるようになりました。RailsVite.dev_server_csp_sourceというdevサーバーのオリジンを動的に解決するヘルパーが用意されているので、それを使ってimg-srcに追加することで解消しました。

ここまではローカルでの検証で気づけた範囲でしたが、実際にCIを回してみると想定していなかった問題が2つ見つかりました。

CIで発覚した想定外の問題

1. db:test:prepareがNode未セットアップのジョブで失敗する

rails_vitedb:test:prepareにRakeタスクの前提条件としてvite:build(さらにその前提としてvite:install = pnpm install)を自動フックする仕組みを持っています。Dogfeedsでは「system spec以外を高速に回す」目的で、あえてNode/pnpmをセットアップしていないCIジョブがあり、そのジョブでpnpm installが見つからずdb:test:prepare自体が失敗しました。

調べたところSKIP_VITE_BUILD=1という環境変数でこのRakeタスクのフックを丸ごとスキップできることがわかり、それで解決しました。このジョブのrequest specはもともとskip_assetsという共有RSpecコンテキストでVitePackHelperをスタブし、ビルド済みアセットに依存しない作りになっていたため、アセットビルド自体を省略しても実害はありませんでした。

vite:build hooks into assets:precompile and test:prepare automatically. Skip with SKIP_VITE_BUILD=1. https://github.com/skryukov/rails_vite#rake-tasks

2. VRTで11ページ分の見た目が丸ごと崩れる

ここが今回のメインの発見です。Visual Regression Test(VRT)を回したところ、「AIチャット一覧」「フィード詳細」など11ページで大きな差分が検出されました。差分画像を見ると、CSSがほぼ全く効いていないような崩れ方で、Tailwindのflexレイアウトが効かず全要素が縦積み・全幅になっていました。

原因は2段階ありました。

原因1(小さい方): rails_viteが内部で利用しているrails-vite-pluginが、build.assetsInlineLimitを未指定時に0で強制する実装になっていました。これにより、4KB未満のfavicon.ico(Viteのデフォルト閾値4096バイトの内側にあり、以前はbase64インライン化されていた)が別ファイル読み込みになり、全ページ共通のNavigationBarコンポーネントの描画タイミングが変わってVRTの差分要因の一部になっていました。assetsInlineLimit: 4096(Viteのデフォルト値)を明示することで解消しました。

assetsInlineLimit: userConfig.build?.assetsInlineLimit ?? 0,

https://github.com/skryukov/rails_vite/blob/1736eff10e29775711e8003defab947178c3285d/packages/rails-vite-plugin/src/index.ts#L85

原因2(本命): assetsInlineLimitを直しても差分が残るページがあったため、with_playwright_page(capybara-playwright-driver経由で生のPlaywright Pageオブジェクトを取得できる仕組み)を使って、ブラウザのconsoleログ・レスポンスステータス・DOM要素のgetComputedStyle()を直接調べるデバッグ用の一時specを書いて調査しました。その結果、CSSファイル自体は200 OKで正しく配信されているのに、TemplateBase.vue(全ページ共通のレイアウトラッパー、<style scoped>でflexboxレイアウトを定義)のdisplay: flexが全く適用されていないことがわかりました。

manifest.jsonを直接見ると、TemplateBaseは複数のページエントリーから共通でimportされる関係でRollupによって独立したchunkに分割されており、そのchunk自身が"css": ["TemplateBase-xxxx.css"]という独自のCSSファイルを持っていました。ところがrails_viteRailsVite::Manifest#resolve_cssの実装は、エントリー自身のcssフィールドしか見ておらず、importsmodulepreload用には再帰的に辿っている)配下のchunkが持つCSSを一切収集していませんでした。つまり複数ページで共有されるVueコンポーネントのscoped CSSが、本番ビルド(manifestベースのレンダリング)だと構造的に読み込まれないバグでした。

devサーバー経由の開発時レンダリングでは各SFCのCSSが個別に注入されるため問題は再現せず、本番ビルド(manifestベース)でのみ顕在化する類のバグでした。開発中には気づけない厄介なパターンです。

vite_rubyViteRuby::Manifest#resolve_entriesのソースを確認したところ、stylesheets: (entries + imports).flat_map { |entry| entry["css"] }.compact.uniqと、エントリー自身とimports両方からCSSを集める実装になっていました。rails_vite側の実装漏れであり、意図的な仕様変更ではなさそうに感じたので対応のPRを作成しました。

github.com

実装例・コード

nonce: trueを吸収するVitePackHelper

# app/helpers/vite_pack_helper.rb
module VitePackHelper
  # rails_viteのTagHelperはRails標準のnonce: true糖衣構文を解釈しないため、
  # 呼び出し側のインターフェースを変えずに済むよう、ここで実際のnonce値に変換する
  def vite_javascript_tag(*names, nonce: false, **options)
    options[:nonce] = content_security_policy_nonce if nonce
    super(*names, **options)
  end
end

devサーバーのオリジンをCSPのimg-srcに追加

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  # rails_viteはプロキシを持たないため、開発環境ではViteのdevサーバー(別オリジン)から
  # 画像が配信される。CSPで明示的に許可しないとimg-srcでブロックされる
  policy.img_src(:self, :data, *(RailsVite.dev_server_csp_source if Rails.env.development?))
end

db:test:prepareのvite:buildフックをスキップ

# .github/workflows/ci.yml(Node未セットアップのジョブ)
- name: Prepare test database
  run: bin/rails db:test:prepare
  env:
    SKIP_VITE_BUILD: "1" # pnpmをセットアップしていないジョブのためvite:buildフックを無効化

resolve_cssに対するModule#prependによる暫定パッチ

upstreamにPRがmergeされるまでの間、以下のパッチで先に問題を解消しました。

# config/initializers/rails_vite_patch.rb
module RailsViteManifestCssPatch
  # imports配下の共有chunkが持つCSSも再帰的に集めるようにする
  # upstream issue: https://github.com/skryukov/rails_vite/pull/40
  def resolve_css(names)
    names.flat_map { |name| collect_css_recursively(find_entry(name)) }.uniq
  end

  private

  def collect_css_recursively(entry, visited = Set.new)
    return [] unless entry
    own_css = entry["css"] || []
    imported_css = (entry["imports"] || []).flat_map do |import_key|
      next [] if visited.include?(import_key)
      visited << import_key
      collect_css_recursively(manifest_entries[import_key], visited)
    end
    own_css + imported_css
  end
end

Rails.application.config.to_prepare do
  RailsVite::Manifest.prepend(RailsViteManifestCssPatch)
end

with_playwright_pageを使ったcomputed styleの直接検証

# spec/system/debug/template_base_css_spec.rb(調査用の一時spec)
RSpec.describe "TemplateBaseのCSS適用確認", type: :system do
  include_context "login_as_user"

  it "TemplateBaseにdisplay: flexが適用されていること" do
    visit feed_bundles_path

    with_playwright_page do |page|
      # レスポンスステータスとconsoleエラーを直接確認し、CSS配信自体は正常であることを切り分ける
      page.on("console") { |msg| puts "[console:#{msg.type}] #{msg.text}" }

      display = page.eval_on_selector(".template-base", "el => getComputedStyle(el).display")
      expect(display).to eq("flex")
    end
  end
end

getComputedStyle()を直接呼ぶことで、「CSSファイルは配信されているが適用されていない」という、devtoolsのNetworkタブだけでは見えにくい状態を機械的に検証できました。

おわりに

rails_vite、移行に少しハマりどころもありましたが、ハックが少なく素直にViteを利用できる感じが良いですね✨

参考リンク

RubyでAIエージェントを作るgem Rixieを作った💎

最近個人開発でRixieというAIエージェントオーケストレーション用のRuby gemを作ってgemとして公開しました。

github.com

Rixieを作った動機はシンプルで、 PythonのLangChainやLlamaIndexのようなエージェントフレームワークをRubyで使いたかったのですが、 既存の選択肢はRailsに依存していたり、機能が限定的だったりと自分のユースケースにフィットするものなかったのと、 単純に自分でAIエージェントの中身の理解のためにAIエージェントフレームワークを作ってみたかったので今回作ってみました。

Railsに依存しないスタンドアロンなgemとして、 セッション管理・ツール実行・複数のエージェント戦略・MCPサポートを一通り揃えています。

動作確認環境: Ruby 3.4 / rixie 0.1.0

本題

概念階層:Session → Task → Run → think

Rixieの実行モデルは4層の階層で整理されています。

Session          # 会話全体を管理。複数のchatをまたいで履歴を保持
└── Task         # 1つのゴールを担当。Strategyを持つ
    └── Run × N  # Agent#thinkを1回呼び出す単位
        └── think (llm_call × N)  # LLMを呼び、ツールを実行し、ループする

Session#chatを呼ぶたびに1つのTaskが生成され、TaskはStrategyに従ってRunを何回実行するかを決めます。RunはAgentにthinkを依頼し、Agentはツール呼び出しが完了するまでLLMとのループを回します。

セットアップと基本的な使い方

以下の通り、gem "openai"や機能によっては追加で必要なライブラリが必要なことはありますが、 基本的にはRubyの標準機能の範囲内で動くように作っています。

# Gemfile
gem "rixie"
gem "openai"     # OpenAI / OpenAI互換エンドポイントを使う場合
gem "nokogiri"   # Fetch・WebSearchツールを使う場合
gem "cli-ui"     # CLIを使う場合

実際に使うときは以下のような感じで設定し、Rixie::Sessionを生成してRixie::Session#chatでLLMを呼び出します。

require "rixie"

Rixie.configure do |config|
  config.default_provider = "openai"
  config.default_model    = "gpt-4.1-mini"
end

session = Rixie::Session.new(instructions: "You are a helpful assistant.")
puts session.chat("Rubyの良いところを教えてください")
# => "Rubyの良いところは..."

OpenAI互換エンドポイント対応

BuiltinのプロバイダはOpenAIですが、register_providerでGitHub ModelsやOllamaなどのOpenAI互換エンドポイントを登録できます。

Rixie.configure do |config|
  # GitHub Models
  config.register_provider("github",
    adapter:  :openai,
    base_url: "https://models.github.ai/inference",
    api_key:  ENV["GITHUB_TOKEN"]
  )

  # Ollama(ローカル)
  config.register_provider("ollama",
    adapter:  :openai,
    base_url: "http://localhost:11434/v1",
    api_key:  "ollama"
  )
end

# GitHubModelsを使う場合
session = Rixie::Session.new(
  instructions: "You are a helpful assistant.",
  provider: "github",
  model: "openai/gpt-4.1-mini"
)

Strategy(エージェント戦略)の切り替え

Session#chatstrategy:引数で実行戦略を切り替えられます。

# Simple(デフォルト): 1つのAgentループで完結するタスク向け
session.chat("今日の東京の天気は?")

# PlanExecute: まず計画を立て、各ステップを順番に実行する複雑なタスク向け
session.chat(
  "Ruby 3.xの新機能を調査してレポートにまとめて",
  strategy: Rixie::Strategy::PlanExecute.new
)

# ReAct: Thought → Action → Observationのサイクルを明示するデバッグ・ステップ確認向け
session.chat(
  "東京の人口を調べて2倍にした数を教えて",
  strategy: Rixie::Strategy::ReAct.new
)

デフォルトでは上記の3つを用意していますが、run(task:, listener:) を実装したクラスを渡すだけで独自のストラテジーを作れます。Run を複数作って task.runs に積み、run.execute(listener:) を呼ぶだけです。

以下は「同じ入力を2つのエージェントに投げて最初に返った方を採用する」例です(シンプルな実装例として直列で書いています)。

class BestOfTwoStrategy
  def run(task:, listener:)
    candidates = 2.times.map do
      run = Rixie::Run.new(
        user_input: task.user_input,
        agent:      task.agent,
        context:    task.context
      )
      task.runs << run
      run.execute(listener:)
      run.output
    end

    # 長い方を「より詳細な回答」として採用する例
    candidates.max_by(&:length)
  end
end

session.chat("Rubyの魅力を教えて", strategy: BestOfTwoStrategy.new)

組み込みツール群

以下のような組み込みツールを用意してるのでWebサーチのようなRAG的な機能や、HILのような機能も実装することができます。

ツール 説明
Tool::WebSearch DuckDuckGo Liteで検索(nokogiriが必要)
Tool::Fetch URLを取得してテキスト抽出(SSRF対策あり)
Tool::WikipediaSearch Wikipedia検索(言語指定可)
Tool::Calculator 四則演算・べき乗など。手書きパーサでevalを使わない実装
Tool::CurrentTime 現在時刻をISO 8601形式で返す
Tool::FileRead ファイル読み込み(root_dirサンドボックス内のみ)
Tool::FileList globパターンでファイル一覧を取得
Tool::FileSearch regexでファイル横断grep
Tool::HumanInput エージェントからユーザーへの質問(Human-in-the-loop)
session = Rixie::Session.new(
  instructions: "You are a research assistant.",
  tools: [
    Rixie::Tool::WebSearch,
    Rixie::Tool::Calculator,
    Rixie::Tool::FileRead.with(root_dir: "/path/to/project")
  ]
)

MCPサポート

HTTPベースだけですがMCPもサポートしています。

require "rixie/mcp"

mcp = Rixie::MCP::Http::Client.new(url: "http://localhost:8000/mcp")

session = Rixie::Session.new(
  instructions: "You are a helpful assistant.",
  tools: mcp.tools + [Rixie::Tool::Calculator]
)

ストリーミング

Session#liveでトークンをリアルタイムに受け取れます。返り値はEnumeratorで、パターンマッチでイベントを振り分けます。

session.live("最新のRubyリリースを調べて").each do |envelope|
  case envelope.event
  in Rixie::Event::Token[delta:]
    print delta; $stdout.flush
  in Rixie::Event::ToolCallStart[tool_call:]
    puts "\n[#{tool_call.name} を呼び出し中...]"
  in Rixie::Event::Finished[content:]
    puts "\n#{content}"
  else
  end
end

マルチエージェントオーケストレーション

SessionRixie::Toolとしてラップすることで、エージェントを組み合わせ、コンテキスト節約や専門的なエージェントに作業を委譲させたりといったマルチエージェントオーケストレーションを構築できます。 (ツールベースの非決定論的なマルチエージェントオーケストレーションではなく決定論的なワークフローのようなものを組みたい場合には前述のStrategyを利用することで実現できます。)

research_agent = Rixie::Session.new(instructions: "You are a research specialist.")
write_agent    = Rixie::Session.new(instructions: "You are a technical writer.")

orchestrator = Rixie::Session.new(
  instructions: "研究と執筆を調整してレポートを作成してください。",
  tools: [
    Rixie::Tool.new(
      name:         "research",
      description:  "トピックを調査して調査結果を返す",
      input_schema: {type: "object", properties: {query: {type: "string"}}, required: ["query"]},
      call: ->(args) { research_agent.chat(args["query"]) }
    ),
    Rixie::Tool.new(
      name:         "write",
      description:  "トピックに基づいてレポートを書く",
      input_schema: {type: "object", properties: {topic: {type: "string"}}, required: ["topic"]},
      call: ->(args) { write_agent.chat(args["topic"]) }
    )
  ],
  parallel_tool_calls: true
)

おわりに

まだv0.1.0で荒削りな部分もありますが、個人サービスで利用している範囲ではありますが、RailsなしのRubyスクリプトやCLIツールからAIエージェントを動かしたいときには使いやすいなぁというのと、自身でAIエージェント系のフレームワークを自作してみて理解が深まったので作ってみてよかったです💎

VSCodeでインタプリタを適切に選択してもpythonの自動補完が表示されない事象の解決方法メモ

VSCode上のMicrosoft公式のPython及びPylanceを使ったpython開発環境でpythonのメソッド等のインラインでの補完が動かず、いろいろ検索するとインタプリタを適切なものに選択すると言うのが結構多く出てきましたが、それでは解決しなかったので、色々試して動くようになったので解決方法をメモ📝

github.com

github.com

結論は以下の設定を.vscode/settings.json等に入れてあげれば解決しました。

{
  "editor.quickSuggestions": {
    "other": "on" // 文字列およびコメント外での補完表示の有効化
  }
}

なんかglobalなVSCodeの設定が、知らず知らずのうちにこの辺の設定を上書きしてしまっていたのかも? 🤔

GitHub Copilot CLI向けの自作プロンプトをpluginとしてGitHubリポジトリで配布するメモ📝

はじめに

以下のリポジトリでGitHub Copilot CLI向けの自作プロンプトをpluginとしてGitHubリポジトリで配布してみたので、その手順をまとめました!

github.com

最近GitHub Copilot CLIを色々使っているのですが、CladeCodeのpluginのようにGitHugb Copilot CLIにもpluginの機能があり、plugin等を配布しているAwesome GitHub Copilotというリポジトリもあるのですが、、、、

GitHub Copilot CLIはまだPublic Previewということもあり、公式ドキュメント等でまだ作り方や配布方法が詳しく記載されていない様なので、awescome-copilotの中身を参考にしつつ、実際に自分で以下にcopilot cliのplugin等を公開するリポジトリを作ってみたので方法をメモしておきます📝

本題

pluginを配布するリポジトリ構成

最低限必要なのは.github/plugin/marketplace.jsonと、plugins/配下にpluginの定義をMarkdownで書いたファイルを置くだけの様です。

root
├── .github/
│       └── plugin/
│               └── marketplace.json
└── plugins/
    └── plugin-name/  # プラグインの本体
        ├── .github/
        │      └── plugin/
        │              └── plugin.json  # プラグインのメタ情報
        ├── skills/ # プラグインで配布するスキル (あれば)
        │       └── skill-name/
        │               └── SKILL.md
        ├── agents/  # プラグインで配布するエージェント(あれば)
        ├── instructions/  # プラグインで配布する命令書 (あれば)
        ├── README.md  # プラグインの説明やインストール方法など
         ...             # その他(例: ドキュメント、サンプルコードなど)

この構成だけで「プラグインの中身」としてCopilot CLIが認識してくれるようでした🎉

ファイルのサンプル

特に重要なのは.github/plugin/marketplace.jsonと、plugins/xxx/.github/plugin/plugin.jsonの2点です。 この2点をもとにGitHub Copilot CLIが「このリポジトリはプラグイン集で、plugins/配下のディレクトリはそれぞれプラグインとして認識する」という挙動になる模様 👀

.github/plugin/marketplace.json

{
  "name": "my-agents-collection",
  "metadata": {
    "description": "A collection of prompts for AI agents that I have created.",
    "version": "0.0.1",
    "pluginRoot": "./plugins"
  },
  "owner": {
    "name": "your name",
    "email": "your email"
  },
  "plugins": [
    {
      "type": "plugin",
      "name": "plugin-name",
      "description": "A plugin that provides ...",
      "path": "./plugins/plugin-name"
    }
  ]
}

plugins/plugin-name/.github/plugin/plugin.json

{
  "name": "plugin-name",
  "description": "A plugin that provides ...",
  "version": "0.0.0",
  "author": {
    "name": "Your Name"
  },
  "repository": "https://github.com/owner/repo",
  "license": "MIT"
}

marketplace登録とインストール

リポジトリをmarketplaceとして登録すると、そこからプラグインを以下のようにinstallできるようになります📥

# コレクション(リポジトリ)をmarketplaceに追加
copilot plugin marketplace add owner/repo

# 例: プラグインをインストール
copilot plugin install plugin-name@repo

ちなみに試してないですがprivateなリポジトリもサポートしているようです 👀

github.com

おわりに

とりあえず手探りではありますが、上記のような構成でリポジトリを作成し、marketplaceに登録することで、GitHub Copilot CLI向けのプラグインを配布できるようになりました🎉

プロンプト周りを再利用したり、チームとして共有したりするのに便利そうなので汎用性の高いプロンプトや、特定の用途に特化したプロンプトなどはpluginとしてまとめて配布できると良さそうですね 👍

参考リンク

`strict_loading_mode=:n_plus_one_only`をグローバルに設定してRails標準機能でN+1を検知するメモ📝

はじめに

Railsのstrict_loadingは「関連を意図せず遅延ロードしてない?」を検知できる便利機能ですが、Rails v8から strict_loading_mode をグローバル設定できて、しかも :n_plus_one_only が選べる のを最近知りました📝

Allow to configure strict_loading_mode globally or within a model.
Defaults to :all, can be changed to :n_plus_one_only.
https://github.com/rails/rails/blob/8-0-stable/activerecord/CHANGELOG.md

以下の記事で以前書いた時にはstrict_loading_mode = trueしかグローバルに設定できなかったので導入を躊躇っていたのですが、n_plus_one_onlyなら現実的に運用できそうだったので、導入してみたメモ📝

madogiwa0124.hatenablog.com

strict_loading_mode=:n_plus_one_onlyをグローバルに有効化する

以下の通り、development/test環境で有効化しました。

# config/environments/development.rb
config.active_record.strict_loading_by_default = true
config.active_record.strict_loading_mode = :n_plus_one_only
# config/environments/test.rb
config.active_record.strict_loading_by_default = true
config.active_record.strict_loading_mode = :n_plus_one_only

これだけで、development/test環境で関連の遅延ロードがN+1的に発生した場合に例外が発生するようになります🎉

またTIPSですが、consoleではstrict_loadingによるN+1の検知を無効化したい場合には以下のようにapplication.rb等で無効化してあげると良いかと思います。

# config/application.rb
console { ApplicationRecord.strict_loading_by_default = false }

おわりに

Rails 8でn_plus_one_onlyモードをグローバルで適用できるようになり、strict_loadingの運用がかなり現実寄りになった気がします。

Rails標準機能でN+1を検知できるのはありがたいですね🙏

参考リンク

DevcontainerのChrome DevTools MCPをコンテナでinstallしたChromiumに接続する方法メモ📝

DevcontainerでChrome DevTools MCPを使うときにホスト側のChromeデバッグモードで起動してipアドレス指定でホスト側もChromeに接続したり、UbuntuベースのDevcontainer環境にGoogle Chromeを入れるの、リポジトリや権限周りでつまずきやすくめどくさいなぁと思っていたのですが、

多少ChromiumChromeの互換性の問題が出たとしてもコンテナ内で完結できるメリットもありそうに感じ、Chrome DevTools MCPをコンテナ内でinstall済みのChromiumに接続できないかと思い試したら出来たのでメモしておきます📝。

本題

Devcontainer上でChrome DevTools MCPはインストール済みなことが前提で、主に必要な対応は以下でした。

  • Devcontainerイメージにchromiumを追加
  • start-chromium-mcp.shChromium(headless)を--remote-debugging-port=9222で起動し、chrome-devtools-mcp--browser-urlで接続
  • devcontainer.jsonMCPサーバー設定をスクリプト実行に変更(ホストChrome依存の設定を撤廃)

これにより、ブラウザ起動からMCP接続までをDevcontainer内で完結でき、ホスト側のChromeを立ち上げたりしなくてもMCP経由でAI AgentがChrome DevToolsにアクセスしてくれるようになりました。

実装例・コード

1) Dockerfile(Chromiumの導入)

# .devcontainer/Dockerfile(抜粋)
RUN apt-get update && apt-get install -y \
    chromium
  • Devcontainer(Debian/Ubuntu系)を想定し、chromiumをAPTで追加しています。

2) DevcontainerのMCP設定(スクリプトに委譲)

// .devcontainer/devcontainer.json(抜粋)
{
  "customizations": {
    "vscode": {
      "settings": {
        "mcp.servers": {
          "io.github.ChromeDevTools/chrome-devtools-mcp": {
            "type": "http",
            "config": {
              // Google Chrome DevToolsをChromiumで使うための設定
              "command": "/app/.devcontainer/scripts/start-chromium-mcp.sh",
              "env": {}
            }
          }
        }
      }
    }
  }
}

コンテナ上のchromiumをurl指定で起動してMCPに設定する/app/.devcontainer/scripts/start-chromium-mcp.shを呼び出します。

3) 起動スクリプトChromium + MCP

# .devcontainer/scripts/start-chromium-mcp.sh
#!/usr/bin/env bash
set -e

PORT=9222

# Chromium が未起動なら起動
if ! curl -sf http://127.0.0.1:${PORT}/json/version > /dev/null; then
  chromium \
    --headless \
    --no-sandbox \
    --disable-gpu \
    --remote-debugging-port=${PORT} \
    about:blank \
    >/tmp/chromium.log 2>&1 &

  # DevTools endpoint 起動待ち
  for i in {1..20}; do
    if curl -sf http://127.0.0.1:${PORT}/json/version > /dev/null; then
      break
    fi
    sleep 0.2
  done
fi

# UUID を直接扱わず browser-url で接続
exec npx chrome-devtools-mcp@latest \
  --browser-url http://127.0.0.1:${PORT}

おわりに

多少ChromiumChromeの互換性の問題があるかもしれませんが、自分のローカルでは今のところはいい感じっぽい?ので、これで使ってみて様子見してみようと思います🫡

参考リンク

qiita.com

github.com

2025年振り返り📝

今年も一年が終わるということで今年も振り返ってみる。

今年の振り返り

アウトプット

BLOG

BLOGは、今年1年で20記事書いていた。色々とあり6月ぐらいから更新が滞ってしまった。。。

pickup

madogiwa0124.hatenablog.com

madogiwa0124.hatenablog.com

madogiwa0124.hatenablog.com

公開したツールとか

今年は独自のUIライブラリを作った。

github.com

あと独自の認証ライブラリをマルチスコープ機能とか入れたりRails標準ぽいcookie + DBセッション形式に直したりした。

github.com

github.com

OSS活動

今年はRailsにCSPのhash-sourceをサポートするPRと、

github.com

stylelint-config-recommended-vueにstylelint-config-recommended v16対応を入れたPRを作成しました。

github.com

ref: 今年のPRリスト

インプット

今年も、去年に引き続き100冊以上読んでいて136冊でした。100冊以上本を読むのは大変かなとも思ったのですが、小説も結構読んでいたのと、割と習慣化されてきていてそこまで無理せずに今年も読めて良かったなと思います。

印象に残ってる本をピックアップして載せておきます。

技術書

面接対策というよりかはオートコンプリートやWebクローラ等々、よくあるシステムの構成の型が知れて勉強になった。

技術キャリアでのリーダーシップについての本、やはりプロダクトとアーキテクチャと組織はセットだなぁという感想になった。

AIを使ったコーディング周りの本、プロンプトテクニックというよりも人間にとって分かりやすいコードベースはAIにとっても分かりやすいだろうなと感じた。

プロンプトテクニックを超えて、エンジニアリングとして実際にLLMをサービス導入する際のテクニックや考え方が学びだった。

去年の目標と結果

目標 メモ 結果
モダンフロントエンドの技術的なキャッチアップ 独自UIライブラリも作れたし一定達成できたということで ⚪︎
OSS活動もうちょっと頑張りたい 数は少ないがCHANGELOGに乗る対応を複数PR出せたということで ⚪︎
人生における楽しみを増やす 散歩とかしか趣味を増やしたりはできなかった。
心身ともに穏やかに健康で過ごす 色々あったが、まぁ穏やかに過ごせたと言えば過ごせた ⚪︎

今年の目標

今年はとりあえず平和に穏やかに心身ともに健康で過ごせれば良しということで。

おわりに

今年は仕事もプライベートもバタバタした1年だったが、 来年は今年よりバタバタしそうなので何とか心身ともに健康で乗り切りたい。