🎎

Figma Context MCPを使ってどこまでUIを自動生成できるのか

に公開

はじめに

こんにちは
株式会社TAIANでフロントエンドエンジニアをしています、竹内です

TAIANでは希望するエンジニアにcursorエディタを配布しています。

これまでも社内ドキュメントをもとに生成AIが回答してくれるSlackbotやオンラインミーティングで自動議事録文字起こしサービスを導入するなどAI関連の技術に触れてきました。

私個人としてもcline(+claude 3.7 so-net)を使ってプロダクションコードを書いてもらったり配布後はcursorを利用するなどAIエージェントを活用したコーディングに取り組んできました。

そして今、最も注目しているのがGLips/Figma-Context-MCPです。

UIコンポーネントを配置する作業はMCPを活用して効率化し、画面の状態設計やインタラクション実装にエンジニアが注力できる世界を迎えられると良いと考えています。

Figma-Context-MCPがどういったものかはより詳しい記事に譲るとして、今回はFigma-Context-MCPを使ってどれくらい精度の高いUI実装が行えるのかを見ていきます。

検証環境

今回の検証ではUIライブラリとしてMUIを採用し、そのデザインデータであるMaterial UI for Figma (and MUI X)を使って検証を進めていきます。このデザインデータはMUIによってCC BY 4.0ライセンスの下で公開されています。

ライブラリ等のバージョンは以下になります。
※Figma側の最新バージョンがv6.1.0だったため、MUIのバージョンはそちらを採用しています、

"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^7.0.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"

今回の検証に利用したレポジトリです
https://github.com/Masashige-T/try-figma-mcp-with-mui

導入

まずはcursorでFigma-Context-MCPを利用できるようにします。

Figma Personal access tokensを発行

Grab your Figma access tokenを参考にPATを発行していきます。

PATの権限には Dev resources:readonly File content:readonlyを付与します。

.cursor/mcp.json設定

次に、上記で発行したPATを使ってcursorがFigma-Context-MCPを利用できるようにします。

mkdir .cursor
touch .cursor/mcp.json
# .cursor/mcp.json
{
  "mcpServers": {
    "Framelink Figma MCP": {
      "command": "npx",
      "args": ["-y", "figma-developer-mcp", "--figma-api-key=${FIGMA_PAT}", "--stdio"]
    }
  }
}

これでcursorがFigma-Context-MCPを利用できるようになりました。

試してみる

それでは実際にcursorにUIを実装してもらいましょう。
まずは試しに以下のような簡単なデザインを作成してもらいます

プロンプト

@fimgaデザインURL

このデザインデータをもとに新たにExampleコンポーネントを作成してください

出力

Figmaのデザインデータを確認して、新しいExampleコンポーネントを作成させていただきます。
まず、Figmaのデータを取得して確認します。

Called MCP tool get_figma_data

Figmaのデザインデータを確認しました。これはMaterial UIのカードコンポーネントを使用したブログ投稿のリストを表示するデザインですね。このデザインに基づいて、新しいExampleコンポーネントを作成します。
まず、必要なファイルを作成していきます。

import React from 'react';
import {
  Card,
  CardContent,
  CardMedia,
  Typography,
  Chip,
  Box,
  Grid,
} from '@mui/material';

interface BlogPost {
  id: number;
  author: string;
  date: string;
  title: string;
  description: string;
  tags: string[];
}

const Example: React.FC = () => {
  const blogPosts: BlogPost[] = [
    {
      id: 1,
      author: 'John Doe',
      date: '4 Feb 2022',
      title: 'New feature available on Devias',
      description: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
      tags: ['Security', 'Back-end'],
    },
    {
      id: 2,
      author: 'John Doe',
      date: '4 Feb 2022',
      title: 'New feature available on Devias',
      description: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
      tags: ['Security', 'Back-end'],
    },
    {
      id: 3,
      author: 'John Doe',
      date: '4 Feb 2022',
      title: 'New feature available on Devias',
      description: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
      tags: ['Security', 'Back-end'],
    },
  ];

  return (
    <Box sx={{ p: 10 }}>
      <Grid container spacing={3}>
        {blogPosts.map((post) => (
          <Grid item xs={12} md={4} key={post.id}>
            <Card>
              <CardMedia
                component="img"
                height="140"
                image="/placeholder.png"
                alt="Blog post image"
              />
              <CardContent>
                <Typography variant="body2" color="primary" gutterBottom>
                  {post.author}{post.date}
                </Typography>
                <Typography variant="h6" component="div" gutterBottom>
                  {post.title}
                </Typography>
                <Typography variant="body2" color="text.secondary" paragraph>
                  {post.description}
                </Typography>
                <Box sx={{ display: 'flex', gap: 1 }}>
                  {post.tags.map((tag) => (
                    <Chip
                      key={tag}
                      label={tag}
                      size="small"
                      sx={{ backgroundColor: 'rgba(0, 0, 0, 0.08)' }}
                    />
                  ))}
                </Box>
              </CardContent>
            </Card>
          </Grid>
        ))}
      </Grid>
    </Box>
  );
};

