🎨

Figma MCP を使って Production Ready な Flutter コードを生成する

に公開

こんにちは。@shanonim です。
デザインからコードを自動生成する技術は、太古の昔から試行錯誤が繰り返されてきた夢の技術ですが、生成AIとMCP (Model Context Protocol) の登場によって大きく進化しています。

特に Figma が公開している Dev Mode MCPサーバー (以下、Figma MCP) は、Claude などのAIアシスタントと連携することで、高品質なコードを生成できる画期的なツールとして注目されています。
Figma MCP公開当初は「意図した Widget 構造にならない」「命名が分かりにくい」といった課題があり、なかなか開発現場に浸透しなかった印象がありますが、最新のオープンベータ版ではツールとプロンプトが以前より充実し、アウトプットされるコードの品質が向上しています。

この進化を活かし、効率よく Production Ready なコードを生成するためには、もう少しだけ工夫が必要です。
本記事では、「MCP friendly な Figma Design」と「プロジェクト固有の Context 設定」という2つの観点から、Figma MCP連携によって自動生成されるコードの品質をさらに高めるための実践的なアプローチを紹介します。

🤝 Figma MCP と Cursor の連携

今回は、Figma MCPの連携先として Cursor を使用します。
サンプルとして簡単な電話アプリの Figma Design を作成しました。

Figma と Cursor それぞれで連携の設定をします。
Ref: https://developers.figma.com/docs/figma-mcp-server/local-server-installation/#cursor


Figma 上で対象のレイヤーを選択してリンクをコピーします。そのURLを Cursor のAIエージェントに渡すことで、Figma MCPを使用したデザイン解析が始まります。
今回の例では、コード情報を解析する get_design_context と、画像からUI情報を解析する get_screenshot の2つのツールが実行されました。

実際にアウトプットされたコードを以下に示します。
デザインが ListView BottomNavigation Floating Action Button といった Component を使っていることを理解し、それらを的確に実装しています。

class VoipHomePage extends StatefulWidget {
  const VoipHomePage({super.key});

  
  State<VoipHomePage> createState() => _VoipHomePageState();
}

class _VoipHomePageState extends State<VoipHomePage> {
  int _selectedIndex = 0;

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // ヘッダー
            const Padding(
              padding: EdgeInsets.fromLTRB(24, 36, 24, 12),
              child: Text(
                'Recents',
                style: TextStyle(
                  fontSize: 30,
                  fontWeight: FontWeight.w500,
                  color: Color(0xFF0A0A0A),
                  letterSpacing: 0.4,
                ),
              ),
            ),
            const Divider(height: 1, thickness: 1, color: Color(0xFFE5E7EB)),
            // 通話履歴リスト
            Expanded(
              child: ListView(
                padding: EdgeInsets.zero,
                children: [
                  _buildCallItem(
                    name: 'John Smith',
                    time: '10:30 AM',
                    isMissed: false,
                  ),
                  _buildCallItem(
                    name: 'Sarah Johnson',
                    time: '9:15 AM',
                    isMissed: true,
                  ),
                  _buildCallItem(
                    name: 'Mike Wilson',
                    time: 'Yesterday',
                    isMissed: false,
                  ),
                  _buildCallItem(
                    name: 'Emily Davis',
                    time: 'Yesterday',
                    isMissed: false,
                  ),
                  _buildCallItem(
                    name: 'Robert Brown',
                    time: 'Monday',
                    isMissed: false,
                  ),
                  _buildCallItem(
                    name: 'Lisa Anderson',
                    time: 'Monday',
                    isMissed: true,
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
      // ボトムナビゲーションバー
      bottomNavigationBar: Container(
        decoration: const BoxDecoration(
          border: Border(
            top: BorderSide(color: Color(0xFFE5E7EB), width: 1),
          ),
        ),
        child: BottomNavigationBar(
          currentIndex: _selectedIndex,
          onTap: (index) {
            setState(() {
              _selectedIndex = index;
            });
          },
          type: BottomNavigationBarType.fixed,
          selectedItemColor: const Color(0xFF2B7FFF),
          unselectedItemColor: const Color(0xFF6A7282),
          selectedFontSize: 12,
          unselectedFontSize: 12,
          backgroundColor: Colors.white,
          elevation: 0,
          items: const [
            BottomNavigationBarItem(
              icon: Icon(Icons.access_time),
              label: 'Recents',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.people_outline),
              label: 'Contacts',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.dialpad),
              label: 'Keypad',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.settings_outlined),
              label: 'Settings',
            ),
          ],
        ),
      ),
      // フローティングアクションボタン
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        backgroundColor: const Color(0xFF00C950),
        elevation: 4,
        child: const Icon(
          Icons.phone,
          color: Colors.white,
          size: 28,
        ),
      ),
    );
  }

  Widget _buildCallItem({
    required String name,
    required String time,
    required bool isMissed,
  }) {
    // 省略
  }
}

