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(
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden. It describes what the tool returns (tree structure and screenshot) but lacks behavioral details such as whether this is a read-only operation, potential rate limits, authentication needs, error handling, or how the screenshot is generated (e.g., rendering time, size). For a tool with 5 parameters and no annotations, this is a significant gap in transparency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized and front-loaded, starting with the core purpose followed by parameter details in a structured list. Every sentence adds value, such as explaining parameter purposes and referencing other tools. Minor improvements could include briefer phrasing, but overall it's efficient and well-organized.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given 5 parameters, no annotations, and no output schema, the description is moderately complete. It covers parameter semantics well and references another tool for schema details, but lacks information on return values, error cases, or behavioral traits. For a tool with this complexity, it should provide more context on output structure and usage constraints to be fully helpful.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate. It provides clear semantics for all 5 parameters: 'file_id' and 'object_id' identify the target, 'fields' specifies what to include with a reference to another tool, 'depth' controls traversal with -1 meaning full depth, and 'format' sets output format. This adds substantial meaning beyond the bare schema, though it could elaborate on default behaviors beyond the schema's defaults.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Get') and the resource ('object tree structure for a Penpot object'), including specific output fields like 'tree' and 'image.mcp_uri'. It distinguishes from siblings like 'get_file' or 'search_object' by focusing on tree structure and screenshot rendering. However, it doesn't explicitly contrast with 'penpot_tree_schema', which is mentioned as a reference tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage when needing object tree structure with a rendered screenshot, but doesn't explicitly state when to use this versus alternatives like 'get_file' for basic file info or 'search_object' for finding objects. It references 'penpot_tree_schema' for field details, providing some context, but lacks clear exclusions or comparative guidance with other tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

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

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