export default Example;

次に、プレースホルダー画像をダウンロードして保存する必要があります。Figmaから画像をダウンロードします。
Invalid type for parameter 'nodes' in tool download_figma_images
申し訳ありません。パラメータの形式を修正して再度試みます。
Invalid type for parameter 'nodes' in tool download_figma_images
申し訳ありません。Figmaからの画像ダウンロードに問題が発生しているようです。この場合、一時的にプレースホルダー画像を使用するようにコンポーネントを修正します。

import React from 'react';
import {
  Card,
  CardContent,
  CardMedia,
  Typography,
  Chip,
  Box,
  Grid,
} from '@mui/material';

interface BlogPost {
  id: number;
  author: string;
  date: string;
  title: string;
  description: string;
  tags: string[];
}

const Example: React.FC = () => {
  const blogPosts: BlogPost[] = [
    {
      id: 1,
      author: 'John Doe',
      date: '4 Feb 2022',
      title: 'New feature available on Devias',
      description: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
      tags: ['Security', 'Back-end'],
    },
    {
      id: 2,
      author: 'John Doe',
      date: '4 Feb 2022',
      title: 'New feature available on Devias',
      description: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
      tags: ['Security', 'Back-end'],
    },
    {
      id: 3,
      author: 'John Doe',
      date: '4 Feb 2022',
      title: 'New feature available on Devias',
      description: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
      tags: ['Security', 'Back-end'],
    },
  ];

  return (
    <Box sx={{ p: 10 }}>
      <Grid container spacing={3}>
        {blogPosts.map((post) => (
          <Grid item xs={12} md={4} key={post.id}>
            <Card>
              <Box
                sx={{
                  height: 140,
                  backgroundColor: 'rgba(0, 0, 0, 0.56)',
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                }}
              >
                <Typography variant="body2" color="white">
                  Image Placeholder
                </Typography>
              </Box>
              <CardContent>
                <Typography variant="body2" color="primary" gutterBottom>
                  {post.author}{post.date}
                </Typography>
                <Typography variant="h6" component="div" gutterBottom>
                  {post.title}
                </Typography>
                <Typography variant="body2" color="text.secondary" paragraph>
                  {post.description}
                </Typography>
                <Box sx={{ display: 'flex', gap: 1 }}>
                  {post.tags.map((tag) => (
                    <Chip
                      key={tag}
                      label={tag}
                      size="small"
                      sx={{ backgroundColor: 'rgba(0, 0, 0, 0.08)' }}
                    />
                  ))}
                </Box>
              </CardContent>
            </Card>
          </Grid>
        ))}
      </Grid>
    </Box>
  );
};

export default Example;

これで、Figmaのデザインに基づいたExampleコンポーネントが作成できました。主な特徴は以下の通りです:
Material UIのコンポーネントを使用
Card
CardContent
Typography
Chip
Grid
Box
レスポンシブデザイン
グリッドシステムを使用して、画面サイズに応じてカードのレイアウトを調整
xs=12(モバイルでは1列)
md=4(デスクトップでは3列)
デザインの詳細
カードの影効果
適切な余白とスペーシング
タイポグラフィの階層
タグのスタイリング
このコンポーネントを使用するには、App.tsxでインポートして使用してください。必要に応じて、スタイルやレイアウトを調整することができます。

実際に実装されたブラウザ表示が以下のようになりました

差分としては

  • 背景がない
  • テキストが中央寄せである
  • 青文字で表示されている部分がFigmaではtypography/subtitle2だがtypography/h6が指定されている
    などがパッとみてわかります。

これらの精度をどう捉えるかは分かれると思いますが、個人的にはかなりの精度で組んでくれているなと思います。

少し興味深いのがFigmaデザインデータでは上下のpaddingを80, 左右を0に指定したものの、実際に実装されたものでは上下左右に80pxのpaddingが設けられたことです

少し複雑なUIを実装してみる

次に、コンポーネントを組み合わせて簡単なTODOアプリのデザインを作成しました

プロンプト

@FigmaURL

このデザインデータをもとに現在のプロジェクトにタスク管理機能を新たに作成してください

出力

Figmaのデザインデータを確認し、タスク管理機能を実装していきましょう。まず、Figmaのデータを取得して分析します。
Figmaのデザインデータを確認しました。タスク管理機能を実装するために、以下のコンポーネントを作成していきます:
タスク追加フォーム
タスクリスト表示
タスクの完了/削除機能
まず、必要なコンポーネントを作成していきましょう。

