FigmaのMCPサーバを作成する

以前に公式ドキュメントを参考に天気のMCPサーバを作成する記事を投稿しましたが、今回はFigmaのデータを取得する簡易的なMCPサーバの作成を試してみます。

FigmaのMCPサーバ作成

MCPサーバのプロジェクトディレクトリを作成して、移動します。

cd プロジェクトディレクトリ

package.jsonを作成します。

npm init -y

使用するパッケージをインストールします。
使用パッケージは前回のサーバ作成と同じです。

npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript

package.json内に以下を追記します。

{
  ~ 略 ~
  "type": "module",
  "bin": {
    "weather": "./build/index.js"
  },
  "scripts": {
    "build": "tsc && chmod 755 build/index.js"
  },
  "files": [
    "build"
  ],
  ~ 略 ~
}

tsconfig.jsonを作成して、以下の内容にします。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

これで作業環境の準備ができたので、実際に作成してみます。
srcディレクトリを作成して、その中に以下の内容でindex.tsファイルを作成します。

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

// Figmaのアクセストークン
const ACCESS_TOKEN = process.env.FIGMA_TOKEN || '';
// アクセストークンが未設定の場合は終了
if (!ACCESS_TOKEN) {
  console.error("Error: Figma access token is not set in environment (FIGMA_TOKEN)");
  process.exit(1);
}
// Figma REST API のベースURL
const API_BASE = "https://api.figma.com";

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

/**
 * APIにリクエストを送って、JSONレスポンスを取得する関数
 */
async function makeFigmaRequest<T>(url: string): Promise<T | null> {
  const headers = {
    "X-Figma-Token": ACCESS_TOKEN
  };

  try {
    const response = await fetch(url, { headers });
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return (await response.json()) as T;
  } catch (error) {
    console.error("Error making Figma request:", error);
    return null;
  }
}

interface FigmaNodesResponse {
  name: string;
  lastModified: string;
  version: string;
  nodes: {
    [key: string]: {
      document: {
        id: string;
        name: string;
        type: string;
        children?: Array<{
          id: string;
          name: string;
          type: string;
        }>;
      }
    }
  };
}

/**
 * FigmaのURLからファイルキーとノードIDを取得するツール
 */
server.tool(
  "get-filekey-and-nodeid-from-url",
  "Get file key and node id from url",
  {
    url: z.string().min(1, "URL is required"),
  },
  ({ url }) => {
    let file_key = '';
    let node_id = '';
    // ファイルキーの取得
    const figmaDomain = 'figma.com';
    const afterDomain = url.split(`${figmaDomain}/`)[1];
    if (afterDomain) {
      const parts = afterDomain.split('/');
      if (parts.length > 1 && (parts[0] === 'file' || parts[0] === 'design')) {
        file_key = parts[1];
      }
    }

    // ノードIDの取得
    const nodeIdParam = 'node-id';
    const afterNodeId = url.split(`${nodeIdParam}=`)[1];
    if (afterNodeId) {
      node_id = afterNodeId.split('&')[0];
    }
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({ file_key, node_id })
        }
      ]
    };
  }
);

/**
 * ノードIDを指定して全体のデータを取得するツール
 */
