ブラウザ拡張機能で MCP Server と会話する — Native Messaging と Vercel AI SDK を活用した MCP Client 実装

本記事は Model Context Protocol(MCP) Advent Calendar 2025 の 19日目の記事 & CARTA アドベントカレンダー2025 の18日目の記事です。

まず始めに3行まとめ

この記事では、ブラウザ拡張機能と Native Messaging を組み合わせることで、ブラウザの制約を超えて MCP Server と対話するアーキテクチャを紹介します。

  1. ユーザー体験の向上: webRequest で認証情報をキャプチャすることで、ユーザーは普段通り Web サイトにログインするだけで AI サービスが利用可能に。
  2. ブラウザの制約突破: Native Messaging を介することで、ローカルで動作する MCP Server への接続や、CORS 制約のある API へのアクセスが可能に。
  3. 開発の効率化: Native Host 側で Vercel AI SDK を利用することで、MCP Client の実装やストリーミング処理を簡潔に記述出来る。

はじめに

こんにちは、サポーターズで、エンジニアではないメンバーの業務もAIで爆速にしたいと思っている ちゅうこ(@y_chu5) です。

CARTA / サポーターズでは普段の業務で、Google Workspace の Gemini の WebUI を使用しています。 Google Workspace with Geminiライセンス上では入力が学習に使われず、安心して業務に必要な情報を入力することが出来るというメリットがあります。 しかし、Web UI 上での利用となるため、業務フローに組み込むのが難しいという側面があります。

これを解消する方法として、真っ先に API 経由で Gemini を利用することが考えられます。 n8n や Dify などの Workflow Automation ツールの AI 機能を利用するために Gemini API キーを登録することで、そのツール上のワークフローに Gemini を組み込むことが可能になります。 そのように何かしらのツールの上に載せることで、業務フローに組み込むということが可能になります。

しかし、ツールの使い方を覚えるのが大変だったり、ローカルで動作する MCP Server に繋いだりのようなことが出来ない、という一定の制約のもとで利用する必要があるため、適用範囲が限られてしまう、という問題があります。

その状況を打破するため、APIキーをクライアントに渡さない!fetchインターセプトで実現するセキュアなAI API配信 の記事では、Biz サイドに APIキーを直接配らずに、MCP Serverとやり取りできるアプリを提供するための、Electron アプリを作成する話をしました。 提供する Electron アプリの使い方などを覚えてもらう必要はありますが、ローカルで動作する MCP Server に繋いだり、Gemini API キーを直接配らずに済む、というメリットがあると考えた試みでした。

しかしながら、もう少し手軽に、普段の業務に使っているツールに近いもので解決できないかという思いがありました。 例えば、認証などの設定をいい感じに…いい感じにやってくれて、使いたい時にすぐそばにある………そう…ブラウザのように…………

ブラウザ……?………ブラウザ拡張機能…!

ということで、今回はブラウザ拡張機能を使って、MCP Server とやり取りできるようにする話を書きたいと思います。

例に漏れず、今回も PoC レベルの実装であるため、実務で使うには一定のカスタムやセキュリティ対策が必要になることをご承知おきください。

コードは以下で公開しています。

https://github.com/yamachu/browser-mcp-client

アーキテクチャ図

今回のアーキテクチャは下図のようなものです。 以下の図では、ブラウザ拡張機能が Native Messaging を使用して、Native Messaging Host アプリケーション上で MCP Client として動作している様子を表しています。

この図で注目するのは大きく3点あります。

  1. 指定した Web サイトで利用している Authorization ヘッダーを拡張機能でキャプチャしている
    • わざわざログイン処理などを行ったり、拡張機能でサービスに応じた処理を行うのはユーザにとっても面倒だろうと判断
  2. Native Messaging を使用して、ブラウザで動作する拡張機能は基本的には UI 処理と AI サービスの接続情報の管理に専念している
    • MCP Client の実装を拡張機能に直接組み込むことも出来るが、Service Worker などの寿命問題や、ブラウザ上で動作することの制約から逃れるため、Host アプリケーション側で MCP Client を動作させている
  3. Native Messaging Host アプリケーションは MCP Client として動作し、Vercel AI SDK を使用して MCP Server とやり取りを行っている
    • CARTAアドカレ9日目の記事で紹介した Fetch Interceptor が標準で使える辺り最高なんですよね…
    • Streaming 対応もすごい楽で、もう…最高なんです…

これらの注目点を踏まえつつ、以下で詳細に説明していきます。

