Playwright MCP + Figma MCP + Supabase MCPを使って開発してみた

## はじめに
最近話題の MCP を使って、Figma デザインから Supabase でのバックエンド構築まで、アプリ開発の工程を一通り試してみました。今回は、AI を用いて0から作るのではなく、あらかじめ用意された設計書をAIに入力して、設計書に近いものを作成することを目指して開発を行いました。この記事では、開発の流れと感想をまとめています。

### MCPとは？
MCP（Model Context Protocol）とは、AI エージェントがさまざまな外部ツールを使用するためのプロトコルです。AI エージェントにとっての USB-C とも言われており、統一されたインターフェースを通じて外部サービスと連携できます。

今回は以下の MCP を使って一連の開発を行いました。

- Figma MCP
- Supabase MCP
- Playwright MCP
- Context7 MCP


#### Figma MCP
Figma の URL を使って、Figma 側でデザインされた画面の情報を読み込むことができます。AI に指示することで、Figma でデザインされた画面に近いものをすぐに生成できるようになります。


https://github.com/GLips/Figma-Context-MCP


#### Supabase MCP
Supabase のプロジェクトに接続することで、データベースの管理をプロンプト一つで行えるようになります。

https://github.com/supabase-community/supabase-mcp

#### Playwright MCP
AI に Playwright を用いてブラウザ操作をさせる MCP です。Playwright の使用用途のように E2E テストを実行することはもちろん、スクレイピングも行えるようになります。

https://github.com/microsoft/playwright-mcp

#### Context7 MCP
LLM モデルは学習した時期までの知識しか持ちません。そのため、コード生成を行う際に古い書き方のコードを書いてしまうことがあります。Context7 は最新のライブラリ情報を提供する MCP であり、使用することで、最新の書き方のコードを書くことができるようになります。

https://github.com/upstash/context7


