PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

Storybook を MCP サーバ化したらフロントエンド開発が自動化される未来が見えた

こんにちは。テックリードの丸山 @maruyamaworks です。最近は Claude Code に全部賭けています。

前回、GitHub Copilot を使って Storybook を作った話を書きました。 developers.play.jp

今回はその応用編として、Storybook を MCP サーバ化して LLM に提供することで、社内 UI コンポーネントを使用したフロントエンドの開発を自動化できるのではないか、ということで実験してみたいと思います。(これをやるために Storybook を作ったといっても過言ではない)

何をやろうとしているのか

この記事では何をやろうとしているのか、最初に概要を説明します。

やりたいことは、LLM にプロンプトを投げてコードを自動生成することなのですが、その際、LLM が Storybook の情報を MCP サーバ経由で取得できるようにします。MCP (Model Context Protocol) についてはもはや説明不要かと思いますが、Claude の開発元である Anthropic 社が策定した、LLM が外部のツールやデータソースとやりとりするときの仕様(プロトコル)を定めたものです。こうすることで、弊社の社内 UI コンポーネントを活用しながらコード生成ができるのではないか、というのが本稿の目指すところとなります。

まずは先駆者から学ぼう

おそらくこの記事にたどり着いた方はこちらの記事もご覧になっているかと思います。

Ubie さんの記事社内デザインシステムをMCPサーバー化したらUI実装が爆速になった
LayerX さんの記事Storybook の情報を抜き出して MCP サーバにしてみる

まずは先駆者の事例から学びます。LLM に情報を提供するにあたり、MCP サーバ側に用意する tool としては、使用可能なコンポーネントの一覧を返却する tool と、指定した 1 個のコンポーネントについてその詳細を返却する tool の 2 個を用意するのが良さそうですね。

というわけで早速、MCP サーバを実装していきましょう。

MCP サーバを実装する

MCP サーバの実装方法は、Model Context Protocol 公式サイトの Quickstart で分かりやすく説明されているので、この手順に従えば誰でも実装できると思います。

modelcontextprotocol.io

今回は、以下の 2 つのツールを実装します。

  • list-components: 使用可能な社内 UI コンポーネントの一覧を返却するツール。実装としては LayerX さんの記事で紹介されているのと同じように、Storybook のビルド時に出力される index.json からコンポーネントの名前を抽出して、その名前をコンマ区切りの文字列として返すようにしました。
  • describe-component: 引数として UI コンポーネントの名前を受け取り、そのコンポーネントの仕様や実装例を返却するツール。実装としては、.stories.tsx ファイルを解析していい感じのドキュメントを作ろうかとも思いましたが、なかなか大変そうだったので、思い切って(?).stories.tsx ファイルのコードをそのまま返却するようにしてみました。

最終的な MCP サーバの実装は以下のようになりました(細かいエラー処理などは省略しています)。

index.ts

import fs from "fs/promises";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// Storybook のビルド時に出力される index.json を読み込み
const manifest = await fs.readFile(import.meta.dirname + "/../../playcloud-frontend/storybook-static/index.json", "utf-8");
const { entries } = JSON.parse(manifest);

// MCP サーバのインスタンスを作成
const server = new McpServer({
  name: "playcloud-storybook",
  version: "1.0.0",
});

/**
 * list-components ツール
 * 使用可能な社内 UI コンポーネントの一覧を返却する
 */