import { useState } from 'react';
import {
  Card,
  CardHeader,
  CardContent,
  CardActions,
  TextField,
  Button,
} from '@mui/material';

interface TodoFormProps {
  onAddTodo: (text: string) => void;
}

export const TodoForm = ({ onAddTodo }: TodoFormProps) => {
  const [text, setText] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (text.trim()) {
      onAddTodo(text.trim());
      setText('');
    }
  };

  return (
    <Card>
      <CardHeader title="新規TODOを作成する" />
      <form onSubmit={handleSubmit}>
        <CardContent>
          <TextField
            fullWidth
            label="todo"
            value={text}
            onChange={(e) => setText(e.target.value)}
            variant="standard"
          />
        </CardContent>
        <CardActions>
          <Button type="submit" variant="contained" color="primary">
            作成
          </Button>
        </CardActions>
      </form>
    </Card>
  );
};

次に、タスクリストのコンポーネントを作成します:

import { Box, Button, Typography } from '@mui/material';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoListProps {
  todos: Todo[];
  onComplete: (id: number) => void;
  onDelete: (id: number) => void;
}

export const TodoList = ({ todos, onComplete, onDelete }: TodoListProps) => {
  return (
    <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
      {todos.map((todo) => (
        <Box
          key={todo.id}
          sx={{
            display: 'flex',
            alignItems: 'center',
            gap: 1,
            p: 2,
            border: '1px solid #e0e0e0',
            borderRadius: 1,
          }}
        >
          <Typography
            sx={{
              flexGrow: 1,
              textDecoration: todo.completed ? 'line-through' : 'none',
              color: todo.completed ? 'text.secondary' : 'text.primary',
            }}
          >
            {todo.text}
          </Typography>
          {!todo.completed && (
            <Button
              variant="contained"
              color="success"
              size="small"
              onClick={() => onComplete(todo.id)}
            >
              完了
            </Button>
          )}
          <Button
            variant="contained"
            color="error"
            size="small"
            onClick={() => onDelete(todo.id)}
          >
            削除
          </Button>
        </Box>
      ))}
    </Box>
  );
};

最後に、メインのTodoページコンポーネントを作成します:

import { useState } from 'react';
import { Container, AppBar, Toolbar, Typography } from '@mui/material';
import { TodoForm } from '../components/TodoForm';
import { TodoList } from '../components/TodoList';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

export const Todo = () => {
  const [todos, setTodos] = useState<Todo[]>([]);

  const handleAddTodo = (text: string) => {
    const newTodo: Todo = {
      id: Date.now(),
      text,
      completed: false,
    };
    setTodos([...todos, newTodo]);
  };

  const handleComplete = (id: number) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: true } : todo
      )
    );
  };

  const handleDelete = (id: number) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  return (
    <>
      <AppBar position="static">
        <Toolbar>
          <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
            Todo
          </Typography>
        </Toolbar>
      </AppBar>
      <Container maxWidth="md" sx={{ mt: 4 }}>
        <TodoForm onAddTodo={handleAddTodo} />
        <TodoList
          todos={todos}
          onComplete={handleComplete}
          onDelete={handleDelete}
        />
      </Container>
    </>
  );
};

これで、Figmaのデザインに基づいたタスク管理機能が実装できました。主な機能は以下の通りです:
新規タスクの追加
タスクの完了状態の切り替え
タスクの削除
タスクリストの表示

実装されたのがこちらです

デザインだけでなく実際にTODOリストとして動くように出力してくれました。

差分としては

  • 画面幅が極端に小さい
  • 背景色がない
  • タスク完了操作部分に白枠がある
    といったところでしょうか

もう少し詳細な指示を加える

流石にプロンプトが雑すぎましたね。
今度は先ほど差分に感じた点をプロンプトに付け加えて再度生成し直してもらいましょう

プロンプト

@https://www.figma.com/design/B9OrHwzgZPWR3lSD5Eq6kH/Material-UI-for-Figma--and-MUI-X---Community-?node-id=8624-8018&t=CDceNirSEV471utg-4

このデザインデータをもとに現在のプロジェクトにタスク管理機能を新たに作成してください。

以下の情報も参考にしてください

  • ページの横幅100dvwとする
  • コンテンツの最大幅は1280pxとする
  • 背景色を定義する
出力

Figmaのデザインデータを確認し、タスク管理機能を実装していきましょう。まず、Figmaのデータを取得します。
Figmaのデザインデータを確認しました。タスク管理機能を実装するために、以下のコンポーネントを作成していきます:
タスク追加フォーム
タスクリスト
タスクアイテム
まず、必要なコンポーネントを作成していきましょう。

import { useState } from 'react';
import {
  Card,
  CardHeader,
  CardContent,
  CardActions,
  TextField,
  Button,
} from '@mui/material';

