Skip to main content
Glama
ArchimedesCrypto

Figma MCP Server with Chunking

get_file_nodes

Retrieve specific nodes from a Figma file using node IDs, enabling efficient access to design elements even in large files managed by the Figma MCP Server with Chunking.

Instructions

Get specific nodes from a Figma file

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
file_keyYesFigma file key
idsYesArray of node IDs to retrieve

Implementation Reference

  • Core handler implementation for fetching specific Figma file nodes. Handles API calls, single/multiple node logic, child pagination (cursor/pageSize for single nodes), depth traversal, node type filtering, property exclusion, and summarization to manage response size.
    async getFileNodes(fileKey: string, ids: string[], options: {
      pageSize?: number;
      maxResponseSize?: number;
      cursor?: string;
      depth?: number;
      nodeTypes?: string[];
      excludeProps?: string[];
      summarizeNodes?: boolean;
    } = {}) {
      try {
        // Input validation
        if (!fileKey || typeof fileKey !== 'string' || fileKey.trim() === '') {
          throw new Error('fileKey must be a non-empty string');
        }
        if (!ids || !Array.isArray(ids) || ids.length === 0) {
          throw new Error('ids must be a non-empty array');
        }
        
        // Basic ID validation - Figma API will handle the rest
        for (const id of ids) {
          if (!id || typeof id !== 'string') {
            throw new Error('All node IDs must be non-empty strings');
          }
        }
        
        if (process.env.MCP_DEBUG) {
          console.debug('[MCP Debug] Getting nodes for file:', fileKey, 'Options:', options);
        }
        
        const {
          pageSize = 50,
          maxResponseSize = 50,
          cursor,
          depth = 2,
          nodeTypes,
          excludeProps,
          summarizeNodes = false
        } = options;
        
        // Validate options
        if (pageSize !== undefined) {
          if (typeof pageSize !== 'number' || pageSize < 1 || pageSize > 1000) {
            throw new Error('pageSize must be between 1 and 1000');
          }
        }
        
        if (depth !== undefined) {
          if (typeof depth !== 'number' || depth < 0 || depth > 10) {
            throw new Error('depth must be between 0 and 10');
          }
        }
        
        if (maxResponseSize !== undefined) {
          if (typeof maxResponseSize !== 'number' || maxResponseSize < 1 || maxResponseSize > 100) {
            throw new Error('maxResponseSize must be between 1 and 100 MB');
          }
        }
        
        if (excludeProps && excludeProps.length > 0) {
          const criticalProps = ['id', 'type', 'name'];
          for (const prop of excludeProps) {
            if (criticalProps.includes(prop)) {
              throw new Error(`Cannot exclude critical property: ${prop}`);
            }
          }
        }
        
    
        // For single node requests, implement child pagination
        if (ids.length === 1) {
          const nodeId = ids[0];
          
          // Step 1: Fetch the parent node with depth=1 to get immediate children list
          // We always fetch parent with depth=1 first for pagination to work
          let shallowResponse;
          try {
            shallowResponse = await this.client.get(`/files/${fileKey}/nodes`, {
              params: { 
                ids: nodeId,
                depth: 1 // Shallow fetch to get list of immediate children IDs
              }
            });
          } catch (error: any) {
            if (error.response?.status === 413 || error.response?.status === 504) {
              throw new Error(
                `Node is too large to fetch all at once. ` +
                `Try these parameters to paginate through its children:\n` +
                `- pageSize: 10 (fetch 10 children at a time)\n` +
                `- cursor: "0" (start from first child)\n` +
                `- depth: 1 (immediate children only)\n` +
                `- summarizeNodes: true (minimal data)\n` +
                `- excludeProps: ["fills", "effects", "strokes"] (remove heavy properties)`
              );
            }
            throw error;
          }
          
          if (!shallowResponse.data || !shallowResponse.data.nodes) {
            throw new Error('Invalid response from Figma API');
          }
          
          let mainNode = shallowResponse.data.nodes[nodeId];
          if (!mainNode) {
            throw new Error('Requested node not found');
          }
          
          // Handle Figma's document wrapper - sometimes nodes come wrapped in a 'document' object
          // Clone to avoid mutations
          const actualNode = { ...(mainNode.document || mainNode) };
          
          // Step 2: Extract children IDs (safely)
          const allChildIds: string[] = [];
          if (actualNode.children) {
            if (!Array.isArray(actualNode.children)) {
              console.warn('[MCP Warning] Node children property is not an array');
            } else {
              for (const child of actualNode.children) {
                if (child && typeof child === 'object' && child.id && typeof child.id === 'string') {
                  allChildIds.push(child.id);
                }
              }
            }
          }
          
          if (process.env.MCP_DEBUG) {
            console.debug(`[MCP Debug] Found ${allChildIds.length} children`);
          }
          
          // Step 3: Paginate through children
          const startIndex = cursor ? parseInt(cursor, 10) : 0;
          if (isNaN(startIndex) || startIndex < 0) {
            throw new Error(`Invalid cursor value: ${cursor}. Must be a non-negative number.`);
          }
          if (startIndex >= allChildIds.length) {
            // Cursor is beyond the last child - return empty result with hasMore=false
            return {
              nodes: { [nodeId]: actualNode },
              pagination: {
                totalChildren: allChildIds.length,
                fetchedChildren: 0,
                nextCursor: undefined,
                hasMore: false,
                message: allChildIds.length === 0 
                  ? 'Node has no children' 
                  : `Cursor beyond range (max: ${allChildIds.length - 1})`
              }
            };
          }
          const safePageSize = pageSize; // Already validated above
          const endIndex = Math.min(startIndex + safePageSize, allChildIds.length);
          const childIdsToFetch = allChildIds.slice(startIndex, endIndex);
          
          let childrenData: any = {};
          
          // Step 4: Fetch the selected page of children (if any)
          if (childIdsToFetch.length > 0) {
            const childParams: any = { 
              ids: childIdsToFetch.join(',')
            };
            // Apply depth to children fetch (depth applies to how deep we fetch each child)
            // depth=1 means fetch children with their immediate children
            // depth=2 means fetch children with 2 levels of descendants, etc.
            if (typeof depth === 'number' && depth >= 0) {
              childParams.depth = depth;
            }
            
            let childResponse;
            try {
              childResponse = await this.client.get(`/files/${fileKey}/nodes`, {
                params: childParams,
                timeout: 30000 // 30 second timeout
              });
            } catch (childError: any) {
              console.error('[MCP Error] Failed to fetch children:', childError.message);
              // Return partial result with parent only
              return {
                nodes: { [nodeId]: actualNode },
                pagination: {
                  totalChildren: allChildIds.length,
                  fetchedChildren: 0,
                  nextCursor: startIndex.toString(),
                  hasMore: true,
                  error: 'Failed to fetch children, returning parent only'
                }
              };
            }
            
            if (!childResponse.data || !childResponse.data.nodes) {
              console.warn('[MCP Warning] Invalid response for child nodes, using empty object');
              childrenData = {};
            } else {
              childrenData = childResponse.data.nodes;
            }
            
            // Handle document wrapper for children too
            for (const [childId, childNode] of Object.entries(childrenData)) {
              if ((childNode as any).document) {
                childrenData[childId] = (childNode as any).document;
              }
            }
          }
          
          // Step 5: Combine parent and children nodes
          // Note: We don't modify the parent's children array - it shows all children IDs
          // The fetched children are separate entries in the nodes object
          let processedData: any = {
            [nodeId]: actualNode, // Parent node (already has full children array from depth=1 fetch)
            ...childrenData       // Fetched children (based on pagination)
          };
          
          // Filter by node types if specified
          // Note: Parent node is always kept regardless of its type (for context)
          if (nodeTypes && nodeTypes.length > 0) {
            const filteredData: any = {
              [nodeId]: processedData[nodeId] // Always keep parent for context
            };
            for (const [id, node] of Object.entries(processedData)) {
              if (id !== nodeId) {
                const nodeType = (node as any).type;
                if (nodeType && nodeTypes.includes(nodeType)) {
                  filteredData[id] = node;
                }
              }
            }
            processedData = filteredData;
          }
          
          // Exclude properties if specified
          if (excludeProps && excludeProps.length > 0) {
            const cleanedData: any = {};
            for (const [id, node] of Object.entries(processedData)) {
              cleanedData[id] = this.nodeProcessor.filterNodeProperties(node as SceneNode, excludeProps);
            }
            processedData = cleanedData;
          }
          
          // Apply summarization if requested
          if (summarizeNodes) {
            const summarizedData: any = {};
            for (const [id, node] of Object.entries(processedData)) {
              summarizedData[id] = this.nodeProcessor.summarizeNode(node as SceneNode);
            }
            processedData = summarizedData;
          }
          
          // Check response size (with safety for large objects)
          let responseSize = 0;
          try {
            responseSize = JSON.stringify(processedData).length / (1024 * 1024); // in MB
            if (responseSize > maxResponseSize) {
              console.warn(`[MCP Warning] Response size ${responseSize.toFixed(2)}MB exceeds max ${maxResponseSize}MB`);
            }
          } catch (e) {
            console.warn('[MCP Warning] Response too large to calculate size');
          }
          
          return {
            nodes: processedData,
            pagination: {
              totalChildren: allChildIds.length,
              fetchedChildren: childIdsToFetch.length,
              nextCursor: endIndex < allChildIds.length ? endIndex.toString() : undefined,
              hasMore: endIndex < allChildIds.length
            }
          };
        }
        
        // For multiple nodes, fetch them all at once (no child pagination)
        
        // Warn if pagination parameters are provided with multiple nodes
        if (cursor || (pageSize && pageSize !== 50)) {
          console.warn(
            '[MCP Warning] Pagination parameters (pageSize, cursor) are ignored when fetching multiple nodes. ' +
            'Pagination only works when fetching a single node (to paginate through its children).'
          );
        }
        
        const params: any = { ids: ids.join(',') };
        if (typeof depth === 'number') params.depth = depth;
        
        const response = await this.client.get(`/files/${fileKey}/nodes`, {
          params,
          timeout: 30000 // 30 second timeout
        });
        
        if (!response.data || !response.data.nodes) {
          throw new Error('Invalid response from Figma API');
        }
        
        let nodeData = response.data.nodes;
        
        // Handle document wrapper for multiple nodes
        for (const [id, node] of Object.entries(nodeData)) {
          if ((node as any).document) {
            nodeData[id] = (node as any).document;
          }
        }
        
        // Apply filters and transformations
        if (nodeTypes && nodeTypes.length > 0) {
          const filteredNodes: any = {};
          for (const [id, node] of Object.entries(nodeData)) {
            const nodeType = (node as any).type;
            if (nodeType && nodeTypes.includes(nodeType)) {
              filteredNodes[id] = node;
            }
          }
          nodeData = filteredNodes;
        }
        
        if (excludeProps && excludeProps.length > 0) {
          const processedNodes: any = {};
          for (const [id, node] of Object.entries(nodeData)) {
            processedNodes[id] = this.nodeProcessor.filterNodeProperties(node as SceneNode, excludeProps);
          }
          nodeData = processedNodes;
        }
        
        if (summarizeNodes) {
          const summarizedNodes: any = {};
          for (const [id, node] of Object.entries(nodeData)) {
            summarizedNodes[id] = this.nodeProcessor.summarizeNode(node as SceneNode);
          }
          nodeData = summarizedNodes;
        }
        
        // Return with clear indication that pagination doesn't apply
        const result: any = {
          nodes: nodeData
        };
        
        // Only include pagination info if user tried to use pagination params
        if (cursor || (pageSize && pageSize !== 50)) {
          result.pagination = {
            warning: 'Pagination parameters were ignored. Pagination only works when fetching a single node.',
            explanation: 'To paginate: fetch one node at a time, use pageSize and cursor to navigate through its children.'
          };
        }
        
        return result;
      } catch (error: any) {
        // Sanitize error messages
        const message = error.message || 'Unknown error';
        const sanitized = message.substring(0, 500); // Limit error message length
        console.error('[MCP Error] Failed to get file nodes:', sanitized);
        throw new Error(`Failed to get nodes: ${sanitized}`);
      }
    }
  • src/index.ts:220-290 (registration)
    Tool registration in ListToolsRequestSchema handler, including name, description, and detailed inputSchema defining parameters like file_key, ids, pageSize, cursor, depth, etc.
    {
      name: 'get_file_nodes',
      description: 'Get specific nodes from a Figma file. IMPORTANT: Pagination (pageSize/cursor) ONLY works when fetching a SINGLE node - it paginates through that node\'s children. When fetching multiple nodes, pagination parameters are ignored. For large nodes use: pageSize: 10-25, summarizeNodes: true, depth: 1.',
      inputSchema: {
        type: 'object',
        properties: {
          file_key: {
            type: 'string',
            description: 'Figma file key'
          },
          ids: {
            type: 'array',
            items: {
              type: 'string'
            },
            description: 'Array of node IDs to retrieve'
          },
          pageSize: {
            type: 'number',
            description: '[SINGLE NODE ONLY] Number of child nodes to fetch per page. Ignored when fetching multiple nodes.',
            minimum: 1,
            maximum: 1000
          },
          maxResponseSize: {
            type: 'number',
            description: 'Maximum response size in MB (defaults to 50)',
            minimum: 1,
            maximum: 100
          },
          cursor: {
            type: 'string',
            description: '[SINGLE NODE ONLY] Pagination cursor - child index to start from (e.g., "0", "50", "100"). Ignored when fetching multiple nodes.'
          },
          depth: {
            type: 'number',
            description: 'Maximum depth to traverse in the node tree',
            minimum: 1
          },
          nodeTypes: {
            type: 'array',
            items: {
              type: 'string',
              enum: [
                'FRAME',
                'GROUP',
                'VECTOR',
                'BOOLEAN_OPERATION',
                'STAR',
                'LINE',
                'TEXT',
                'COMPONENT',
                'INSTANCE'
              ]
            },
            description: 'Filter nodes by type. For single nodes: filters children only (parent always kept). For multiple nodes: filters all requested nodes.'
          },
          excludeProps: {
            type: 'array',
            items: {
              type: 'string'
            },
            description: 'Properties to exclude from node data'
          },
          summarizeNodes: {
            type: 'boolean',
            description: 'Return only essential node properties to reduce response size'
          }
        },
        required: ['file_key', 'ids']
      }
    }
  • MCP CallToolRequestSchema handler case for 'get_file_nodes'. Validates arguments, calls figmaClient.getFileNodes, checks response size with helpful error if too large, and formats response.
    case 'get_file_nodes': {
      const args = request.params.arguments as unknown as FileNodesArgs & { 
        pageSize?: number;
        maxResponseSize?: number;
        cursor?: string;
        depth?: number;
        nodeTypes?: string[];
        excludeProps?: string[];
        summarizeNodes?: boolean;
      };
      if (!args.file_key) {
        throw new McpError(ErrorCode.InvalidParams, 'file_key is required');
      }
      if (!args.ids || !Array.isArray(args.ids) || args.ids.length === 0) {
        throw new McpError(
          ErrorCode.InvalidParams,
          'ids array is required and must not be empty'
        );
      }
      console.debug('[MCP Debug] Fetching file nodes', args);
      const data = await this.figmaClient.getFileNodes(
        args.file_key, 
        args.ids, 
        {
          pageSize: args.pageSize,
          maxResponseSize: args.maxResponseSize,
          cursor: args.cursor,
          depth: args.depth,
          nodeTypes: args.nodeTypes,
          excludeProps: args.excludeProps,
          summarizeNodes: args.summarizeNodes
        }
      );
      
      // Check response size and provide helpful error if too large
      const jsonString = JSON.stringify(data, null, 2);
      const sizeInBytes = new TextEncoder().encode(jsonString).length;
      const estimatedTokens = Math.ceil(sizeInBytes / 4); // ~4 bytes per token
      
      if (estimatedTokens > 25000) {
        const helpfulError = {
          error: 'Response too large',
          estimatedTokens,
          maxTokens: 25000,
          currentParams: {
            pageSize: args.pageSize || 50,
            depth: args.depth || 'full',
            summarizeNodes: args.summarizeNodes || false
          },
          solutions: [
            '1. Use summarizeNodes: true to strip nodes to essentials',
            '2. Reduce pageSize to 10-25 (fetches fewer children per request)',
            '3. Set depth: 1 to fetch only immediate children',
            '4. Add excludeProps: ["fills", "effects", "strokes", "exportSettings"]',
            '5. Use cursor to paginate through children (cursor: "0" for first batch, "10" for second, etc.)'
          ],
          exampleCall: {
            file_key: args.file_key,
            ids: args.ids,
            pageSize: 10,
            depth: 1,
            summarizeNodes: true,
            excludeProps: ["fills", "effects", "strokes"],
            cursor: "0"
          },
          nodeInfo: data.pagination || { note: 'Multiple nodes requested - try fetching one at a time' }
        };
        
        return {
          content: [{ type: 'text', text: JSON.stringify(helpfulError, null, 2) }],
        };
      }
      
      return {
        content: [{ type: 'text', text: jsonString }],
      };
    }
  • Input schema/JSON Schema for the get_file_nodes tool, defining all parameters, types, descriptions, constraints, and required fields.
    inputSchema: {
      type: 'object',
      properties: {
        file_key: {
          type: 'string',
          description: 'Figma file key'
        },
        ids: {
          type: 'array',
          items: {
            type: 'string'
          },
          description: 'Array of node IDs to retrieve'
        },
        pageSize: {
          type: 'number',
          description: '[SINGLE NODE ONLY] Number of child nodes to fetch per page. Ignored when fetching multiple nodes.',
          minimum: 1,
          maximum: 1000
        },
        maxResponseSize: {
          type: 'number',
          description: 'Maximum response size in MB (defaults to 50)',
          minimum: 1,
          maximum: 100
        },
        cursor: {
          type: 'string',
          description: '[SINGLE NODE ONLY] Pagination cursor - child index to start from (e.g., "0", "50", "100"). Ignored when fetching multiple nodes.'
        },
        depth: {
          type: 'number',
          description: 'Maximum depth to traverse in the node tree',
          minimum: 1
        },
        nodeTypes: {
          type: 'array',
          items: {
            type: 'string',
            enum: [
              'FRAME',
              'GROUP',
              'VECTOR',
              'BOOLEAN_OPERATION',
              'STAR',
              'LINE',
              'TEXT',
              'COMPONENT',
              'INSTANCE'
            ]
          },
          description: 'Filter nodes by type. For single nodes: filters children only (parent always kept). For multiple nodes: filters all requested nodes.'
        },
        excludeProps: {
          type: 'array',
          items: {
            type: 'string'
          },
          description: 'Properties to exclude from node data'
        },
        summarizeNodes: {
          type: 'boolean',
          description: 'Return only essential node properties to reduce response size'
        }
      },
      required: ['file_key', 'ids']
    }
Install Server

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ArchimedesCrypto/figma-mcp-chunked'

If you have feedback or need assistance with the MCP directory API, please join our Discord server