Skip to main content
Glama

export_object

Export a Penpot design object as an image by specifying file, page, object IDs, format, and scale.

Instructions

Export a Penpot design object as an image.

        Args:
            file_id: The ID of the Penpot file
            page_id: The ID of the page containing the object
            object_id: The ID of the object to export
            export_type: Image format (png, svg, etc.)
            scale: Scale factor for the exported image
        

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
file_idYes
page_idYes
object_idYes
export_typeNopng
scaleNo

Implementation Reference

  • The primary handler function for the 'export_object' MCP tool. It orchestrates the export by calling the PenpotAPI.export_and_download method, handles temporary files, creates an MCP Image object, and optionally serves the image via HTTP.
    @self.mcp.tool()
    def export_object(
            file_id: str,
            page_id: str,
            object_id: str,
            export_type: str = "png",
            scale: int = 1) -> Image:
        """Export a Penpot design object as an image.
        
        Args:
            file_id: The ID of the Penpot file
            page_id: The ID of the page containing the object
            object_id: The ID of the object to export
            export_type: Image format (png, svg, etc.)
            scale: Scale factor for the exported image
        """
        temp_filename = None
        try:
            import tempfile
            temp_dir = tempfile.gettempdir()
            temp_filename = os.path.join(temp_dir, f"{object_id}.{export_type}")
            output_path = self.api.export_and_download(
                file_id=file_id,
                page_id=page_id,
                object_id=object_id,
                export_type=export_type,
                scale=scale,
                save_to_file=temp_filename
            )
            with open(output_path, "rb") as f:
                file_content = f.read()
                
            image = Image(data=file_content, format=export_type)
            
            # If HTTP server is enabled, add the image to the server
            if self.image_server and self.image_server.is_running:
                image_id = hashlib.md5(f"{file_id}:{page_id}:{object_id}".encode()).hexdigest()
                # Use the current image_server_url to ensure the correct port
                image_url = self.image_server.add_image(image_id, file_content, export_type)
                # Add HTTP URL to the image metadata
                image.http_url = image_url
                
            return image
        except Exception as e:
            if isinstance(e, CloudFlareError):
                raise Exception(f"CloudFlare Protection: {str(e)}")
            else:
                raise Exception(f"Export failed: {str(e)}")
        finally:
            if temp_filename and os.path.exists(temp_filename):
                try:
                    os.remove(temp_filename)
                except Exception as e:
                    print(f"Warning: Failed to delete temporary file {temp_filename}: {str(e)}")
  • Core helper method 'export_and_download' in PenpotAPI class that handles the actual Penpot API interactions: creates an export job and downloads the resulting image file.
    def export_and_download(self, file_id: str, page_id: str, object_id: str,
                            save_to_file: Optional[str] = None, export_type: str = "png",
                            scale: int = 1, name: str = "Board", suffix: str = "",
                            email: Optional[str] = None, password: Optional[str] = None,
                            profile_id: Optional[str] = None) -> Union[bytes, str]:
        """
        Create and download an export in one step.
    
        This is a convenience method that combines create_export and get_export_resource.
    
        Args:
            file_id: The file ID
            page_id: The page ID
            object_id: The object ID to export
            save_to_file: Path to save the file (if None, returns the content)
            export_type: Type of export (png, svg, pdf)
            scale: Scale factor for the export
            name: Name for the export
            suffix: Suffix to add to the export name
            email: Email for authentication (if different from instance)
            password: Password for authentication (if different from instance)
            profile_id: Optional profile ID (if not provided, will be fetched automatically)
    
        Returns:
            Either the file content as bytes, or the path to the saved file
        """
        # Create the export
        resource_id = self.create_export(
            file_id=file_id,
            page_id=page_id,
            object_id=object_id,
            export_type=export_type,
            scale=scale,
            email=email,
            password=password,
            profile_id=profile_id
        )
    
        # Download the resource
        return self.get_export_resource(
            resource_id=resource_id,
            save_to_file=save_to_file,
            email=email,
            password=password
        )
  • Supporting helper 'create_export' that sends the Transit+JSON payload to Penpot's /export endpoint to initiate the object export job.
    def create_export(self, file_id: str, page_id: str, object_id: str,
                      export_type: str = "png", scale: int = 1,
                      email: Optional[str] = None, password: Optional[str] = None,
                      profile_id: Optional[str] = None):
        """
        Create an export job for a Penpot object.
    
        Args:
            file_id: The file ID
            page_id: The page ID
            object_id: The object ID to export
            export_type: Type of export (png, svg, pdf)
            scale: Scale factor for the export
            name: Name for the export
            suffix: Suffix to add to the export name
            email: Email for authentication (if different from instance)
            password: Password for authentication (if different from instance)
            profile_id: Optional profile ID (if not provided, will be fetched automatically)
    
        Returns:
            Export resource ID
        """
        # This uses the cookie auth approach, which requires login
        token = self.login_for_export(email, password)
    
        # If profile_id is not provided, get it from instance variable
        if not profile_id:
            profile_id = self.profile_id
    
        if not profile_id:
            raise ValueError("Profile ID not available. It should be automatically extracted during login.")
    
        # Build the URL for export creation
        url = f"{self.base_url}/export"
    
        # Set up the data for the export
        payload = {
            "~:wait": True,
            "~:exports": [
                {"~:type": f"~:{export_type}",
                 "~:suffix": "",
                 "~:scale": scale,
                 "~:page-id": f"~u{page_id}",
                 "~:file-id": f"~u{file_id}",
                 "~:name": "",
                 "~:object-id": f"~u{object_id}"}
            ],
            "~:profile-id": f"~u{profile_id}",
            "~:cmd": "~:export-shapes"
        }
    
        if self.debug:
            print("\nCreating export with parameters:")
            print(json.dumps(payload, indent=2))
    
        # Create a session with the auth token
        export_session = requests.Session()
        export_session.cookies.set("auth-token", token)
    
        headers = {
            "Content-Type": "application/transit+json",
            "Accept": "application/transit+json",
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
        }
    
        # Make the request
        response = export_session.post(url, json=payload, headers=headers)
    
        if self.debug and response.status_code != 200:
            print(f"\nError response: {response.status_code}")
            print(f"Response text: {response.text}")
    
        response.raise_for_status()
    
        # Parse the response
        data = response.json()
    
        if self.debug:
            print("\nExport created successfully")
            print(f"Response: {json.dumps(data, indent=2)}")
    
        # Extract and return the resource ID
        resource_id = data.get("~:id")
        if not resource_id:
            raise ValueError("Resource ID not found in response")
    
        return resource_id
  • Supporting helper 'get_export_resource' that polls/retrieves the exported image content from Penpot using the resource ID.
    def get_export_resource(self,
                            resource_id: str,
                            save_to_file: Optional[str] = None,
                            email: Optional[str] = None,
                            password: Optional[str] = None) -> Union[bytes,
                                                                     str]:
        """
        Download an export resource by ID.
    
        Args:
            resource_id: The resource ID from create_export
            save_to_file: Path to save the file (if None, returns the content)
            email: Email for authentication (if different from instance)
            password: Password for authentication (if different from instance)
    
        Returns:
            Either the file content as bytes, or the path to the saved file
        """
        # This uses the cookie auth approach, which requires login
        token = self.login_for_export(email, password)
    
        # Build the URL for the resource
        url = f"{self.base_url}/export"
    
        payload = {
            "~:wait": False,
            "~:cmd": "~:get-resource",
            "~:id": resource_id
        }
        headers = {
            "Content-Type": "application/transit+json",
            "Accept": "*/*",
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
        }
        if self.debug:
            print(f"\nFetching export resource: {url}")
    
        # Create a session with the auth token
        export_session = requests.Session()
        export_session.cookies.set("auth-token", token)
    
        # Make the request
        response = export_session.post(url, json=payload, headers=headers)
    
        if self.debug and response.status_code != 200:
            print(f"\nError response: {response.status_code}")
            print(f"Response headers: {response.headers}")
    
        response.raise_for_status()
    
        # Get the content type
        content_type = response.headers.get('Content-Type', '')
    
        if self.debug:
            print(f"\nResource fetched successfully")
            print(f"Content-Type: {content_type}")
            print(f"Content length: {len(response.content)} bytes")
    
        # Determine filename if saving to file
        if save_to_file:
            if os.path.isdir(save_to_file):
                # If save_to_file is a directory, we need to figure out the filename
                filename = None
    
                # Try to get filename from Content-Disposition header
                content_disp = response.headers.get('Content-Disposition', '')
                if 'filename=' in content_disp:
                    filename = content_disp.split('filename=')[1].strip('"\'')
    
                # If we couldn't get a filename, use the resource_id with an extension
                if not filename:
                    ext = content_type.split('/')[-1].split(';')[0]
                    if ext in ('jpeg', 'png', 'pdf', 'svg+xml'):
                        if ext == 'svg+xml':
                            ext = 'svg'
                        filename = f"{resource_id}.{ext}"
                    else:
                        filename = f"{resource_id}"
    
                save_path = os.path.join(save_to_file, filename)
            else:
                # Use the provided path directly
                save_path = save_to_file
    
            # Ensure the directory exists
            os.makedirs(os.path.dirname(os.path.abspath(save_path)), exist_ok=True)
    
            # Save the content to file
            with open(save_path, 'wb') as f:
                f.write(response.content)
    
            if self.debug:
                print(f"\nSaved resource to {save_path}")
    
            return save_path
        else:
            # Return the content
            return response.content
  • The @self.mcp.tool() decorator registers the export_object function as an MCP tool named 'export_object'.
    @self.mcp.tool()