🎨 MCP friendly な Figma Design

前述の状態でも十分に動作するコードですが、さらに品質を向上させるために Figma Design 側の構造を見直してみます。

先ほどコード生成したデザイン (Before デザイン) は、見た目を作るためだけに単純な Component を積んだレイヤー構造でした。[1]
After デザインでは、すべての Component に AutoLayout を設定し Size を明記、命名をわかりやすくアップデートしています。

Before デザインのレイヤー構造 After デザインのレイヤー構造

After デザインで再度コード生成を行った結果の Diff を以下に抜粋します。
より適切な Widget が採用される、Column/Row に適切な Alignment が設定される等のアップデートが確認できました。



⚙️ プロジェクト固有の Context 設定

Flutter プロジェクトでは、以下のような横断的なライブラリやアーキテクチャパターンを採用することが一般的です。

  • 状態管理: Riverpo, Provider, Bloc など
  • イミュータブルなデータクラス: freezed
  • ルーティング: go_router, auto_route など
  • その他: flutter_hooks, retrofit など

これらの情報を事前にAIに伝えることで、生成されるコードがプロジェクトのアーキテクチャに沿ったものになります。

Cursor の場合、Cursor Settings -> Rule, Memories, Commands のセクションから Project Rules を設定することが可能です。

Figma MCPに関する rule に加え、アプリで使用する主要なライブラリの使い方に関する記述を追加します。

---
description: Figma Dev Mode MCPルール
globs:
alwaysApply: true
---

## Figma MCP アセット
- Figma Dev Mode MCP サーバーが提供する画像/SVGのローカルホストソースを直接使用すること
- 新しいアイコンパッケージをインポートしない(すべてFigmaペイロードに含まれる)
- プレースホルダーを使用/作成しない

## Flutter 開発スタック

### 状態管理: Riverpod
- すべての状態管理に Riverpod(hooks_riverpod)を使用
- Provider の種類: `Provider`, `StateProvider`, `StateNotifierProvider`, `FutureProvider`, `StreamProvider`
- ConsumerWidget または HookConsumerWidget を使用(StatelessWidget の代わり)
- ref.watch() でリアクティブに監視、ref.read() でイベントハンドラー内で読み取り
- パラメータ付きは `.family`、自動クリーンアップは `.autoDispose` 修飾子を使用

### React Hooks: flutter_hooks
- HookWidget または HookConsumerWidget を使用(StatefulWidget の代わり)
- よく使う Hooks: `useState`, `useEffect`, `useMemoized`, `useTextEditingController`, `useAnimationController`
- Hooks は build メソッド内でのみ、条件分岐/ループ内では呼び出さない、常に同じ順序で呼び出す

### イミュータブルデータ: freezed
- すべてのデータモデルに freezed を使用
- json_serializable と組み合わせて JSON シリアライゼーション
- Union types で複数の状態を表現(initial/loading/loaded/error など)
- copyWith, ==, hashCode は自動生成される

### コード生成
- `flutter pub run build_runner build` でコード生成
- 開発中は `flutter pub run build_runner watch` を使用
- `*.freezed.dart` と `*.g.dart` は自動生成なので編集しない

### ベストプラクティス
- ConsumerWidget/HookConsumerWidget を優先使用
- プロバイダーは単一責任で小さく保つ
- freezed でイミュータブルな状態管理
- `select` で部分的な再ビルド制御、`autoDispose` で自動破棄

この rule を設定した状態で、再度コード生成を行った結果を一部抜粋します。ライブラリ固有の記法がコード生成時に反映されていることが確認できました。

🎯 まとめ

Figma MCPをより活用するために、Design 側の構造改善と Context 設定の2つのアプローチを紹介しました。
今回はシンプルなUIデザインで検証しましたが、複雑なUIの場合でも適切にレイヤーを分離して Widget 毎にコードを生成すれば、高い精度でUI構築が可能です。

正式リリースに向けた、さらなる Figma MCP の進化も楽しみですね。
うまく使いこなしながらデザインと実装のサイクルを加速させていきましょう🚀

🔖 参考リンク

https://help.figma.com/hc/ja/articles/32132100833559-Dev-Mode-MCPサーバー利用ガイド
https://cursor.com/ja/docs/context/mcp
https://docs.cursor.com/ja/context/rules

📢 [PR] We're Hiring

IVRy では「イベントや最新ニュース、募集ポジションの情報を受け取りたい」「会社について詳しく話を聞いてみたい」といった方に向けて、キャリア登録やカジュアル面談の機会をご用意しています。
ご興味をお持ちいただけた方は、ぜひ以下のページよりご登録・お申し込みください。
https://ivry-jp.notion.site/209eea80adae800483a9d6b239281f1b

脚注
  1. 実際の現場でこのような単純なデザイン構造はあまりないかもしれませんが、ここでは Before/After をわかりやすくするために単純化しています。 ↩︎

IVRyテックブログ

Discussion