Files
leo-claude-mktplace/docs/references/projman-pmo/projman-python-quickstart.md

18 KiB

ProjMan Plugins - Python Quick Start

This guide provides Python-specific setup and development information for the projman and projman-pmo plugins.

⚠️ IMPORTANT: For the definitive repository structure, refer to CORRECT-ARCHITECTURE.md. This guide shows Python-specific patterns and setup.


Technology Stack

  • MCP Server: Python 3.11+
  • Commands: Markdown files
  • Agents: Markdown files
  • Dependencies: pip with requirements.txt
  • Virtual Environment: .venv (per plugin)

Initial Setup

1. System Requirements

# Python 3.11 or higher
python --version

# pip (latest)
pip --version

# git
git --version

2. System-Level Configuration

# Create config directory
mkdir -p ~/.config/claude

# Create gitea.env with your credentials
cat > ~/.config/claude/gitea.env << EOF
GITEA_API_URL=https://gitea.hyperhivelabs.com/api/v1
GITEA_API_TOKEN=your_token_here
GITEA_OWNER=hyperhivelabs
EOF

# Secure the file
chmod 600 ~/.config/claude/gitea.env

3. Project-Level Configuration

# In each repository root
echo "GITEA_REPO=cuisineflow" > .env

# Add to .gitignore
echo ".env" >> .gitignore

MCP Server Structure

hyperhivelabs/claude-plugins/
├── mcp-servers/               # SHARED by both plugins
│   ├── gitea/
│   │   ├── .venv/
│   │   ├── requirements.txt
│   │   ├── mcp_server/
│   │   │   ├── __init__.py
│   │   │   ├── server.py
│   │   │   ├── config.py
│   │   │   ├── gitea_client.py
│   │   │   └── tools/
│   │   └── tests/
│   └── wikijs/
│       ├── .venv/
│       ├── requirements.txt
│       ├── mcp_server/
│       │   ├── __init__.py
│       │   ├── server.py
│       │   ├── config.py
│       │   └── wikijs_client.py
│       └── tests/
├── projman/
│   ├── .mcp.json              # Points to ../mcp-servers/
│   ├── commands/
│   └── agents/
└── projman-pmo/
    ├── .mcp.json              # Points to ../mcp-servers/
    └── commands/

Dependencies (requirements.txt)

# anthropic-sdk==0.18.0  # MCP SDK
anthropic-sdk>=0.18.0
# python-dotenv==1.0.0   # Environment variable loading
python-dotenv>=1.0.0
# requests==2.31.0       # HTTP client for Gitea API
requests>=2.31.0
# pydantic==2.5.0        # Data validation
pydantic>=2.5.0
# pytest==7.4.3          # Testing framework
pytest>=7.4.3
# pytest-asyncio==0.23.0 # Async testing support
pytest-asyncio>=0.23.0

Note: Following your coding preferences, library versions are specified with comments showing the exact version being used.


Development Workflow

Initial MCP Server Setup

# Navigate to MCP servers directory
cd /path/to/claude-plugins/mcp-servers/gitea

# Create virtual environment
python -m venv .venv

# Activate virtual environment
source .venv/bin/activate  # Linux/Mac
# or
.venv\Scripts\activate     # Windows

# Install dependencies
pip install -r requirements.txt

# Verify installation
python -c "import anthropic; print('SDK installed')"

Configuration Loader (config.py)

# mcp-servers/gitea/mcp_server/config.py
from pathlib import Path
from dotenv import load_dotenv
import os
from typing import Dict, Optional