server.tool(
  // ツールの名前
  "list-components",
  // ツールの説明(LLM はこれを読んでいつどのツールを使うべきか決める)
  "Get the list of available PLAY CLOUD UI components",
  // ツールの引数定義(このツールは引数を受け取らないため空オブジェクト)
  {},
  // ツールの実装
  async () => {
    // index.json からコンポーネントの名前を抽出
    const keys = Object.keys(entries).filter(key => /^components-.*--docs$/.test(key));
    const componentNames = keys.map(key => entries[key].title.replace(/^Components\//, ""));

    return {
      content: [{
        type: "text",
        // LLM に返却されるレスポンス(コンポーネントの名前をコンマ区切りで連結)
        text: `There are ${componentNames.length} available components: ${componentNames.join(", ")}.`,
      }],
    };
  }
);

/**
 * describe-component ツール
 * 指定されたコンポーネントの使用や実装例を返却する
 */
server.tool(
  // ツールの名前
  "describe-component",
  // ツールの説明(LLM はこれを読んでいつどのツールを使うべきか決める)
  "Get the storybook file content of the specified PLAY CLOUD UI component",
  // ツールの引数定義(このツールは引数としてコンポーネント名の文字列を要求する)
  { componentName: z.string().describe("The name of PLAY CLOUD UI component") },
  // ツールの実装
  async ({ componentName }) => {
    const entry = entries[`components-${componentName.toLowerCase()}--docs`];
    if (!entry) {
      return {
        content: [{
          type: "text",
          // 存在しないコンポーネント名が渡された場合はその旨のメッセージを返却
          text: `The specified component "${componentName}" does not exist.`,
        }],
      };
    }

    return {
      content: [{
        type: "text",
        // 渡されたコンポーネントの Story ファイルのコードをそのまま返却
        text: await fs.readFile(import.meta.dirname + "/../../playcloud-frontend/" + entry.importPath, "utf-8"),
      }],
    };
  }
);

// MCP サーバを起動
const transport = new StdioServerTransport();
await server.connect(transport);

あとは、このファイルを tsc でトランスパイルして node コマンドに渡して実行すれば、ローカルで MCP サーバを起動できます(最近の Node.js であれば TS のままでも実行できると思います)。

実際に試してみた

それでは、実際に MCP サーバを使ったコーディングを試してみましょう。MCP クライアントとしては、VSCode や Claude Code など色々なものが対応していますが、今回は VSCode を使ってみます。LLM は Claude Sonnet 4 を使用しますが、他の LLM でも問題ありません。

VSCode の場合は、settings.json に以下のように設定を追記することで MCP サーバが使えるようになります。

settings.json

{
  "mcp": {
    "servers": {
      "playcloud-storybook": {
        // MCP サーバを起動するためのコマンド
        "command": "node",
        // そのコマンドに渡す引数
        "args": ["/Users/kenichi.maruyama/playcloud-frontend-mcp/build/index.js"]
      }
    }
  }
}

正しく設定されていれば、Copilot Chat のウィンドウ下部にある「Configure Tools」ボタンを押したときに表示される MCP Tools の一覧の中に、自作の MCP サーバが表示されているはずです。

MCP Tools の一覧を確認する

この状態で以下のプロンプトを投げてみます。*1

PLAY CLOUD UI components は私たちの会社で独自に開発している React コンポーネントです。
あなたは、PLAY CLOUD UI components を使用してユーザーのプロフィール情報を入力するフォームを実装し、
src/components/UserProfileForm.tsx として保存してください。

<instructions>
- 使用可能なコンポーネントの一覧は list-components ツールで取得できます。
  また、各コンポーネントの実装例は describe-component ツールで取得できます。  
- 可能な限り PLAY CLOUD UI components を使用すること。
  特に、HTML5 native の <input> 要素は絶対に使用しないでください(代わりに
  Input コンポーネントを使用してください)。  
- tailwind CSS は絶対に使用しないでください。
</instructions>

すると、まず list-components ツールを使ってコンポーネントの一覧を取得し、続いてその中からプロフィール情報のフォームを作るのに必要そうなコンポーネント(Form、Input、Select、DatePicker、Panel など)の仕様を describe-component ツールを使って順番に調べるという動きになりました。

UserProfileForm の生成過程

そして、できあがったフォームはこのような感じになりました。

生成された UserProfileForm

PLAY CLOUD のデザインシステムに則ったフォームができあがりました。「プロフィール情報を入力するフォームを作って」という雑な指示だけでここまで作ってくるとは、たいへん驚きました。もう人間がフロントエンドのコードを書く必要はないのではないかと思いました。

しかも、バリデーションもちゃんと実装されていたり...*2

生年月日は過去の日付からしか選べないようになっていたり...*3

性別は「その他」「回答しない」が選択肢として用意されていたり...

もはや一流のエンジニアですね!

今後の展望

今回実装した MCP サーバはローカルで動作するため、利用するためには各開発者の環境で MCP サーバを動かす必要があります。まずはこれを改善するため、リモート MCP サーバとしてデプロイし、開発者が簡単かつセキュアに利用できる状態にすることを現在検討しています。

また、今回プロフィール入力フォームを実装させてみましたが、生成されるコードの品質は、MCP サーバ経由で渡しているドキュメントの内容によって大きく左右されます。なるべく高品質かつ正確なドキュメントを LLM に提供することで、生成物のクオリティも高まると考えられますので、今後も継続的にドキュメント(Storybook)の改善を行っていく必要があると感じました。

おわりに

今回、フロントエンドの開発が生成 AI の力によって自動化できる可能性を感じました。さすがに AI が生成したコードをそのまま本番環境にリリースできるわけではありませんが、画面のレイアウトなどベースを作る段階では非常に役に立つと感じましたし、人間がゼロからコードを書くのに比べれば圧倒的に早く目的の画面を完成させることができました。こういったツールを上手に使いながら、皆さんも開発生産性を高めていきましょう!

*1:稀に tailwind CSS を勝手に使うことがあるのですが、PLAY CLOUD では tailwind CSS を導入していないので、使わないように指示を追加しています。

*2:名前が「名 → 姓」の順になっているのがちょっと惜しいですね。ただ、この程度であればプロンプトで指示すれば改善されそうですし、人間が手で修正してもいいでしょう。

*3:弊社の自作の DatePicker コンポーネントには "disableFuture" という props が用意されているのですが、気を利かせてそれを使ってくれました。