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:
@@ -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
|
||||||
|
try:
|
||||||
self.get_profile()
|
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}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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
80
test_credentials.py
Executable 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)
|
||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ 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,
|
|
||||||
export_tree_to_dot,
|
|
||||||
find_page_containing_object,
|
|
||||||
find_object_in_tree,
|
|
||||||
convert_node_to_dict,
|
convert_node_to_dict,
|
||||||
|
export_tree_to_dot,
|
||||||
|
find_object_in_tree,
|
||||||
|
find_page_containing_object,
|
||||||
get_object_subtree,
|
get_object_subtree,
|
||||||
get_object_subtree_with_fields
|
get_object_subtree_with_fields,
|
||||||
|
print_tree,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user