Contributing to Cycloid MCP Server
September 1, 2025 · View on GitHub
Thank you for your interest in contributing to the Cycloid MCP Server! This document provides guidelines for setting up your development environment and contributing to the project.
Development Environment Setup
Prerequisites
- Python 3.12 or higher
- uv package manager (recommended) or pip
- Git
- Valid Cycloid API credentials
Quick Start
-
Clone the repository
git clone <repository-url> cd cycloid-mcp-server -
Set up the development environment with uv (recommended)
# Install uv if you haven't already curl -LsSf https://astral.sh/uv/install.sh | sh # Setup development environment make setup -
Alternative: Set up with pip
# Create virtual environment python -m venv .venv source .venv/bin/activate # On Windows: .venv\Scripts\activate # Install dependencies pip install -e . -
Configure MCP client
Set up your MCP client (like Cursor) to use the development server. See MCP Configuration Examples for detailed instructions.
Development Workflow
Running the Server
Development Server (Python Virtual Environment)
# Run the development server
make dev-server
# Or manually:
uv run python server.py
Production Server (Docker)
# Build the production image
make build
# Run the production server
make prod-server
# Or manually:
docker run --rm -i \
-e CY_ORG=your-organization \
-e CY_API_KEY=your-api-key \
-e CY_API_URL=https://http-api.cycloid.io \
cycloid-mcp-server:latest
Code Quality Checks
The project uses several tools to maintain code quality. Run these before committing:
# Run all quality checks (tests + type checking + linting)
make quality-check
# Individual checks:
make test # Run all tests
make type-check # Type checking with Pyright
make lint # Linting with flake8
make format # Format code with Black and isort
Testing
# Run all tests
make test
# Run specific test file
uv run pytest tests/test_stack_component.py -v
# Run with coverage
uv run pytest tests/ --cov=src --cov-report=html
Project Structure
Component Architecture with BaseHandler Pattern
The server uses an optimized dynamic component registration system with BaseHandler inheritance and centralized utilities:
src/
├── base_handler.py # Base class for all handlers
├── types.py # Centralized type definitions
├── error_handling.py # Unified error handling system
├── components/
│ ├── catalogs/ # Catalog management components
│ │ ├── catalogs_tools.py
│ │ ├── catalogs_resources.py
│ │ └── catalogs_handler.py # Inherits from BaseHandler
│ └── stacks/ # Stack management components
│ ├── stacks_tools.py
│ ├── stacks_resources.py
│ ├── stacks_handler.py # Inherits from BaseHandler
│ ├── stackforms_tools.py
│ ├── stackforms_handler.py # Inherits from BaseHandler
│ └── constants.py # Minimal constants only
├── cli_mixin.py # CLI execution utilities
├── component_registry.py # Automatic component discovery
├── config.py # Configuration management
└── exceptions.py # Custom exceptions
Key Architectural Improvements
- BaseHandler Pattern: All handlers inherit from
BaseHandlerfor consistent CLI access and logging - Centralized Types: Common types defined in
src.typesfor better type safety - Unified Error Handling:
@handle_errorsdecorator for consistent error management - Template Externalization: Large templates moved to separate
.mdfiles with caching - Memory Optimization: Conditional debug logging and proper resource management
- Import Organization: Standardized import order across all files
Component Patterns
Each component follows an optimized consistent pattern:
-
*_handlers.py: Core utilities and private functions- MUST inherit from
BaseHandler(getsself.cliandself.loggerautomatically) - Use
@handle_errorsdecorator for consistent error handling - Import types from
src.types(JSONDict, JSONList, etc.) - Use proper import organization (stdlib → third-party → local → relative)
- MUST inherit from
-
*_tools.py: MCP tools (@mcp_tooldecorated functions)- Inherits from
MCPMixin - Uses handler for core logic
- Proper type annotations from
src.types
- Inherits from
-
*_resources.py: MCP resources (@mcp_resourcedecorated functions)- Inherits from
MCPMixin - Uses handler for core logic
- Proper type annotations from
src.types
- Inherits from
-
*_prompts.py: MCP prompts (@mcp_promptdecorated functions) (if needed)
Automatic Registration
Components are automatically discovered and registered by the ComponentRegistry:
- Discovery: Scans
src/components/for files matching the patterns - Loading: Imports modules and finds classes inheriting from
MCPMixin - Registration: Uses FastMCP's
register_all()method to register tools/resources - Logging: Provides detailed logging of the registration process
Adding New Components
1. Create Component Files
Create a new directory in src/components/ for your feature:
mkdir src/components/your-feature/
2. Implement Handler Class
# src/components/your-feature/your_feature_handler.py
"""Your feature handler utilities and core logic."""
# Standard library imports
import asyncio
from typing import Optional
# Third-party imports
from fastmcp.utilities.logging import get_logger
# Local imports
from src.base_handler import BaseHandler
from src.cli_mixin import CLIMixin
from src.error_handling import handle_errors
from src.types import JSONDict, JSONList, OptionalString
class YourFeatureHandler(BaseHandler):
"""Core your feature operations and utilities."""
def __init__(self, cli: CLIMixin):
"""Initialize handler with BaseHandler."""
super().__init__(cli) # Provides self.cli and self.logger automatically
@handle_errors(
action="fetch your feature data",
suggestions=["Check API connectivity", "Verify permissions", "Review CLI configuration"]
)
async def get_data(self) -> JSONList:
"""Get data from CLI with unified error handling."""
data = await self.cli.execute_cli("your-command", ["args"])
return self.cli.process_cli_response(data, list_key="items")
3. Implement Tools Class (OPTIMIZED)
# src/components/your-feature/your_feature_tools.py
"""Your feature MCP tools."""
# Standard library imports
import json
# Third-party imports
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
# Local imports
from src.cli_mixin import CLIMixin
from src.types import JSONList, OptionalString
from .your_feature_handler import YourFeatureHandler
class YourFeatureTools(MCPMixin):
"""Your feature MCP tools with optimized patterns."""
def __init__(self, cli: CLIMixin):
"""Initialize tools with CLI mixin."""
super().__init__()
self.handler = YourFeatureHandler(cli)
@mcp_tool(
name="your_tool_name",
description="Description of what this tool does",
enabled=True,
)
async def your_tool_method(self, param: str) -> str:
"""Your tool implementation."""
# Error handling is done by @handle_errors in handler
data = await self.handler.get_data()
# Process and return data
return json.dumps(data, indent=2)
4. Implement Resources Class
# src/components/your-feature/your_feature_resources.py
"""Your feature MCP resources."""
# Standard library imports
import json
# Third-party imports
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_resource
# Local imports
from src.cli_mixin import CLIMixin
from src.types import JSONDict
from .your_feature_handler import YourFeatureHandler
class YourFeatureResources(MCPMixin):
"""Your feature MCP resources with optimized patterns."""
def __init__(self, cli: CLIMixin):
"""Initialize resources with CLI mixin."""
super().__init__()
self.handler = YourFeatureHandler(cli)
@mcp_resource("cycloid://your-resource")
async def get_your_resource(self) -> str:
"""Get your resource data."""
# Error handling is done by @handle_errors in handler
data = await self.handler.get_data()
result = {
"items": data,
"count": len(data)
}
return json.dumps(result, indent=2)
5. Add Tests
Create comprehensive test files using the optimized patterns:
# tests/test_your_feature_component.py
"""Tests for YourFeature component using optimized patterns."""
# Standard library imports
import pytest
from unittest.mock import AsyncMock, patch
# Third-party imports
from fastmcp import FastMCP, Client
# Local imports
from src.components.your_feature.your_feature_tools import YourFeatureTools
from src.components.your_feature.your_feature_resources import YourFeatureResources
from src.components.your_feature.your_feature_handler import YourFeatureHandler
from src.cli_mixin import CLIMixin
@pytest.fixture
def your_feature_server():
"""Create a test MCP server with your feature components."""
server = FastMCP("TestYourFeatureServer")
# Initialize CLI mixin
cli = CLIMixin()
# Create and register components
tools = YourFeatureTools(cli)
resources = YourFeatureResources(cli)
tools.register_all(server)
resources.register_all(server)
return server
class TestYourFeatureHandler:
"""Test YourFeatureHandler with BaseHandler inheritance."""
@pytest.fixture
def handler(self):
"""Create handler instance."""
cli = CLIMixin()
return YourFeatureHandler(cli)
def test_handler_inherits_from_base_handler(self, handler):
"""Test that handler inherits from BaseHandler."""
from src.base_handler import BaseHandler
assert isinstance(handler, BaseHandler)
assert hasattr(handler, 'cli')
assert hasattr(handler, 'logger')
@patch('src.components.your_feature.your_feature_handler.CLIMixin')
async def test_your_feature_tools(mock_cli_class, your_feature_server):
"""Test YourFeature tools functionality."""
# Mock setup
mock_cli = AsyncMock()
mock_cli_class.return_value = mock_cli
mock_cli.execute_cli.return_value = [{"test": "data"}]
mock_cli.process_cli_response.return_value = [{"test": "data"}]
# Test using FastMCP Client
async with Client(your_feature_server) as client:
result = await client.call_tool("your_tool_name", {"param": "test"})
assert "data" in result.data
4. No Manual Registration Required
The component will be automatically discovered and registered - no changes to server.py needed!
Code Style Guidelines
Python Code Style
- Follow PEP 8 guidelines with optimized import organization
- Use type hints from
src.typesfor all function parameters and return values - Keep functions focused and under 50 lines when possible
- Use meaningful variable and function names
- Apply proper import organization: stdlib → third-party → local → relative
Architecture Patterns
-
BaseHandler Inheritance: All handlers MUST inherit from
src.base_handler.BaseHandler -
Centralized Types: Import types from
src.typesinstead oftypingdirectly -
Unified Error Handling: Use
@handle_errorsdecorator for consistent error management -
Memory Optimization: Use conditional debug logging and proper resource management
Error Handling
- Use
@handle_errorsdecorator fromsrc.error_handlingfor consistent error handling - Provide descriptive action names and helpful suggestions
- Let the decorator handle
CycloidCLIErrorvsExceptionautomatically - Use
self.logger(provided byBaseHandler) for consistent logging - Return user-friendly error messages with consistent formatting
Type Safety
- Import common types from
src.types:JSONDict,JSONList,CliFlags, etc. - Avoid
Anyusage - prefer specific type aliases - Use
ElicitationResult,StackCreationParamsfor complex operations - Maintain comprehensive type annotations throughout
Performance & Memory
- Use conditional debug logging:
if logger.isEnabledFor(logger.DEBUG): logger.debug(...) - Apply
@lru_cacheonly for static data (templates, file discovery) - NEVER cache CLI API responses (dynamic data)
- Use
tempfile.NamedTemporaryFile(delete=True)for automatic resource cleanup - Use
execute_cli_commandwithauto_parseparameter to avoid duplication
Testing
- Write unit tests for all new functionality using optimized patterns
- Test
BaseHandlerinheritance and@handle_errorsdecorator usage - Include integration tests for complex workflows
- Test edge cases and error conditions with new error handling system
- Maintain test coverage with proper mocking of CLI operations
Commit Guidelines
Commit Message Format
type(scope): description
[optional body]
[optional footer]
Types
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, etc.)refactor: Code refactoringtest: Adding or updating testschore: Maintenance tasks
Examples
feat(stacks): add blueprint validation tool
Add new MCP tool for validating blueprint configurations
before stack creation.
Closes #123
fix(catalogs): handle empty repository list
Return empty table instead of error when no repositories
are found.
Pull Request Process
- Create a feature branch from
main - Make your changes following the guidelines above
- Add tests for new functionality
- Run quality checks to ensure code quality
- Update documentation if needed
- Submit a pull request with a clear description
PR Checklist
- Code follows style guidelines
- Tests pass (
make test) - Type checking passes (
make type-check) - Linting passes (
make lint) - Documentation updated
- Commit messages follow guidelines
Getting Help
- Issues: Use GitHub issues for bug reports and feature requests
- Discussions: Use GitHub discussions for questions and general discussion
- Documentation: Check the README.md and MCP Configuration Examples
License
By contributing to this project, you agree that your contributions will be licensed under the MIT License.