class Config:
    """Hybrid configuration loader for projman plugins"""
    
    def __init__(self):
        self.api_url: Optional[str] = None
        self.api_token: Optional[str] = None
        self.owner: Optional[str] = None
        self.repo: Optional[str] = None
    
    def load(self) -> Dict[str, str]:
        """
        Load configuration from system and project levels.
        Project-level configuration overrides system-level.
        """
        # Load system config
        system_config = Path.home() / '.config' / 'claude' / 'gitea.env'
        if system_config.exists():
            load_dotenv(system_config)
        else:
            raise FileNotFoundError(
                f"System config not found: {system_config}\n"
                "Create it with: mkdir -p ~/.config/claude && "
                "cat > ~/.config/claude/gitea.env"
            )
        
        # Load project config (overrides system)
        project_config = Path.cwd() / '.env'
        if project_config.exists():
            load_dotenv(project_config, override=True)
        
        # Extract values
        self.api_url = os.getenv('GITEA_API_URL')
        self.api_token = os.getenv('GITEA_API_TOKEN')
        self.owner = os.getenv('GITEA_OWNER')
        self.repo = os.getenv('GITEA_REPO')  # Optional for PMO
        
        # Validate required variables
        self._validate()
        
        return {
            'api_url': self.api_url,
            'api_token': self.api_token,
            'owner': self.owner,
            'repo': self.repo
        }
    
    def _validate(self) -> None:
        """Validate that required configuration is present"""
        required = {
            'GITEA_API_URL': self.api_url,
            'GITEA_API_TOKEN': self.api_token,
            'GITEA_OWNER': self.owner
        }
        
        missing = [key for key, value in required.items() if not value]
        
        if missing:
            raise ValueError(
                f"Missing required configuration: {', '.join(missing)}\n"
                "Check your ~/.config/claude/gitea.env file"
            )

# Usage
config = Config()
config_dict = config.load()

Gitea API Client (gitea_client.py)

# mcp-servers/gitea/mcp_server/gitea_client.py
import requests
from typing import List, Dict, Optional
from .config import Config

class GiteaClient:
    """Client for interacting with Gitea API"""
    
    def __init__(self):
        config = Config()
        config_dict = config.load()
        
        self.base_url = config_dict['api_url']
        self.token = config_dict['api_token']
        self.owner = config_dict['owner']
        self.repo = config_dict.get('repo')  # Optional
        
        self.session = requests.Session()
        self.session.headers.update({
            'Authorization': f'token {self.token}',
            'Content-Type': 'application/json'
        })
    
    def list_issues(
        self, 
        state: str = 'open',
        labels: Optional[List[str]] = None,
        repo: Optional[str] = None
    ) -> List[Dict]:
        """
        List issues from Gitea repository.
        
        Args:
            state: Issue state (open, closed, all)
            labels: Filter by labels
            repo: Override configured repo (for PMO multi-repo)
        """
        target_repo = repo or self.repo
        if not target_repo:
            raise ValueError("Repository not specified")
        
        url = f"{self.base_url}/repos/{self.owner}/{target_repo}/issues"
        params = {'state': state}
        
        if labels:
            params['labels'] = ','.join(labels)
        
        response = self.session.get(url, params=params)
        response.raise_for_status()
        return response.json()
    
    def create_issue(
        self,
        title: str,
        body: str,
        labels: Optional[List[str]] = None,
        repo: Optional[str] = None
    ) -> Dict:
        """Create a new issue in Gitea"""
        target_repo = repo or self.repo
        if not target_repo:
            raise ValueError("Repository not specified")
        
        url = f"{self.base_url}/repos/{self.owner}/{target_repo}/issues"
        data = {
            'title': title,
            'body': body
        }
        
        if labels:
            data['labels'] = labels
        
        response = self.session.post(url, json=data)
        response.raise_for_status()
        return response.json()
    
    def get_labels(
        self,
        repo: Optional[str] = None
    ) -> List[Dict]:
        """Get all labels from repository"""
        target_repo = repo or self.repo
        if not target_repo:
            raise ValueError("Repository not specified")
        
        url = f"{self.base_url}/repos/{self.owner}/{target_repo}/labels"
        response = self.session.get(url)
        response.raise_for_status()
        return response.json()

MCP Server Entry Point (server.py)

# mcp-servers/gitea/mcp_server/server.py
from anthropic import Anthropic
from .gitea_client import GiteaClient
from .tools import IssueTools, LabelTools, WikiTools

class ProjManMCPServer:
    """Main MCP server for projman plugin"""
    
    def __init__(self):
        self.gitea = GiteaClient()
        self.issue_tools = IssueTools(self.gitea)
        self.label_tools = LabelTools(self.gitea)
        self.wiki_tools = WikiTools(self.gitea)
    
    def register_tools(self):
        """Register all available MCP tools"""
        return [
            # Issue tools
            self.issue_tools.list_issues,
            self.issue_tools.get_issue,
            self.issue_tools.create_issue,
            self.issue_tools.update_issue,
            self.issue_tools.add_comment,
            
            # Label tools
            self.label_tools.get_labels,
            self.label_tools.suggest_labels,
            
            # Wiki tools
            self.wiki_tools.search_wiki,
            self.wiki_tools.get_wiki_page,
            self.wiki_tools.create_wiki_page
        ]

