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.
- Python 3.12 or higher
- uv package manager (recommended) or pip
- Git
- Valid Cycloid API credentials
-
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.
# Run the development server
make dev-server
# Or manually:
uv run python server.py# 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:latestThe 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# 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=htmlThe 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
- 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
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)
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
Create a new directory in src/components/ for your feature:
mkdir src/components/your-feature/# 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")# 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)# 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)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.dataThe component will be automatically discovered and registered - no changes to server.py needed!
- 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
-
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
- 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
- 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
- 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
- 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
type(scope): description
[optional body]
[optional footer]
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, etc.)refactor: Code refactoringtest: Adding or updating testschore: Maintenance tasks
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.
- 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
- Code follows style guidelines
- Tests pass (
make test) - Type checking passes (
make type-check) - Linting passes (
make lint) - Documentation updated
- Commit messages follow guidelines
- 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
By contributing to this project, you agree that your contributions will be licensed under the MIT License.