Repository navigation
Contributing
hypen-code edited this page Mar 11, 2026
·
3 revisions
Thank you for your interest in contributing to MCE. This page covers everything you need to open a high-quality pull request — from local setup through the definition of done.
- Prerequisites
- Local Setup
- Project Structure
- Development Workflow
- Branch and Commit Conventions
- Pull Request Process
- Coding Standards
- Testing Requirements
- Definition of Done
- Getting Help
| Tool | Minimum version |
|---|---|
| Python | 3.13 |
| Docker | 24.0 |
| Git | 2.40 |
| pre-commit | 3.x |
# 1. Fork the repo on GitHub, then clone your fork
git clone https://github.com/<your-username>/mcp-code-execution.git
cd mcp-code-execution
# 2. Install in editable mode with all dev dependencies
pip install -e ".[dev]"
# 3. (Optional) Install LLM enhancement support
pip install -e ".[llm]"
# 4. Install pre-commit hooks
pre-commit install
# 5. Copy and configure environment
cp .env.example .env
# Edit .env — you do not need real API keys for unit tests
# 6. Build the sandbox image (required for executor integration tests)
docker build -t mce-sandbox:latest sandbox/
docker network create mce_network
# 7. Verify everything works
pytestsrc/mce/
├── __init__.py # Version exports
├── __main__.py # CLI entry point (compile, clean, serve, run)
├── server.py # FastMCP tool registration
├── config.py # Pydantic settings — all MCE_* env vars
├── errors.py # Exception hierarchy — all custom exceptions
├── models/__init__.py # ALL Pydantic models — single file, no exceptions
│
├── compiler/ # Code generation pipeline
│ ├── swagger_parser.py # OpenAPI 3.x / Swagger 2.0 parser
│ ├── codegen.py # Jinja2 Python code generator
│ ├── orchestrator.py # Compile pipeline coordinator
│ ├── llm_enhancer.py # Optional LLM docstring improvement
│ └── templates/
│ └── function.py.j2 # Jinja2 template for generated functions.py
│
├── runtime/ # Execution pipeline
│ ├── registry.py # Loads manifests, provides function lookups
│ ├── executor.py # Docker sandbox code execution
│ └── cache.py # Async SQLite code cache
│
├── security/ # Defense layers
│ ├── ast_guard.py # AST static analysis
│ ├── policies.py # Read-only + domain allowlist enforcement
│ └── vault.py # Credential → Docker env var injection
│
└── utils/
├── logging.py # Structlog setup
└── hashing.py # SHA-256 helpers
tests/
├── conftest.py # Shared fixtures
├── fixtures/ # YAML swagger test specs
│ ├── weather_api.yaml
│ ├── hotel_api.yaml
│ └── petstore.yaml
├── unit/ # 13+ fast test modules (no Docker, no network)
└── integration/ # Tests with compiled output
These are not style preferences — they are enforced:
| Rule | Rationale |
|---|---|
All Pydantic models in models/__init__.py
|
Single source of truth; prevents model sprawl |
All exceptions in errors.py
|
Uniform exception hierarchy; easy to audit |
| No new top-level directories | Structure is intentional; discuss with maintainers first |
| Max file length: 400 lines | Keeps modules focused |
| Max function length: 50 lines | Forces decomposition |
No print() — use structlog
|
print() breaks stdio MCP transport |
No requests or urllib — use httpx
|
Consistent async HTTP client |
No os.environ in business logic — use MCEConfig
|
Centralizes config management |
Optional[X] / Union[X,Y] not allowed |
Use `X |
graph TD
main["main (protected — release-ready at all times)"]
main --> feat["feat/your-feature"]
main --> fix["fix/the-bug-description"]
main --> chore["chore/housekeeping-task"]
main --> docs["docs/update-readme"]
- Create a branch from
main. - Make atomic commits — one logical change per commit.
- Run the full quality suite before pushing:
ruff check src/ tests/ # lint
ruff format --check src/ tests/ # format
mypy src/ # type check
pytest # tests + coverage ≥ 90%
pre-commit run --all-files # full hook suiteAll steps must exit 0.
- Open a pull request against
main.
feat/<short-description> # new feature
fix/<short-description> # bug fix
chore/<short-description> # tooling, deps, CI
docs/<short-description> # documentation only
refactor/<short-description> # no behaviour change
test/<short-description> # adding or fixing tests
Follow Conventional Commits:
<type>(<scope>): <imperative summary>
[optional body — explain the *why*, not the *what*]
[optional footer — e.g. Closes #123]
Types: feat, fix, docs, chore, refactor, test, perf, ci
Examples:
feat(server): add run_cached_code tool with parameter injection
fix(executor): handle container timeout when Docker daemon is slow
docs(readme): update env var table to include MCE_DOCKER_HOST
test(ast_guard): add tests for nonlocal statement blocking
chore(deps): bump fastmcp to 2.1.0
Rules:
- Summary line ≤ 72 characters, imperative mood, no trailing period
- Reference issues with
Closes #NorRefs #Nin the footer - Never use
git commit --no-verifyfor feature work
- Fill in the PR template completely. Reviewers will not merge PRs with empty sections.
- Keep PRs focused — one feature or fix per PR. Bundled unrelated changes will be asked to split.
- All CI checks must be green before review begins.
-
Update
README.mdin the same PR if the change affects documented behaviour. - One approving review from a maintainer is required to merge.
- PRs are merged with squash merge to keep
mainhistory linear.
Before marking your PR ready for review, confirm every item:
- New/modified functions have full type annotations (
from __future__ import annotationsat top) - All public functions have Google-style docstrings
-
ruff check src/ tests/exits 0 -
ruff format --check src/ tests/exits 0 -
mypy src/exits 0 -
pytest --cov=mce --cov-fail-under=90exits 0 -
pre-commit run --all-filesexits 0 -
README.mdupdated if documented behaviour changed - No credentials, tokens, or secrets in code, tests, or assertions
- No
print()statements (usestructlogviaget_logger(__name__)) - No new files created outside the defined project structure
-
mce compile --dry-runsucceeds if compiler was touched - Security guard tests updated if
ast_guard.pywas touched
from __future__ import annotations # required in every file
# Use modern union syntax
def foo(value: str | None) -> list[str] | None: ...
# Use lowercase generics
items: list[str] = []
mapping: dict[str, Any] = {}
# NOT this:
from typing import Optional, Union, List, Dict
def foo(value: Optional[str]) -> Optional[List[str]]: ...from mce.utils.logging import get_logger
logger = get_logger(__name__)
# Correct: structured key=value pairs
logger.info("cache_stored", id=entry_id[:12], description=description[:50])
# Wrong: f-strings, print()
logger.info(f"stored cache entry {entry_id}")
print("stored")# MCP tool functions must never raise — always return an error dict
async def execute_code(code: str, description: str) -> dict[str, Any]:
try:
result = await executor.run(code)
return {"success": True, "data": result.data}
except SecurityViolationError as e:
return {"success": False, "error": str(e)}
except Exception as e:
logger.exception("unexpected_error", error=str(e))
return {"success": False, "error": "Internal error"}
# Never use bare except:
try:
...
except: # wrong
pass
except Exception: # correct
...def get_current_weather(city: str, units: str = "metric") -> GetCurrentWeatherResponse:
"""Get current weather conditions for a city.
Args:
city: City name, e.g. 'London'.
units: Temperature units: metric | imperial. Defaults to 'metric'.
Returns:
GetCurrentWeatherResponse containing temperature, condition, and humidity.
Raises:
httpx.HTTPStatusError: If the API returns a non-2xx response.
"""- Coverage gate: ≥ 90% — enforced by the pre-commit hook. PRs that drop coverage below this threshold will not be merged.
-
Unit tests (
tests/unit/) must have no Docker, no network, no real filesystem side effects. - Use
tmp_pathfor SQLite or file I/O in tests — never share state across tests. - Mock
httpxcalls withrespx; mock Docker withunittest.mock.MagicMock. -
Test naming:
test_{unit}_{condition}_{expected_outcome}. - One assertion concept per test — split long tests into smaller focused ones.
Any new blocked pattern in ast_guard.py must include a test:
def test_import_subprocess_blocked(guard: ASTGuard) -> None:
with pytest.raises(SecurityViolationError, match="subprocess"):
guard.validate("import subprocess")
def test_import_allowed_module_passes(guard: ASTGuard) -> None:
guard.validate("import json") # must not raise# All tests with coverage
pytest
# Fast unit tests only (no Docker)
pytest tests/unit/ --no-cov -v
# Integration tests
pytest tests/integration/ --no-cov -v
# Specific file
pytest tests/unit/test_ast_guard.py -v
# With coverage report
pytest --cov=mce --cov-report=htmlA change is complete when all of the following are true:
| Check | Command |
|---|---|
| Ruff lint passes | ruff check src/ tests/ |
| Format passes | ruff format --check src/ tests/ |
| Type check passes | mypy src/ |
| Tests pass with ≥ 90% coverage | pytest --cov-fail-under=90 |
| Pre-commit passes | pre-commit run --all-files |
| README updated (if behaviour changed) | Manual check |
| No credentials in code/tests | Manual check |
No print() statements |
grep -r "print(" src/ |
| Dry-run succeeds (if compiler touched) | mce compile --dry-run |
| Channel | Use for |
|---|---|
| GitHub Discussions | Usage questions, design discussions |
| GitHub Issues | Bug reports, feature requests |
| PR comments | Code review, implementation questions |
| AGENTS.md | Canonical internal development reference for AI agents and human contributors |
This project follows the Contributor Covenant Code of Conduct. By participating you agree to uphold it. Report unacceptable behaviour to the maintainers via a private GitHub issue.