Behavior2/5

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

No annotations are provided, so the description carries the full burden of behavioral disclosure. It states the action ('Export') but lacks details on permissions needed, whether it's a read-only or write operation, rate limits, or what the output entails (e.g., file download link, binary data). This is inadequate for a tool with potential side effects like file generation.

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 in the first sentence. The parameter explanations are listed efficiently in bullet-point style under 'Args:', though some could be more concise (e.g., 'etc.' in export_type is vague). Overall, it avoids unnecessary verbosity.

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

Completeness2/5

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

Given no annotations, 0% schema description coverage, and no output schema, the description is incomplete. It covers the basic action and parameters but misses critical behavioral context (e.g., output format, side effects) and usage guidelines relative to siblings. For a 5-parameter tool with potential file operations, this leaves significant gaps for an agent.

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

Parameters3/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 lists all 5 parameters with brief explanations (e.g., 'Image format (png, svg, etc.)' for export_type), adding basic semantics beyond the schema's titles. However, it doesn't provide deeper context like valid export_type values beyond examples or scale constraints, leaving gaps in understanding.

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 verb ('Export') and resource ('a Penpot design object as an image'), making the purpose immediately understandable. However, it doesn't explicitly differentiate this tool from sibling tools like 'get_rendered_component' or 'search_object', which might also involve object retrieval or rendering, so it falls short of a perfect score.

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

Usage Guidelines2/5

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

The description provides no guidance on when to use this tool versus alternatives. With sibling tools like 'get_rendered_component' and 'search_object' available, there's no indication of scenarios where 'export_object' is preferred, such as for image file generation versus in-app viewing, leaving the agent to guess based on tool names alone.

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