ブラウザ拡張機能で Authorization ヘッダーをキャプチャする

今回開発したブラウザ拡張機能では、特定の Web サイトで利用している Authorization ヘッダーをキャプチャするために、webRequest API を使用しています。

https://developer.chrome.com/docs/extensions/reference/api/webRequest?hl=ja

この機能を実装した背景として、CARTA 社内には社員が誰でも利用可能な ChatGPT ライクな Chat アプリがあり、そのアプリは OpenAI や Anthropic の API Proxy としても機能しています。 つまり、社員はそのアプリにログインするだけで、OpenAI や Anthropic の API キーを意識せずに AI サービスを利用できる仕組みになっています。

// wxt を使用しているため、chrome ではなく  browser 名前空間を使用しています
browser.webRequest.onBeforeSendHeaders.addListener(
    (details) => {
        const host = new URL(details.url).host;
        const authHeader = details.requestHeaders?.find(
            (header) => header.name.toLowerCase() === "authorization"
        );

        if (authHeader?.value?.startsWith("Bearer ")) {
            const token = authHeader.value.slice("Bearer ".length);
            // キャプチャした JWT トークンを Native Messaging Host アプリケーションに送信するためにローカルに Store
        }

        return {}; // Do nothing
    },
    {
        urls: JWT_SNIFFER_URI.map((uri) => `${uri}/*`),
    },
    ["requestHeaders"]
);

これにより、設定した Web サイトで利用している Authorization ヘッダーをキャプチャし、当該の Proxy サービスに対応した JWT トークンを取得しリクエストを送信することが可能になります。

またそれは、ユーザが拡張機能のために明示的に何かをするのではなく、普段通りにその Web サイトにログインして利用するだけで済む、というメリットもあります。

………とは言いつつも、Authorization ヘッダーに関わらず、ユーザのリクエスト情報を扱うのは一定のリスクを伴います。

※ 良い子の皆さんは、くれぐれも悪用厳禁でお願いしますね!!!!!!!あくまで信頼できる社内環境での一解決策です…

Native Messaging でブラウザの制限を突破する

ブラウザ拡張機能は非常に便利で、前述したようにリクエストの傍受みたいなことも出来ます。 しかしセキュリティ上の制約から、直接ローカルのリソースや外部サービスにアクセスすること(CORS制約)が制限されています。

そのため、例えば AI Service の Proxy サービスなどがリクエストの送信元を制限していたり、後述するようなローカルで動作する MCP Server を使用する際、起動はおろか、stdio 経由での通信すら出来ない、という問題があります。

そこで登場するのが Native Messaging です。

ネイティブメッセージングはユーザーのコンピューターにインストールされたアプリケーションと拡張機能との間のメッセージ交換を可能にします。

引用: https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Native_messaging

つまり、ブラウザ上で考えないといけない制約を、Native アプリケーションに委譲することで見なかったことに出来るのです。

以下が Chrome 拡張機能の Background Script 側で Native Messaging Host アプリケーションに接続し、メッセージを送信するコード例です。

let nativePort: ReturnType<typeof browser.runtime.connectNative> | null = null;

function getNativePort(): ReturnType<typeof browser.runtime.connectNative> {
  if (nativePort === null) {
    nativePort = browser.runtime.connectNative(NATIVE_HOST_NAME);
    // 中略
  }

  return nativePort;
}

// Background Script
  browser.runtime.onMessage.addListener(
    (
      message: ExtensionMessage,
      _sender,
      sendResponse: (response: MessageResponseMap[typeof message.type]) => void
    ) => {
        const port = getNativePort();

        port.postMessage({
          action: "chat",
          prompt: message.prompt,
          jwt: message.jwt,
          apiBaseUrl: message.apiBaseUrl,
          provider: message.provider,
          model: message.model,
          tools: message.tools,
        } satisfies ToNativeChatMessage);

        return true;
      }
  );

Native Messaging Host アプリケーション側では、stdio 経由で特定のメッセージを受け取るのですが、この記事では割愛します。 また Native Messaging Host を呼び出すための Manifest ファイルの存在などもあるのですが、同様に以下の記事におまかせしてしまいます。

zenn.dev

こちらの記事で解説しているため、併せてご覧ください。 受け取ったメッセージをもとに、例えば MCP Server を起動したり、Vercel AI SDK を使用して MCP Client として、各種 AI Service とやり取りを行うことが可能になります。