if __name__ == '__main__':
    server = ProjManMCPServer()
    # MCP server startup logic here

Testing

Unit Tests

# tests/test_config.py
import pytest
from pathlib import Path
from mcp_server.config import Config

def test_load_system_config(tmp_path):
    """Test loading system-level configuration"""
    # Create mock system config
    config_dir = tmp_path / '.config' / 'claude'
    config_dir.mkdir(parents=True)
    
    config_file = config_dir / 'gitea.env'
    config_file.write_text(
        "GITEA_API_URL=https://test.com/api/v1\n"
        "GITEA_API_TOKEN=test_token\n"
        "GITEA_OWNER=test_owner\n"
    )
    
    # Test config loading
    config = Config()
    # ... test assertions

def test_project_config_override(tmp_path):
    """Test that project config overrides system config"""
    # ... test implementation

def test_missing_required_config():
    """Test error handling for missing configuration"""
    with pytest.raises(ValueError):
        config = Config()
        config.load()

Integration Tests

# tests/test_gitea_client.py
import pytest
from mcp_server.gitea_client import GiteaClient

@pytest.fixture
def gitea_client():
    """Fixture providing configured Gitea client"""
    return GiteaClient()

def test_list_issues(gitea_client):
    """Test listing issues from Gitea"""
    issues = gitea_client.list_issues(state='open')
    assert isinstance(issues, list)

def test_create_issue(gitea_client):
    """Test creating an issue in Gitea"""
    issue = gitea_client.create_issue(
        title="Test Issue",
        body="Test body",
        labels=["Type/Bug"]
    )
    assert issue['title'] == "Test Issue"
    assert "Type/Bug" in [label['name'] for label in issue['labels']]

Running Tests

# Activate virtual environment
source .venv/bin/activate

# Run all tests
pytest

# Run with coverage
pytest --cov=mcp_server --cov-report=html

# Run specific test file
pytest tests/test_config.py

# Run with verbose output
pytest -v

.mcp.json Configuration

projman (Repository-Specific)

{
  "mcpServers": {
    "gitea-projman": {
      "command": "python",
      "args": ["-m", "mcp_server.server"],
      "cwd": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/gitea",
      "env": {
        "PYTHONPATH": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/gitea",
        "GITEA_API_URL": "${GITEA_API_URL}",
        "GITEA_API_TOKEN": "${GITEA_API_TOKEN}",
        "GITEA_OWNER": "${GITEA_OWNER}",
        "GITEA_REPO": "${GITEA_REPO}"
      }
    },
    "wikijs-projman": {
      "command": "python",
      "args": ["-m", "mcp_server.server"],
      "cwd": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/wikijs",
      "env": {
        "PYTHONPATH": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/wikijs",
        "WIKIJS_API_URL": "${WIKIJS_API_URL}",
        "WIKIJS_API_TOKEN": "${WIKIJS_API_TOKEN}",
        "WIKIJS_BASE_PATH": "${WIKIJS_BASE_PATH}",
        "WIKIJS_PROJECT": "${WIKIJS_PROJECT}"
      }
    }
  }
}

projman-pmo (Multi-Project)

{
  "mcpServers": {
    "gitea-pmo": {
      "command": "python",
      "args": ["-m", "mcp_server.server"],
      "cwd": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/gitea",
      "env": {
        "PYTHONPATH": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/gitea",
        "GITEA_API_URL": "${GITEA_API_URL}",
        "GITEA_API_TOKEN": "${GITEA_API_TOKEN}",
        "GITEA_OWNER": "${GITEA_OWNER}"
      }
    },
    "wikijs-pmo": {
      "command": "python",
      "args": ["-m", "mcp_server.server"],
      "cwd": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/wikijs",
      "env": {
        "PYTHONPATH": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/wikijs",
        "WIKIJS_API_URL": "${WIKIJS_API_URL}",
        "WIKIJS_API_TOKEN": "${WIKIJS_API_TOKEN}",
        "WIKIJS_BASE_PATH": "${WIKIJS_BASE_PATH}"
      }
    }
  }
}

