Add test_credentials.py for Penpot API credential verification and project listing

- Introduced a new script, `test_credentials.py`, to verify Penpot API credentials and list associated projects.
- The script loads environment variables, checks for required credentials, and attempts to authenticate with the Penpot API.
- Added functionality to fetch and display project details and files, including error handling for authentication and project retrieval.
- Updated `PenpotAPI` class to include a User-Agent header and improved error handling during profile retrieval.
- Minor adjustments in import order across various modules for consistency.
This commit is contained in:
chema
2025-06-29 18:22:23 +02:00
parent 0d4d34904c
commit cc9d0312e3
9 changed files with 122 additions and 22 deletions

View File

@@ -31,7 +31,8 @@ class PenpotAPI:
# based on the required content type (JSON vs Transit+JSON)
self.session.headers.update({
"Accept": "application/json, application/transit+json",
"Content-Type": "application/json"
"Content-Type": "application/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"
})
def set_access_token(self, token: str):
@@ -64,7 +65,12 @@ class PenpotAPI:
token = self.login_for_export(email, password)
self.set_access_token(token)
# Get profile ID after login
try:
self.get_profile()
except Exception as e:
if self.debug:
print(f"\nWarning: Could not get profile (may be blocked by Cloudflare): {e}")
# Continue without profile_id - most operations don't need it
return token
def get_profile(self) -> Dict[str, Any]:
@@ -138,7 +144,8 @@ class PenpotAPI:
# Set headers
headers = {
"Content-Type": "application/transit+json"
"Content-Type": "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"
}
response = login_session.post(url, json=payload, headers=headers)
@@ -171,7 +178,7 @@ class PenpotAPI:
# If we reached here, we couldn't find the token
raise ValueError("Auth token not found in response cookies or JSON body")
def _make_authenticated_request(self, method: str, url: str, **kwargs) -> requests.Response:
def _make_authenticated_request(self, method: str, url: str, retry_auth: bool = True, **kwargs) -> requests.Response:
"""
Make an authenticated request, handling re-auth if needed.
@@ -269,7 +276,11 @@ class PenpotAPI:
except requests.HTTPError as e:
# Handle authentication errors
if e.response.status_code in (401, 403) and self.email and self.password:
if e.response.status_code in (401, 403) and self.email and self.password and retry_auth:
# Special case: don't retry auth for get-profile to avoid infinite loops
if url.endswith('/get-profile'):
raise
if self.debug:
print("\nAuthentication failed. Trying to re-login...")
@@ -280,7 +291,7 @@ class PenpotAPI:
headers['Authorization'] = f"Token {self.access_token}"
combined_headers = {**self.session.headers, **headers}
# Retry the request with the new token
# Retry the request with the new token (but don't retry auth again)
response = getattr(self.session, method)(url, headers=combined_headers, **kwargs)
response.raise_for_status()
return response
@@ -500,7 +511,8 @@ class PenpotAPI:
headers = {
"Content-Type": "application/transit+json",
"Accept": "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
@@ -557,7 +569,8 @@ class PenpotAPI:
}
headers = {
"Content-Type": "application/transit+json",
"Accept": "*/*"
"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}")

View File

@@ -5,13 +5,14 @@ This module defines the MCP server with resources and tools for interacting with
the Penpot design platform.
"""
import argparse
import hashlib
import json
import os
import re
import argparse
import sys
from typing import List, Optional, Dict
from typing import Dict, List, Optional
from mcp.server.fastmcp import FastMCP, Image
from penpot_mcp.api.penpot_api import PenpotAPI

View File

@@ -6,7 +6,7 @@ a tree representation, which can be displayed or exported.
"""
import re
from typing import Any, Dict, Optional, Union, List
from typing import Any, Dict, List, Optional, Union
from anytree import Node, RenderTree
from anytree.exporter import DotExporter

View File

@@ -3,7 +3,8 @@ Cache utilities for Penpot MCP server.
"""
import time
from typing import Optional, Dict, Any
from typing import Any, Dict, Optional
class MemoryCache:
"""In-memory cache implementation with TTL support."""

View File

@@ -2,9 +2,10 @@
import io
import json
import socketserver
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import socketserver
class InMemoryImageHandler(BaseHTTPRequestHandler):
"""HTTP request handler for serving images stored in memory."""

80
test_credentials.py Executable file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""
Test script to verify Penpot API credentials and list projects.
"""
import os
from dotenv import load_dotenv
from penpot_mcp.api.penpot_api import PenpotAPI
def test_credentials():
"""Test Penpot API credentials and list projects."""
load_dotenv()
api_url = os.getenv("PENPOT_API_URL")
username = os.getenv("PENPOT_USERNAME")
password = os.getenv("PENPOT_PASSWORD")
if not all([api_url, username, password]):
print("❌ Missing credentials in .env file")
print("Required: PENPOT_API_URL, PENPOT_USERNAME, PENPOT_PASSWORD")
return False
print(f"🔗 Testing connection to: {api_url}")
print(f"👤 Username: {username}")
try:
api = PenpotAPI(api_url, debug=False, email=username, password=password)
print("🔐 Authenticating...")
token = api.login_with_password()
print("✅ Authentication successful!")
print("📁 Fetching projects...")
projects = api.list_projects()
if isinstance(projects, dict) and "error" in projects:
print(f"❌ Failed to list projects: {projects['error']}")
return False
print(f"✅ Found {len(projects)} projects:")
for i, project in enumerate(projects, 1):
if isinstance(project, dict):
name = project.get('name', 'Unnamed')
project_id = project.get('id', 'N/A')
team_name = project.get('team-name', 'Unknown Team')
print(f" {i}. {name} (ID: {project_id}) - Team: {team_name}")
else:
print(f" {i}. {project}")
# Test getting project files if we have a project
if projects and isinstance(projects[0], dict):
project_id = projects[0].get('id')
if project_id:
print(f"\n📄 Testing project files for project: {project_id}")
try:
files = api.get_project_files(project_id)
print(f"✅ Found {len(files)} files:")
for j, file in enumerate(files[:3], 1): # Show first 3 files
if isinstance(file, dict):
print(f" {j}. {file.get('name', 'Unnamed')} (ID: {file.get('id', 'N/A')})")
else:
print(f" {j}. {file}")
if len(files) > 3:
print(f" ... and {len(files) - 3} more files")
except Exception as file_error:
print(f"❌ Error getting files: {file_error}")
return True
except Exception as e:
print(f"❌ Error: {e}")
return False
if __name__ == "__main__":
success = test_credentials()
exit(0 if success else 1)

View File

@@ -3,9 +3,12 @@ Tests for the memory caching functionality.
"""
import time
import pytest
from penpot_mcp.utils.cache import MemoryCache
@pytest.fixture
def memory_cache():
"""Create a MemoryCache instance with a short TTL for testing."""

View File

@@ -1,12 +1,13 @@
"""Tests for the MCP server module."""
import hashlib
import json
import os
import hashlib
from unittest.mock import MagicMock, mock_open, patch
import yaml
import pytest
import yaml
from penpot_mcp.server.mcp_server import PenpotMCPServer, create_server

View File

@@ -8,13 +8,13 @@ from anytree import Node, RenderTree
from penpot_mcp.tools.penpot_tree import (
build_tree,
print_tree,
export_tree_to_dot,
find_page_containing_object,
find_object_in_tree,
convert_node_to_dict,
export_tree_to_dot,
find_object_in_tree,
find_page_containing_object,
get_object_subtree,
get_object_subtree_with_fields
get_object_subtree_with_fields,
print_tree,
)