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) # based on the required content type (JSON vs Transit+JSON)
self.session.headers.update({ self.session.headers.update({
"Accept": "application/json, application/transit+json", "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): def set_access_token(self, token: str):
@@ -64,7 +65,12 @@ class PenpotAPI:
token = self.login_for_export(email, password) token = self.login_for_export(email, password)
self.set_access_token(token) self.set_access_token(token)
# Get profile ID after login # Get profile ID after login
self.get_profile() 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 return token
def get_profile(self) -> Dict[str, Any]: def get_profile(self) -> Dict[str, Any]:
@@ -138,7 +144,8 @@ class PenpotAPI:
# Set headers # Set headers
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) 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 # If we reached here, we couldn't find the token
raise ValueError("Auth token not found in response cookies or JSON body") 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. Make an authenticated request, handling re-auth if needed.
@@ -269,7 +276,11 @@ class PenpotAPI:
except requests.HTTPError as e: except requests.HTTPError as e:
# Handle authentication errors # 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: if self.debug:
print("\nAuthentication failed. Trying to re-login...") print("\nAuthentication failed. Trying to re-login...")
@@ -280,7 +291,7 @@ class PenpotAPI:
headers['Authorization'] = f"Token {self.access_token}" headers['Authorization'] = f"Token {self.access_token}"
combined_headers = {**self.session.headers, **headers} 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 = getattr(self.session, method)(url, headers=combined_headers, **kwargs)
response.raise_for_status() response.raise_for_status()
return response return response
@@ -500,7 +511,8 @@ class PenpotAPI:
headers = { headers = {
"Content-Type": "application/transit+json", "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 # Make the request
@@ -557,7 +569,8 @@ class PenpotAPI:
} }
headers = { headers = {
"Content-Type": "application/transit+json", "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: if self.debug:
print(f"\nFetching export resource: {url}") 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. the Penpot design platform.
""" """
import argparse
import hashlib import hashlib
import json import json
import os import os
import re import re
import argparse
import sys import sys
from typing import List, Optional, Dict from typing import Dict, List, Optional
from mcp.server.fastmcp import FastMCP, Image from mcp.server.fastmcp import FastMCP, Image
from penpot_mcp.api.penpot_api import PenpotAPI 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 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 import Node, RenderTree
from anytree.exporter import DotExporter from anytree.exporter import DotExporter

View File

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

View File

@@ -2,9 +2,10 @@
import io import io
import json import json
import socketserver
import threading import threading
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
import socketserver
class InMemoryImageHandler(BaseHTTPRequestHandler): class InMemoryImageHandler(BaseHTTPRequestHandler):
"""HTTP request handler for serving images stored in memory.""" """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 time
import pytest import pytest
from penpot_mcp.utils.cache import MemoryCache from penpot_mcp.utils.cache import MemoryCache
@pytest.fixture @pytest.fixture
def memory_cache(): def memory_cache():
"""Create a MemoryCache instance with a short TTL for testing.""" """Create a MemoryCache instance with a short TTL for testing."""

View File

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

View File

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