### 開発するもの
今回は試しに、[https://warikan.tool.icchi.me/](https://warikan.tool.icchi.me/) のような割り勘計算サイトを開発しました。開発は TypeScript + Nuxt.js でフロントエンドと API を実装し、データベースは Supabase を用いて構築しました。

使用した AI エージェントは Cline と GitHub Copilot（gpt-4o）です。アーキテクチャは「クライアント」→「サーバ（Nuxt）」→「データベース（Supabase）」という三層構造で作成しました。

完成したものは [こちら](https://nomisapo-x.vercel.app/) です。Supabase Auth を使用しているため、利用にはサインアップが必要です。


## 開発の流れ

以下のような流れで開発を行いました。

### 設計
1. 設計書を Markdown で作成する
1. 画面デザインを Figma で作成する
1.  Nuxt.js の環境構築を行う

### バックエンド
1. Supabase MCPの設定をする
1. データベースを作成する
1. データベースにアクセスするAPIを作成する


### フロントエンド
1. Figma MCP で画面を作成する
1. コンポーネントの分割を行う
1. 画面の修正を行う
1. 画面にロジックを実装する


### 設計
#### 1. 設計書を Markdown で作成する
Clineに読み込ませる簡単な設計書を作成します。今回作成したのは以下の項目です：
- 実現したい機能一覧(機能要件)
- 画面設計 (Figma のURL)
- DB設計 (カラムとその型)
- API一覧　(メソッド、URL、パラメータ、説明)

以下は一例です。今回は既存の設計書を流用しましたが、Clineを使って一からしっかり作成しても良いかもしれません。また記載する情報量は最低限に絞りました。


```機能要件.md(一部)
# 要件定義

## 飲み会幹事アプリ

### 機能要件

- 今まで幹事を行った飲み会を管理できる.
- 飲み会の詳細情報を編集できる
- 飲み会の詳細情報を表示できる。さらにユーザの役職に応じて自動的に傾斜を計算することができる
- 傾斜は手動で微調整ができる。傾斜には「緩め」、「普通」、「激しい」の三段階を初期値として選べる。
- 傾斜計算結果は表としてクリップボードにコピーすることができる。
```

```画面設計.md
# 画面設計

figma を用いて実装する。アクセスする際は MCP を用いて、必要な情報を取得すること。

## 飲み会履歴画面
https://www.figma.com/design/hogehoge

## 飲み会詳細画面
https://www.figma.com/design/hogehoge

## 飲み会編集画面
https://www.figma.com/design/hogehoge
```

```DB設計.md(一部)
tb_user
| 項番 | 項目ＩＤ | 項目名 | 型 | 桁数 | 項目説明 | 一意 Key | NULL 値 | 初期値 | 形式 |
| 1 | user_id |ユーザ ID | Char | 10 | 　 | ○ | not null| 　 | | 　 |
| 2 | last_name | 氏 | Varchar | 10 | 　 | | not null | 　 　 | | 　 |
| 3 | first_name | 名 | Varchar | 10 | 　 | | | not null | 　 　 | | 　 |
| 4 | organizer_user_id | 幹事ユーザ ID | Char | 10 | 　 | | not null | 　 | |
```

```API一覧.md(一部)
auth : 認証に関するAPI
| method | url | description | 返値の方注釈 | パラメータ |
| POST | /auth/signup | supabase の認証機能を用いる。supabase の signup をする。| boolean | email:string, password:string |
| POST | /auth/login | supabase の認証機能を用いる。supabase の login をする。 uuid を取得する。| string | |
| GET | /auth/logout | supabase の認証機能を用いる。ログアウトを行う API をたたく| boolean | |
```


#### 2. 画面デザインの Figma を作成する
Figmaで一からデザインを作るのが大変だったため、v0 を使って仮サイトを作成し、それをFigmaに変換しました。変換手順は以下の記事を参考にしました：

https://zenn.dev/dely_jp/articles/5715965001b440 


#### 3. Nuxt.jsの環境構築を行う
Nuxtを使用して開発を進めました。後々使用する可能性のあるライブラリ（Vitest、Storybook、Pinia など）を初期段階でセットアップし、不要な初期ファイルの削除も行いました。開発以外の部分でエラーが出ないよう、事前準備を入念にしています。


### バックエンド
#### 1. Supabase MCPの設定をする。
Supabaseにログイン後、新規プロジェクトを作成します。ダッシュボードの左のメニューバーから「Access Tokens」を選択します。開いた画面の右上に「Generate new token」とあるので、クリックしてトークン名を入力します。最後に「generate token」をクリックすると、トークンが生成されるのでコピーしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/a547d296-95ff-4d75-88c7-cc5194a3b8d0.png)

生成したトークンを使って、以下のように設定ファイルに記述します。これでSupabase MCPが使えるようになりました。
```cline_mcp_settings.json
{
  "mcpServers": {
    "supabase": {
      "timeout": 60,
      "command": "npx",
      "args": [
        "-y",
        "@supabase/mcp-server-supabase@latest",
        "--access-token",
        "************************"
      ],
      "transportType": "stdio",
    }
  }
}
```

#### 2. SupabaseでDBを作成する。
Supabase MCPを使って、設計書（DB設計.md）をもとにデータベースを自動生成します。今回はDB設計.mdを使用します。

```DB設計.md(再掲)
tb_user
| 項番 | 項目ＩＤ | 項目名 | 型 | 桁数 | 項目説明 | 一意 Key | NULL 値 | 初期値 | 形式 |
| 1 | user_id |ユーザ ID | Char | 10 | 　 | ○ | not null| 　 | | 　 |
| 2 | last_name | 氏 | Varchar | 10 | 　 | | not null | 　 　 | | 　 |
| 3 | first_name | 名 | Varchar | 10 | 　 | | | not null | 　 　 | | 　 |
| 4 | organizer_user_id | 幹事ユーザ ID | Char | 10 | 　 | | not null | 　 | |
```

Clineに以下のプロンプトを投げるだけです：
```
DB設計.mdをもとに、Supabase MCPを用いて{project id}にDBを作成してください。
```
このプロンプトだけで簡単にSupabase上にDBを作成することができました。さらに「テストデータを追加してください」というだけで、それぞれでテストデータを追加してくれました。

#### 3. DBにアクセスするAPIを作成する

Nuxtの server ディレクトリにAPIを実装していきます。

##### 型定義
TypeScriptの型として、「フロントエンドで使用する型」と「DBのカラム名に対応する型」を作成します。「フロントエンドで用いる型」については、自力で実装したり、markdownに記載してAIに読み込ませて作成させます。深くネストしていなければAIで十分生成できるので、今回はAIで作成しました。

```型定義.md
# はじめに

ファイルごとに定義する型を記載する。この型はブラウザ側で使用する。
型定義は types ディレクトリに作成する。

# party.ts

## Party

飲み会の情報
partyId: string;
partyName: string;
shopName: string;
eventDate: Date;
totalAmount: number;

```
DB用の型は次のプロンプトで生成しました：
```
DB設計.mdを読んで、DBに対応する型をtypes/db.tsに作成してください。型定義からcreated_atとupdated_atは除いてください。
```
このプロンプトで問題なくDBの型を実装してくれました。今回は9個のテーブルを使用していますが、問題なく作成できます。今回はAIを用いて生成しましたが、Supabaseには専用のCLIがあるのでそちらを使用しても問題ないと思います。

##### Supabaseクライアント部品を作成
Supabaseアクセス用の共通クライアント（`utils/supabaseClient.ts`）を先に作成し、各APIで共通利用させることで生成精度が上がります。Supabaseの設定は .env や nuxt.config.ts に定義します。このような部品を作成して読み込ませないと、各APIごとにクライアントを定義したファイルが作成されやすいです。
Supabaseの情報はあらかじめ、`.env`や`nuxt.config.ts`に定義しておきます。

```utils/supabaseClient.ts
import { createClient } from "@supabase/supabase-js";

const supabaseUrl = useRuntimeConfig().supabaseUrl;
const supabaseAnonKey = useRuntimeConfig().supabaseKey;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

```

##### API生成
API設計（`API一覧.md`）をもとにNuxtの `server/api` ディレクトリにAPIを自動生成させます。OpenAPIなどの一般的なファイルを用いれば確実かもしれませんが、今回はmarkdownにAPIの表とプロンプトを記入しました:

```API設計.md
## はじめに

これは nuxt の server directory に実装する API の一覧です。
対応する api を次のルールに従ってファイルを作成してください

- ファイルはそれぞれの API に対して, server/api/{path}/index.{method}.ts に保存する。
- supabase を用いて、DB にアクセスする。utils/supabaseClient.ts を読み込んで使用する。
- DB の定義は designs/DB 設計.md にあるので、読み込むこと
- DB の型定義は types/db.ts で定義されているので型注釈をする。
- それぞれの API の返値の型注釈は表の指示に従うこと
- 返値と引数の型定義は types/particpant.ts, types/party.ts を用いる。
- DB からデータを取得して、必要なものを関数の返値とすること

## API設計
party :飲み会に関する API
| method | url | description | 返値の方注釈 | パラメータ |
| GET | /party/{party_id} | 飲み会の詳細情報を返す API | Party & Participant[] | userId:string (query)|
| POST | /party | 飲み会情報を新規登録する API | boolean | userId:string & Party & Participant[] (body) |

```
生成された一例：
```server/api/party/[party_id]/index.get.ts
import { defineEventHandler } from "h3";
import { supabase } from "@/utils/supabaseClient";
import { Party } from "@/types/party";
import { Participant } from "@/types/participant";

export default defineEventHandler(async (event) => {
  const query = getQuery(event);
  const userId = query.userId as string;
  const partyId = event.context.params.party_id;

  if (!userId || !partyId) {
    return {
      success: false,
      message: "Missing userId or partyId in query parameters",
    };
  }

  // tb_partyは飲み会情報のテーブル
  const { data: partyData, error: partyError } = await supabase
    .from("tb_party")
    .select("*")
    .eq("party_id", partyId)
    .single();

  if (partyError) {
    return { success: false, message: partyError.message };
  }
  // tb_payment_statusは飲み会参加者ごとの支払い状況テーブル
  const { data: participantData, error: participantError } = await supabase
    .from("tb_payment_status")
    .select("*")
    .eq("party_id", partyId);

  if (participantError) {
    return { success: false, message: participantError.message };
  }

  const party: Party = {
    partyId: partyData.party_id,
    partyName: partyData.party_name,
    shopName: partyData.shop_id,
    eventDate: partyData.event_data,
    totalAmount: partyData.total_amount,
  };

  const participants: Participant[] = participantData.map((participant) => ({
    userId: participant.participant_user_id,
    userName: participant.participant_user_name,
    amount: participant.payment_amount,
    isPaid: participant.payment_status,
  }));

  return { success: true, data: { party, participants } };
});
```
生成した結果を見ると8割くらいは実装ができています。実装できていない点でいえば
- supabaseのテーブルの結果に型注釈をつけていない
- 複数のテーブルにアクセスするときに存在しないカラムを参照してしまう
- Nuxtの関数である`createError`を指示していないためか使用していない

などがあります。基本的に単一のテーブルで完結する処理は問題ないですが、複数のテーブルを結合する必要があるものはうまくできていないです。とはいえは、ほとんどすべてのAPIを少しの修正、ものによっては修正なしで作成できるので非常に便利です。

##### APIの修正
作成したAPIを想定動作をするように修正していきます。今回は複雑な処理がないため、GitHub Copilot と手動で十分対応可能でした。



### フロントエンド


#### 1. Figma MCPで画面を作成する
Figma MCPを使い、Figmaの情報をAIに読み込ませて画面を生成していきます。`画面設計.md`にFigmaのURLを記載しているので、AIに読み込ませてアクセスさせます。
```指示
画面設計.mdを読み込んで、飲み会履歴画面をpages/index.vueに作成してください。
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/c558bc05-7084-4afd-b1b5-f1199d1f23ea.png)


色や一部のコンポーネントが不足しているものの、ほぼ同じ見た目の画面が作成されました。一瞬で作れるので非常に便利です。

以下が生成されたコードです。当然ながら、ロジックはほとんど含まれておらず、見た目中心の実装になっています。
```index.vue
<template>
  <div class="nomikai-history">
    <header class="header">
      <h1>飲み会幹事くん</h1>
      <p>
        飲み会の計画から割り勘計算まで、幹事業務をもっと楽しく、もっと簡単に。
      </p>
      <button @click="createNewNomikai">新しい飲み会を作成</button>
    </header>
    <main class="main">
      <h2>あなたの飲み会一覧</h2>
      <div
        class="nomikai-card"
        v-for="nomikai in nomikaiList"
        :key="nomikai.id"
      >
        <h3>{{ nomikai.title }}</h3>
        <p>{{ nomikai.date }}</p>
        <p>{{ nomikai.location }}</p>
        <p>{{ nomikai.participants }}人参加</p>
        <button @click="viewDetails(nomikai.id)">詳細</button>
        <button @click="editNomikai(nomikai.id)">編集</button>
      </div>
    </main>
  </div>
</template>

<script setup>
import { ref } from "vue";

const nomikaiList = ref([
  {
    id: 1,
    title: "チーム打ち上げ",
    date: "2023年12月15日 19:00",
    location: "居酒屋いろは",
    participants: 3,
  },
  {
    id: 2,
    title: "部署忘年会",
    date: "2023年12月22日 18:30",
    location: "焼肉やまと",
    participants: 2,
  },
]);

const createNewNomikai = () => {
  console.log("新しい飲み会を作成");
};

const viewDetails = (id) => {
  console.log(`飲み会ID ${id} の詳細を表示`);
};

const editNomikai = (id) => {
  console.log(`飲み会ID ${id} を編集`);
};
</script>

<style scoped>
省略
</style>

```

#### 2. コンポーネントの分割を行う
Figma MCPを用いて「画面を作って」と指示すると、すべてのコンポーネントが1つのファイルに実装されてしまいます。これでは扱いにくいため、分割を行います。

分割方法は以下の2つです：
- 手動でファイルを作成
- Clineに依頼する

今回は以下のプロンプトで分割を依頼しました。

```
飲み会履歴画面から飲み会情報カードコンポーネントを分割してください。
```
ロジックが少ないため、正確に分割してくれました。ただし注意点として、index.vue のCSSを編集せずにコンポーネントの部分修正されることがあるため、index.vue で定義されたCSSがコンポーネントにも影響する可能性があります（実際にこの問題が発生しました）。
上記プロンプトにより、以下のようなコンポーネントが生成されました。Props や Emits もある程度実装されています。
```NomikaiCard.vue
<template>
  <div class="nomikai-card">
    <h3>{{ title }}</h3>
    <p>{{ date }}</p>
    <p>{{ location }}</p>
    <p>{{ participants }}人参加</p>
    <button @click="viewDetails">詳細</button>
    <button @click="editNomikai">編集</button>
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from "vue";

defineProps({
  title: String,
  date: String,
  location: String,
  participants: Number,
});

const emit = defineEmits(["view-details", "edit-nomikai"]);

const viewDetails = () => {
  emit("view-details");
};

const editNomikai = () => {
  emit("edit-nomikai");
};
</script>

<style scoped>
省略
</style>

```

#### 3. 画面の修正を行う
生成された画面は細かい部分でFigmaと異なるため、修正を加えていきます。Figmaのデザインが複雑になるほど情報量が増え、AIの生成漏れも増えていきます。
そのため、「より小さなコンポーネントのFigma情報を読み込ませて修正する」といったアプローチで進めました。

##### デザインを修正する
CSSに詳しければ自力で修正できますが、今回は以下のようにAIにプロンプトを投げて修正しました。
生成されたコードとFigmaのデザインを突き合わせて、プロンプトに入力します。たとえば「飲み会幹事くん」をデザインを修正する場合は、ソースコードのh1タグとFigmaの該当コンポーネントのURLを入力します。
```
pages/index.vueのh1をfigma MCPを用いて{figmaの該当コンポーネントのURL}のデザインにしてください。
またassets/beer1.svgとassets/beer2.svgを左右においてください
```

##### 不足しているコンポーネントを追加する
基本は画面の作成方法と同じです。AIに次のプロンプトを投げます。
```
{figmaのURL}を読み込んで、そのコンポーネントを `components/hogehoge.vue`を作成してください。
```
このようにすれば、見た目はほぼ再現されたコンポーネントが作成されます。さらに修正が必要な場合は、より小さなコンポーネントに対して同様の操作を繰り返してブレイクダウンしていきます。
ある程度まで進むと自力とCopilotの力だけで十分修正できる（というかそちらのほうが早く直せる）ので、自力で修正してコンポーネントを完成させました。
完成したコンポーネントは手動で配置していきました。

最終的に次の結果になりました。サイズやpaddingなどの細かいところは再現していませんが、見た目に関してはほとんど一致させるところまで行けました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/adff7664-604d-47dd-8e48-abb0f5900cf5.png)


#### 4. 画面にロジックを実装する
作成した画面にロジックを追加していきます。
基本の流れは以下の通りです。
1. コンポーネント、画面に対して実現していほしい機能を記載したmarkdownを作成する
1. AIにmarkdownを入力して修正してもらう
1. 生成結果を手動またはAIを使って修正する

コンポーネントに関しては、Props、Emitsの情報を記載したmarkdownを作成しました
```NomikaiCard.md
# NomikaiCard.vue

ロジックに関する部分だけを修正してください。

## Props

title:string
location: string
date: string
isCompleted: boolean

## Emit

- view-details: 「詳細」ボタンを押したら実行される。返り値はnull
- edit-nomikai: 「編集」ボタンを押したら実行される。返り値はnull

## Design

すでに実装されている CSS を変更しないこと。

日付は YYYY 年 MM 月 DD 日で表示する。
isComplted が true なら完了、そうでないないなら未完了と表示する。
```
これくらいの単一ファイルで完結するような修正は問題なく修正してくれます。

次に画面に対するプロンプトです。

```飲み会履歴画面.md
# pages/index.vue

playwright で機能に記述した通りの動作をしているか確認してください。
エラーがなくなるまで修正してください。
3 回失敗したら、修正をあきらめて指示者に直させる。

## 使用ライブラリ、外部ファイル

- utils/party.ts を参照すること
- fetch を行うときは utils/fetchHelper.ts の関数を読み込んで、使用すること
- 画面遷移は NuxtLink か vue-router を使用する

# 機能

- ログインしている userId を用いて、計算済み飲み会と未計算済み飲み会を取得して表示する。userId は stores の userStore から取得する。
  - /api/party/calculated, server/api/party/uncalculated に通信する。
  - server/api のディレクトリで通信先のファイルを読み込むこと
- 「新しい飲み会を作成」を押すと 固定の仮の ID が生成されて、飲み会編集画面(/[id]/edit)に遷移する
- 飲み会カードの「詳細」を押すと、その飲み会の詳細画面(/[id]/detail)に遷移する
- 飲み会カードの「編集」を押すと、その飲み会の編集画面(/[id]/edit)に遷移する

```
これをClineに入力して、画面にロジックを組み込んでいきました。基本は普通のAIで生成するのとあまり変わりませんが、playwright MCPを用いると使うと次のような利点がありました。

- 画面へアクセスしたときに発生するエラー対処してくれる
    - サーバを起動して画面にアクセスすると実行時エラーが発生することがありますが、画面のレスポンスからエラーを読み取って対処をしてくれます
- 開発者モードのconsoleやnetworkの内容を確認できる
    - エラーの情報が取得しやすくなります。`console.log`を使ったデバッグやHTTP通信のレスポンスからバグの特定が早くできました



## MCPを使った感想
### Figma MCP
- Figmaの再現率はおおよそ8割。一発でそれなりの画面が作れるのは非常に便利
- ただし、Figmaのデータ量が多くなると安定性が下がる。大きな画面を一気に作るより、小さなコンポーネント単位で分割・生成したほうが確実
-生成される画面は1ファイルに大きなコンポーネントとして実装されるため、扱いづらい
- SVGの扱いは苦手。プロンプト次第かもしれないが、事前にSVGファイルを用意し、パスを明示したほうが安定する印象

### Supabase MCP
- DB設計を渡せば、全部作ってくれるので、非常に便利
- テストデータも勝手に作ってくれる
- 制約の追加などもプロンプトで済むので簡単な開発ならMCP経由だけで十分
- DB関連の操作がプロンプトだけ済むので簡単な開発ならかなり楽にできる

### Playwright MCP
- 実行時のエラーログや、Chromeの開発者ツールのconsoleやnetworkの情報を取得できるので、エラーに関する情報でデバッグがしやすくなる
- 画面に対して直接テストすると複数のファイルが絡んでいるので、AIが勝手に画面以外のファイルを編集してしまい、暴れやすい
- MCPで試した後に、Playwrigthのテストコードを保存するができるので、テストの再現がしやすい

### Context7
- 読み込む前と後で書き方は変わるが、気休め程度でやはりモデルの学習時期のほうがかなり依存する
- ライブラリを適切に検索できないことがあるので、使い方が難しい
    - 例：`vue の defineModel を Context7 で調べて` と指示してもうまく動作しないことが多かった。


### 開発全体を通して
- 開発後半になるにつれて、AIに読み込ませる必要のあるコンテキストが多くなっていくので、読み込ませるのに時間がかかる
    - そのため、開発初めの方は使いやすいがだんだんと使いにくくなる
- 8割程度を作成させてから自分で修正していくのがなんだかんだで一番早い
    - 穴埋め問題を解いている気分になる


## おわりに
AIエージェントを使って初めて開発を行いましたが、開発後半になると「プロンプトを考える時間」> 「自力で修正する時間」になるような気分でした。なので、**開発初めにスタートダッシュを決める**にはいいと思いました。今回はほとんどAIに作らせましたが、似たような実装が必要なる場合はfew shotを用いる方法がいいかもしれません。