Vercel AI SDK で MCP Client を実装する

Native Messaging Host アプリケーション側では、Vercel AI SDK を使用して MCP Client として動作するように実装しています。 今回Vercel AI SDK を使用した理由として、以下の点が挙げられます。

  • Vercel AI SDK は MCP サポートを(一部実験的サポートではあるが)行っている
  • fetch をカスタマイズできるため、本来の AI Service へのリクエストをカスタマイズしやすい作りになっている
  • Streaming でのメッセージの処理の面倒な箇所が SDK 側で吸収されているため、UI 側での実装が楽になる

などの理由があります。

そんな Vercel AI SDK を使用して AI Service とやり取りを行うコード例が以下になります。

import { createAnthropic } from "@ai-sdk/anthropic";
import { stepCountIs, streamText } from "ai";

function createLLM() {
  return createAnthropic({
    apiKey: jwt,
    baseURL: apiBaseUrl,
    headers: {
        authorization: `Bearer ${jwt}`,
    },
    // fetch: ここに Custom した fetch が渡せる
  })(model);
}

const model = createLLM();
const result = streamText({
    model,
    stopWhen: stepCountIs(10),
    messages: [{ role: "user", content: prompt }],
});

for await (const part of result.fullStream) {
    switch (part.type) {
        // text-start, text-endは無視してtext-deltaだけ処理
        case "text-delta":
            yield { type: "token", token: part.text }; // こんな感じで UI に徐々に結果を渡せる
            break;

        case "tool-call":
            yield {
                type: "tool_call",
                toolName: part.toolName,
                toolArgs: JSON.stringify(part.input),
            };
            break;

        case "tool-result":
            yield {
                type: "tool_result",
                toolName: part.toolName,
                result: part.output,
            };
            break;

        case "error":
            debug("Stream error:", part.error);
            break;
    }
}

上記の例では tool calling を行っていませんが、実際のテキストの生成結果や tool calling の結果を上記のような処理で取得することが出来ます。

また本来行いたかった MCP Client としての動作して、Tool calling するのは以下のようなコード例で実装することが出来ます。

import {
  experimental_createMCPClient,
  experimental_MCPClient,
} from "@ai-sdk/mcp";
import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";

async function loadMcpTools(userDefinedTool: any) {
    // Experimental…! Stable じゃないけど…大丈夫だよね…!
    const transport = new Experimental_StdioMCPTransport({
        command: userDefinedTool.command,
        args: userDefinedTool.args,
    });

    const client = await experimental_createMCPClient({
        name: "browser-mcp-client",
        version: "1.0.0",
        capabilities: {},
        transport,
    });

    const toolsResult = await client.tools();

    return { toolsResult, client };
}

//

const { toolsResult, client } = await loadMcpTools(userDefinedTool);
const result = streamText({
    model,
    tools: toolsResult, // <- ここで Tools を渡す
    stopWhen: stepCountIs(10),
    messages: [{ role: "user", content: prompt }],
});

Experimental ではありますが、experimental_createMCPClient や Experimental_StdioMCPTransport のような SDK が提供するインターフェースに乗っかるだけで、自前でstdioをガチャガチャしなくて済むため、非常に楽に MCP Client としての動作を実装することが出来ます。

まとめ

本記事では、ブラウザ拡張機能と Native Messaging を組み合わせることで、ブラウザの制約を超えて MCP Server と対話するアーキテクチャを紹介しました。

この構成のポイントは以下の3点です。

  1. ユーザー体験の向上: webRequest で認証情報をキャプチャすることで、ユーザーは普段通り Web サイトにログインするだけで AI サービスが利用可能に。
  2. ブラウザの制約突破: Native Messaging を介することで、ローカルで動作する MCP Server への接続や、CORS 制約のある API へのアクセスが可能に。
  3. 開発の効率化: Native Host 側で Vercel AI SDK を利用することで、MCP Client の実装やストリーミング処理を簡潔に記述出来る。

今回は PoC としての実装でしたが、このアーキテクチャを発展させることで、「社内ツールやローカルファイル(MCP Server)を、普段使っているブラウザ上からシームレスに操作する」という、新しい AI アシスタントの形が作れるのではないかと考えています。

ブラウザという最も身近なインターフェースに MCP の力を持ち込むことで、業務フローへの AI 組み込みがより加速するのではないでしょうか…!

コードは GitHub で公開していますので、興味のある方はぜひ触ってみてください。

https://github.com/yamachu/browser-mcp-client