"""Client for connecting to the Penpot MCP server.""" import asyncio from typing import Any, Dict, List, Optional from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client class PenpotMCPClient: """Client for interacting with the Penpot MCP server.""" def __init__(self, server_command="python", server_args=None, env=None): """ Initialize the Penpot MCP client. Args: server_command: The command to run the server server_args: Arguments to pass to the server command env: Environment variables for the server process """ self.server_command = server_command self.server_args = server_args or ["-m", "penpot_mcp.server.mcp_server"] self.env = env self.session = None async def connect(self): """ Connect to the MCP server. Returns: The client session """ # Create server parameters for stdio connection server_params = StdioServerParameters( command=self.server_command, args=self.server_args, env=self.env, ) # Connect to the server read, write = await stdio_client(server_params).__aenter__() self.session = await ClientSession(read, write).__aenter__() # Initialize the connection await self.session.initialize() return self.session async def disconnect(self): """Disconnect from the server.""" if self.session: await self.session.__aexit__(None, None, None) self.session = None async def list_resources(self) -> List[Dict[str, Any]]: """ List available resources from the server. Returns: List of resource information """ if not self.session: raise RuntimeError("Not connected to server") return await self.session.list_resources() async def list_tools(self) -> List[Dict[str, Any]]: """ List available tools from the server. Returns: List of tool information """ if not self.session: raise RuntimeError("Not connected to server") return await self.session.list_tools() async def get_server_info(self) -> Dict[str, Any]: """ Get server information. Returns: Server information """ if not self.session: raise RuntimeError("Not connected to server") info, _ = await self.session.read_resource("server://info") return info async def list_projects(self) -> Dict[str, Any]: """ List Penpot projects. Returns: Project information """ if not self.session: raise RuntimeError("Not connected to server") return await self.session.call_tool("list_projects") async def get_project(self, project_id: str) -> Dict[str, Any]: """ Get details for a specific project. Args: project_id: The project ID Returns: Project information """ if not self.session: raise RuntimeError("Not connected to server") return await self.session.call_tool("get_project", {"project_id": project_id}) async def get_project_files(self, project_id: str) -> Dict[str, Any]: """ Get files for a specific project. Args: project_id: The project ID Returns: File information """ if not self.session: raise RuntimeError("Not connected to server") return await self.session.call_tool("get_project_files", {"project_id": project_id}) async def get_file(self, file_id: str, features: Optional[List[str]] = None, project_id: Optional[str] = None) -> Dict[str, Any]: """ Get details for a specific file. Args: file_id: The file ID features: List of features to include project_id: Optional project ID Returns: File information """ if not self.session: raise RuntimeError("Not connected to server") params = {"file_id": file_id} if features: params["features"] = features if project_id: params["project_id"] = project_id return await self.session.call_tool("get_file", params) async def get_components(self) -> Dict[str, Any]: """ Get components from the server. Returns: Component information """ if not self.session: raise RuntimeError("Not connected to server") components, _ = await self.session.read_resource("content://components") return components async def export_object(self, file_id: str, page_id: str, object_id: str, export_type: str = "png", scale: int = 1, save_to_file: Optional[str] = None) -> Dict[str, Any]: """ Export an object from a Penpot file. Args: file_id: The ID of the file containing the object page_id: The ID of the page containing the object object_id: The ID of the object to export export_type: Export format (png, svg, pdf) scale: Scale factor for the export save_to_file: Optional path to save the exported file Returns: If save_to_file is None: Dictionary with the exported image data If save_to_file is provided: Dictionary with the saved file path """ if not self.session: raise RuntimeError("Not connected to server") params = { "file_id": file_id, "page_id": page_id, "object_id": object_id, "export_type": export_type, "scale": scale } result = await self.session.call_tool("export_object", params) # The result is now directly an Image object which has 'data' and 'format' fields # If the client wants to save the file if save_to_file: import os # Create directory if it doesn't exist os.makedirs(os.path.dirname(os.path.abspath(save_to_file)), exist_ok=True) # Save to file with open(save_to_file, "wb") as f: f.write(result["data"]) return {"file_path": save_to_file, "format": result.get("format")} # Otherwise return the result as is return result async def run_client_example(): """Run a simple example using the client.""" # Create and connect the client client = PenpotMCPClient() await client.connect() try: # Get server info print("Getting server info...") server_info = await client.get_server_info() print(f"Server info: {server_info}") # List projects print("\nListing projects...") projects_result = await client.list_projects() if "error" in projects_result: print(f"Error: {projects_result['error']}") else: projects = projects_result.get("projects", []) print(f"Found {len(projects)} projects:") for project in projects[:5]: # Show first 5 projects print(f"- {project.get('name', 'Unknown')} (ID: {project.get('id', 'N/A')})") # Example of exporting an object (uncomment and update with actual IDs to test) """ print("\nExporting object...") # Replace with actual IDs from your Penpot account export_result = await client.export_object( file_id="your-file-id", page_id="your-page-id", object_id="your-object-id", export_type="png", scale=2, save_to_file="exported_object.png" ) print(f"Export saved to: {export_result.get('file_path')}") # Or get the image data directly without saving image_data = await client.export_object( file_id="your-file-id", page_id="your-page-id", object_id="your-object-id" ) print(f"Received image in format: {image_data.get('format')}") print(f"Image size: {len(image_data.get('data'))} bytes") """ finally: # Disconnect from the server await client.disconnect() def main(): """Run the client example.""" asyncio.run(run_client_example()) if __name__ == "__main__": main()