server.tool(
  "get-node-all-data",
  "Get Figma node data from a file key and node id. By default, set the depth parameter to 5. If the response is too large, reduce the depth value. You may use this tool multiple times if needed.",
  {
    filekey: z
      .string()
      .min(1, "File key is required"),
    nodeid: z.string(),
    depth: z
      .string()
      .regex(/^\d+$/, "Depth must be a number between 1 and 99")
      .optional(),
  },
  async ({ filekey, nodeid, depth = '5' }) => {
    const queryParams = new URLSearchParams();
    if (nodeid) queryParams.append('ids', nodeid);
    queryParams.append('depth', depth);

    const nodesUrl = `${API_BASE}/v1/files/${filekey}/nodes/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
    const fileData = await makeFigmaRequest<FigmaNodesResponse>(nodesUrl);

    if (!fileData) {
      return {
        content: [
          {
            type: "text",
            text: "Failed to fetch Figma data from API."
          }
        ]
      };
    }

    // 必要な情報のみを抽出
    function buildNode(node: any): any {
      if (!node) return null;
      const {
        id,
        name,
        type,
        absoluteBoundingBox,
        fills,
        strokes,
        strokeWeight,
        cornerRadius,
        style,
        characters,
        children
      } = node;
      const result: any = { id, name, type };
      if (absoluteBoundingBox) result.absoluteBoundingBox = absoluteBoundingBox;
      if (fills) result.fills = fills;
      if (strokes) result.strokes = strokes;
      if (typeof strokeWeight !== 'undefined') result.strokeWeight = strokeWeight;
      if (typeof cornerRadius !== 'undefined') result.cornerRadius = cornerRadius;
      if (style) result.style = style;
      if (typeof characters !== 'undefined') result.characters = characters;
      if (children && Array.isArray(children) && children.length > 0) {
        result.children = children.map(buildNode);
      }
      return result;
    }

    // nodeidの区切り調整
    const nodeKey = nodeid.replace(/-/g, ':');
    const rootNode = fileData.nodes[nodeKey]?.document;
    if (!rootNode) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to find node for nodeid: ${nodeid}`
          }
        ]
      };
    }

    const compressedData = {
      name: fileData.name,
      lastModified: fileData.lastModified,
      version: fileData.version,
      document: buildNode(rootNode)
    };

    const jsonText = JSON.stringify(compressedData, null, 2);
    if (jsonText.length > 500_000) {
      return {
        content: [
          {
            type: "text",
            text: `Warning: The Figma file is too large to display in full. (${jsonText.length} bytes)`
          }
        ]
      };
    }

    return {
      content: [
        {
          type: "text",
          text: jsonText
        }
      ]
    };
  }
);

/**
 * MCPサーバのメイン処理
 */
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Figma MCP Server running on stdio");
}

// メイン処理の実行
main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

最後にビルドを実行して、buildディレクトリ内にindex.jsファイルが生成されたらOKです。

npm run build

Cursorの設定

作成したFigmaのMCPサーバをCursorに設定して試してみます。
詳しい設定方法については以前にCursorでMCPを使用する記事を投稿していますので、そちらをご確認ください。

また使用の際にFigmaのアクセストークンが必要になりますので、事前に取得しておいてください。
こちらも以前に記事を投稿していますので、合わせてご確認ください。

mcp.jsonを開いて、下記を追加します。

{
  "mcpServers": {
    ~ 略 ~
    "Figma": {
      "command": "node",
      "args": [
        "\\プロジェクトディレクトリのパス\\build\\index.js"
      ],
      "env": {
        "FIGMA_TOKEN": "Figmaのアクセストークンをここに入れる"
      }
    }
  }
}

Cursor Settings内のTools & Integrations内に緑色で表示されていればOKです。

実際の動作テストとして、以下の質問をしてみます。

次のFigmaデザインデータ内にあるテキストレイヤーのテキストを返答してください。
https://www.figma.com/design/xxxxx/yyyyy

MCPサーバ作成時に用意した、get-filekey-and-nodeid-from-urlとget-node-all-dataのツールの利用許可が確認されます。
Run toolをクリックします。

以下のような、Figmaデータの内容に沿った回答を確認できました。

Figmaデザインのファイル名は「APIテスト用ファイル」です。
データ内にあるテキストレイヤーのテキストは以下の通りです:
「APIテスト用。これはテキストレイヤーで記述しています。

次にボタンのコンポーネントを追加した上で、そのコンポーネントのCSS作成を依頼してみます。

次のボタンコンポーネントのCSSを実装してください。
https://www.figma.com/design/xxxxx/yyyyy?node-id=zz-zz

結果としては以下のように、調整は必要そうですが内容に沿ったCSSを返してくれました。

.button {
  width: 100px;
  height: 47px;
  background: #d9d9d9; /* RGB(217,217,217) */
  border: 1px solid #868686; /* RGB(134,134,134) */
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Noto Sans JP', sans-serif;
  font-size: 16px;
  font-weight: 400;
  color: #000;
  cursor: pointer;
  /* テキストは中央寄せ */
  text-align: center;
  /* テキストの高さ調整 */
  line-height: 19.2px;
  letter-spacing: 0;
  box-sizing: border-box;
}

参考サイト

このエントリーをはてなブックマークに追加

関連記事

コメントを残す

メールアドレスが公開されることはありません。
* が付いている欄は必須項目です

CAPTCHA


コメントが承認されるまで時間がかかります。

2025年8月
 12
3456789
10111213141516
17181920212223
24252627282930
31