"""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()