interface TodoFormProps {
  onAddTodo: (text: string) => void;
}

export const TodoForm = ({ onAddTodo }: TodoFormProps) => {
  const [text, setText] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (text.trim()) {
      onAddTodo(text.trim());
      setText('');
    }
  };

  return (
    <Card sx={{ maxWidth: 1280, width: '100%', mx: 'auto' }}>
      <CardHeader title="新規TODOを作成する" />
      <form onSubmit={handleSubmit}>
        <CardContent>
          <TextField
            fullWidth
            label="todo"
            value={text}
            onChange={(e) => setText(e.target.value)}
            variant="standard"
          />
        </CardContent>
        <CardActions>
          <Button type="submit" variant="contained" color="primary">
            作成
          </Button>
        </CardActions>
      </form>
    </Card>
  );
};

次に、タスクアイテムコンポーネントを作成します。

import { Stack, Typography, Button } from '@mui/material';

interface TodoItemProps {
  text: string;
  onComplete: () => void;
  onDelete: () => void;
}

export const TodoItem = ({ text, onComplete, onDelete }: TodoItemProps) => {
  return (
    <Stack
      direction="row"
      alignItems="center"
      spacing={2}
      sx={{ width: '100%', maxWidth: 1280, mx: 'auto' }}
    >
      <Typography variant="body1" sx={{ flexGrow: 1 }}>
        {text}
      </Typography>
      <Button
        variant="contained"
        color="success"
        onClick={onComplete}
        size="small"
      >
        完了
      </Button>
      <Button
        variant="contained"
        color="error"
        onClick={onDelete}
        size="small"
      >
        削除
      </Button>
    </Stack>
  );
};

最後に、メインのTodoコンポーネントを作成します。

import { useState } from 'react';
import { AppBar, Toolbar, Typography, Container, Stack } from '@mui/material';
import { TodoForm } from './TodoForm';
import { TodoItem } from './TodoItem';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

export const Todo = () => {
  const [todos, setTodos] = useState<Todo[]>([]);

  const handleAddTodo = (text: string) => {
    setTodos([
      ...todos,
      {
        id: Date.now(),
        text,
        completed: false,
      },
    ]);
  };

  const handleCompleteTodo = (id: number) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const handleDeleteTodo = (id: number) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  return (
    <>
      <AppBar position="static">
        <Toolbar>
          <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
            Todo
          </Typography>
        </Toolbar>
      </AppBar>
      <Container maxWidth={false} sx={{ py: 4, bgcolor: '#f5f5f5', minHeight: '100vh' }}>
        <Stack spacing={3} sx={{ width: '100%' }}>
          <TodoForm onAddTodo={handleAddTodo} />
          {todos.map((todo) => (
            <TodoItem
              key={todo.id}
              text={todo.text}
              onComplete={() => handleCompleteTodo(todo.id)}
              onDelete={() => handleDeleteTodo(todo.id)}
            />
          ))}
        </Stack>
      </Container>
    </>
  );
};

生成されたのがこちらです

先ほど追加したプロンプトの中で、ページの横幅100dvwとする辺りは反映できていなさそうです。

実際にcursorに取得できた横幅について聞くと

"layout_F7FLQV": {
  "mode": "column",
  "gap": 80,
  "padding": "64px 0px 0px",
  "sizing": {
    "horizontal": "fixed",
    "vertical": "fixed"
  }
}

という形式で、横幅が固定値であるという情報しか取得できていなさそうです。
figma側では1280pxと指定してるので、改善の余地がありそうですね。

まとめ

一旦ここまででFigma-Context-MCPを使ってUIの生成が行えることを確かめてきました。
今回つっこみたくなるほど破綻していないのはインターネット上に多くの知見が存在するMUIを採用していることと、Figma側でとても充実したUI Kitを用いたことが大きいと思います。

しかし、幅がきちんと定義されていなかったことやプロンプトに含めなければ背景色が定義されていないなど、どのような情報がFigma-Context-MCPから提供されているのかを理解した上でプロンプトを調整していくことや、MUIをカスタマイズした場合その内容を理解させるために.cursor/rulesを充実させるなど通常のAIエージェントとの向き合い方は重要になりそうです。

このあとはFigma-Context-MCPの実装を読みどのような情報がやり取りされているのか詳しく見ていき自社で作成しているFigmaとReactコンポーネントを使って精度を高められないか探っていきたいと思います。

We are hiring!

TAIANでは、このような開発・技術・思想に向き合い、未来をつくる仲間を一人でも多く探しています。少しでも興味を持っていただいた方は弊社の紹介ページをご覧ください。

https://taian-inc.notion.site/Entrance-Book-for-Engineer-1829555d9582804cad9ff48ad6cc3605

TAIANテックブログ

Discussion