Note: Both plugins reference ../mcp-servers/ (shared location). PMO doesn't use GITEA_REPO since it operates across all repositories.


Modular Code Structure (Following Your Preferences)

Single Responsibility Functions

def validate_configuration(config: Dict[str, str]) -> None:
    """
    Validate that all required configuration values are present.
    Raises ValueError if any required values are missing.
    """
    required_keys = ['api_url', 'api_token', 'owner']
    missing = [key for key in required_keys if not config.get(key)]
    
    if missing:
        raise ValueError(f"Missing configuration: {', '.join(missing)}")

def load_system_config() -> Dict[str, str]:
    """
    Load configuration from system-level gitea.env file.
    Returns dictionary of configuration values.
    """
    config_path = Path.home() / '.config' / 'claude' / 'gitea.env'
    
    if not config_path.exists():
        raise FileNotFoundError(f"System config not found: {config_path}")
    
    load_dotenv(config_path)
    
    return {
        'api_url': os.getenv('GITEA_API_URL'),
        'api_token': os.getenv('GITEA_API_TOKEN'),
        'owner': os.getenv('GITEA_OWNER')
    }

def load_project_config() -> Dict[str, Optional[str]]:
    """
    Load project-specific configuration from local .env file.
    Returns dictionary with 'repo' key, value may be None if not configured.
    """
    project_env = Path.cwd() / '.env'
    
    if project_env.exists():
        load_dotenv(project_env, override=True)
    
    return {
        'repo': os.getenv('GITEA_REPO')
    }

def merge_configurations(system: Dict, project: Dict) -> Dict[str, str]:
    """
    Merge system and project configurations.
    Project values override system values where present.
    """
    merged = system.copy()
    merged.update({k: v for k, v in project.items() if v is not None})
    return merged

def main():
    """Main entry point that orchestrates configuration loading"""
    system_config = load_system_config()
    project_config = load_project_config()
    final_config = merge_configurations(system_config, project_config)
    validate_configuration(final_config)
    return final_config

Virtual Environment Management

Creation

# In plugin mcp-server directory
python -m venv .venv

Activation

# Linux/Mac
source .venv/bin/activate

# Windows
.venv\Scripts\activate

Deactivation

deactivate

Cleanup & Rebuild

# Remove old virtual environment
rm -rf .venv

# Create fresh virtual environment
python -m venv .venv

# Activate and reinstall
source .venv/bin/activate
pip install -r requirements.txt

Debugging

Enable Debug Logging

# Add to server.py
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)

Common Issues

Issue: Module not found

# Solution: Ensure PYTHONPATH is set in .mcp.json
"env": {
  "PYTHONPATH": "${CLAUDE_PLUGIN_ROOT}/mcp-server"
}

Issue: Configuration not loading

# Solution: Check file permissions
chmod 600 ~/.config/claude/gitea.env

# Verify file exists
cat ~/.config/claude/gitea.env

Issue: API authentication failing

# Solution: Test token manually
curl -H "Authorization: token YOUR_TOKEN" \
  https://your-gitea.com/api/v1/user

Performance Optimization

Caching with functools

from functools import lru_cache

@lru_cache(maxsize=128)
def get_labels_cached(repo: str) -> List[Dict]:
    """Cached label retrieval to reduce API calls"""
    return self.gitea.get_labels(repo)

Async Operations

import asyncio
import aiohttp

async def fetch_multiple_repos(repos: List[str]) -> List[Dict]:
    """Fetch data from multiple repositories concurrently"""
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_repo_data(session, repo) for repo in repos]
        return await asyncio.gather(*tasks)

Next Steps

  1. Set up system configuration as shown above
  2. Create project configuration in your first repository
  3. Navigate to Phase 1.1 of the implementation plan
  4. Build the MCP server following the structure above
  5. Write tests as you implement each component
  6. Test with real Gitea instance early and often

Key Differences from Node.js Approach

Aspect Node.js Python (Your Choice)
Dependencies package.json requirements.txt
Package Manager npm/yarn pip
Isolation node_modules .venv
Module System ES6 imports Python imports
Async async/await async/await
Type Checking TypeScript Type hints + Pydantic
Testing Jest pytest

Resources


Ready to build! 🚀