Skip to main content
Glama

get_object_tree

Retrieve the hierarchical structure and screenshot of a Penpot design object to analyze its composition and relationships within design files.

Instructions

Get the object tree structure for a Penpot object ("tree" field) with rendered screenshot image of the object ("image.mcp_uri" field). Args: file_id: The ID of the Penpot file object_id: The ID of the object to retrieve fields: Specific fields to include in the tree (call "penpot_tree_schema" resource/tool for available fields) depth: How deep to traverse the object tree (-1 for full depth) format: Output format ('json' or 'yaml')

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
file_idYes
object_idYes
fieldsYes
depthNo
formatNojson

Implementation Reference

  • The primary handler for the 'get_object_tree' MCP tool. Retrieves cached file data, extracts subtree using helper function, generates rendered image with URI, handles JSON/YAML formatting, and manages errors.
    @self.mcp.tool()
    def get_object_tree(
        file_id: str, 
        object_id: str, 
        fields: List[str],
        depth: int = -1,
        format: str = "json"
    ) -> dict:
        """Get the object tree structure for a Penpot object ("tree" field) with rendered screenshot image of the object ("image.mcp_uri" field).
        Args:
            file_id: The ID of the Penpot file
            object_id: The ID of the object to retrieve
            fields: Specific fields to include in the tree (call "penpot_tree_schema" resource/tool for available fields)
            depth: How deep to traverse the object tree (-1 for full depth)
            format: Output format ('json' or 'yaml')
        """
        try:
            file_data = get_cached_file(file_id)
            if "error" in file_data:
                return file_data
            result = get_object_subtree_with_fields(
                file_data, 
                object_id, 
                include_fields=fields,
                depth=depth
            )
            if "error" in result:
                return result
            simplified_tree = result["tree"]
            page_id = result["page_id"]
            final_result = {"tree": simplified_tree}
            
            try:
                image = export_object(
                    file_id=file_id,
                    page_id=page_id,
                    object_id=object_id
                )
                image_id = hashlib.md5(f"{file_id}:{object_id}".encode()).hexdigest()
                self.rendered_components[image_id] = image
                
                # Image URI preferences:
                # 1. HTTP server URL if available
                # 2. Fallback to MCP resource URI
                image_uri = f"render_component://{image_id}"
                if hasattr(image, 'http_url'):
                    final_result["image"] = {
                        "uri": image.http_url,
                        "mcp_uri": image_uri,
                        "format": image.format if hasattr(image, 'format') else "png"
                    }
                else:
                    final_result["image"] = {
                        "uri": image_uri,
                        "format": image.format if hasattr(image, 'format') else "png"
                    }
            except Exception as e:
                final_result["image_error"] = str(e)
            if format.lower() == "yaml":
                try:
                    import yaml
                    yaml_result = yaml.dump(final_result, default_flow_style=False, sort_keys=False)
                    return {"yaml_result": yaml_result}
                except ImportError:
                    return {"format_error": "YAML format requested but PyYAML package is not installed"}
                except Exception as e:
                    return {"format_error": f"Error formatting as YAML: {str(e)}"}
            return final_result
        except Exception as e:
            return self._handle_api_error(e)
  • Key helper function called by the handler to construct the filtered object subtree, respecting field selection, depth limits, circular reference detection, and recursive child traversal.
    def get_object_subtree_with_fields(file_data: Dict[str, Any], object_id: str, 
                                      include_fields: Optional[List[str]] = None, 
                                      depth: int = -1) -> Dict[str, Any]:
        """
        Get a filtered tree representation of an object with only specified fields.
        
        This function finds an object in the Penpot file data and returns a subtree
        with the object as the root, including only the specified fields and limiting
        the depth of the tree if requested.
        
        Args:
            file_data: The Penpot file data
            object_id: The ID of the object to get the tree for
            include_fields: List of field names to include in the output (None means include all)
            depth: Maximum depth of the tree (-1 means no limit)
        
        Returns:
            Dictionary containing the filtered tree or an error message
        """
        try:
            # Get the content from file data
            content = file_data.get('data', file_data)
            
            # Find which page contains the object
            page_id = find_page_containing_object(content, object_id)
            
            if not page_id:
                return {"error": f"Object {object_id} not found in file"}
                
            # Get the page data
            page_data = content.get('pagesIndex', {}).get(page_id, {})
            objects_dict = page_data.get('objects', {})
            
            # Check if the object exists in this page
            if object_id not in objects_dict:
                return {"error": f"Object {object_id} not found in page {page_id}"}
                
            # Track visited nodes to prevent infinite loops
            visited = set()
            
            # Function to recursively build the filtered object tree
            def build_filtered_object_tree(obj_id: str, current_depth: int = 0):
                if obj_id not in objects_dict:
                    return None
                
                # Check for circular reference
                if obj_id in visited:
                    # Return a placeholder to indicate circular reference
                    return {
                        'id': obj_id,
                        'name': objects_dict[obj_id].get('name', 'Unnamed'),
                        'type': objects_dict[obj_id].get('type', 'unknown'),
                        '_circular_reference': True
                    }
                
                # Mark this object as visited
                visited.add(obj_id)
                
                obj_data = objects_dict[obj_id]
                
                # Create a new dict with only the requested fields or all fields if None
                if include_fields is None:
                    filtered_obj = obj_data.copy()
                else:
                    filtered_obj = {field: obj_data[field] for field in include_fields if field in obj_data}
                
                # Always include the id field
                filtered_obj['id'] = obj_id
                
                # If depth limit reached, don't process children
                if depth != -1 and current_depth >= depth:
                    # Remove from visited before returning
                    visited.remove(obj_id)
                    return filtered_obj
                    
                # Find all children of this object
                children = []
                for child_id, child_data in objects_dict.items():
                    if child_data.get('parentId') == obj_id:
                        child_tree = build_filtered_object_tree(child_id, current_depth + 1)
                        if child_tree:
                            children.append(child_tree)
                
                # Add children field only if we have children
                if children:
                    filtered_obj['children'] = children
                
                # Remove from visited after processing
                visited.remove(obj_id)
                    
                return filtered_obj
            
            # Build the filtered tree starting from the requested object
            object_tree = build_filtered_object_tree(object_id)
            
            if not object_tree:
                return {"error": f"Failed to build object tree for {object_id}"}
                
            return {
                "tree": object_tree,
                "page_id": page_id
            }
            
        except Exception as e:
            return {"error": str(e)}
  • MCP resource that serves the JSON schema defining available fields in Penpot object trees, referenced in the get_object_tree tool documentation.
    @self.mcp.resource("penpot://tree-schema", mime_type="application/schema+json")
    def penpot_tree_schema() -> dict:
        """Provide the Penpot object tree schema as JSON."""
        schema_path = os.path.join(config.RESOURCES_PATH, 'penpot-tree-schema.json')
        try:
            with open(schema_path, 'r') as f:
                return json.load(f)
        except Exception as e:
            return {"error": f"Failed to load tree schema: {str(e)}"}
  • Decorator that registers the get_object_tree function as an MCP tool.
    @self.mcp.tool()
    def get_object_tree(

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/montevive/penpot-mcp'

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