Contributions are welcome — bug reports, new analysis methods, documentation improvements, and feature requests.
# Fork and clone
git clone https://github.com/YOUR_USERNAME/ChatSpatial.git
cd ChatSpatial
# Create environment and install
python3 -m venv venv && source venv/bin/activate
pip install -e ".[dev]"
# Verify
pytest tests/unit/ -xPrerequisites: Python 3.11-3.14, Git. For R-based methods (RCTD, CellChat, SPARK-X, etc.): R 4.4+ and rpy2.
chatspatial/
├── server.py # MCP tool definitions (entry point)
├── spatial_mcp_adapter.py # ToolContext and data manager
├── config.py # Runtime configuration
├── tools/ # Analysis implementations
│ ├── spatial_genes.py # SpatialDE, SPARK-X, FlashS
│ ├── spatial_domains.py # SpaGCN, STAGATE, GraphST, BANKSY, Leiden
│ ├── cell_communication.py # FastCCC, LIANA, CellPhoneDB, CellChat (`cellchat_r`)
│ ├── deconvolution/ # FlashDeconv, Cell2location, RCTD, etc.
│ ├── visualization/ # 11 plot types
│ └── ...
├── models/
│ ├── data.py # Pydantic parameter models
│ └── analysis.py # Pydantic result models
└── utils/
├── mcp_utils.py # @mcp_tool_error_handler decorator
├── exceptions.py # Custom exception classes
├── adata_utils.py # AnnData validation helpers
└── dependency_manager.py # Optional dependency checking
This is the most common contribution. Follow the existing pattern:
class YourMethodParameters(BaseModel):
method: Literal["method_a", "method_b"] = Field(
default="method_a",
description="Which algorithm to use.",
)
n_top_genes: Optional[int] = Field(
default=None, description="Number of top genes to return."
)class YourMethodResult(BaseModel):
data_id: str
method: str
n_genes_analyzed: int
results_key: Optional[str] = Nonefrom ..utils.exceptions import DataError, ProcessingError
from ..utils.dependency_manager import require
async def your_method(
data_id: str,
ctx: "ToolContext",
params: YourMethodParameters,
) -> YourMethodResult:
"""Implement your analysis."""
require("optional_package") # Checks at runtime, clear error if missing
adata = await ctx.get_adata(data_id)
# ... analysis logic ...
return YourMethodResult(...)@mcp.tool()
@mcp_tool_error_handler()
async def your_tool(
data_id: str,
params: Optional[YourMethodParameters] = None,
context: Optional[Context] = None,
) -> YourMethodResult:
"""Brief description for LLM tool selection."""
ctx = ToolContext(_data_manager=data_manager, _mcp_context=context)
p = _resolve_params(params, YourMethodParameters)
return await your_method(data_id, ctx, p)# tests/unit/test_your_tool.py
@pytest.mark.asyncio
async def test_your_method_basic(minimal_spatial_adata, monkeypatch):
# Mock external dependencies, test logic
...- Parameter model with Pydantic validation
- Result model following existing patterns
- Implementation using
ToolContext(not raw data_store dict) - Optional dependencies handled via
require() - MCP tool registered with
@mcp_tool_error_handler() - Unit tests with mocked dependencies
- Docstrings on public functions
# Format and lint
black chatspatial/
isort chatspatial/
ruff check chatspatial/ --fix
# Type check
mypy chatspatial/- Max line length: 88 (Black default)
- Type hints on all public functions
- Imports: stdlib, third-party, local (isort handles this)
pytest tests/unit/ # Fast, no external deps
pytest tests/integration/ # Multi-component workflows
pytest tests/e2e/ # Full MCP tool calls
# Pre-PR quality gate
make test-gates- Unit tests: mock external packages, test logic in isolation
- Integration tests: test tool dispatch and result storage
- Keep test data small (<1000 spots, <500 genes)
- Set random seeds for reproducibility
- Create a branch:
git checkout -b feature/your-feature - Make changes, run tests and linting
- Commit with clear messages:
feat: add X method for Y analysis - Open a PR against
main
feat: add new spatial analysis method
fix: handle edge case in deconvolution
docs: update methods reference
test: add integration test for trajectory
Releases follow one tested path rather than a separate set of manual commands:
- Update the version in
pyproject.tomland the matching changelog entry. - Merge to
mainand wait for CI to pass before deciding to release. - Create an annotated
vX.Y.Ztag on a commit contained inmainand push only that tag. - The tag workflow calls the same CI definition on the tagged commit. Its
release-readiness job builds and audits the wheel and sdist once, and the
remaining release jobs consume those immutable artifacts from the same
workflow run. Separate single-purpose jobs create a draft GitHub Release,
publish the same files to PyPI with trusted publishing, and make the GitHub
Release public only after the PyPI upload succeeds. The final jobs then
register that exact release with the MCP Registry and build its versioned,
latest, and commit-addressed GHCR image once. Repository write access, package write access, PyPI's OIDC token, and the MCP Registry's OIDC token are never granted to the same job.
The release frontend and build backend versions are intentionally pinned in
pyproject.toml and constraints/release-build.txt. Runtime dependencies remain
ranges because ChatSpatial is a library; CI tests the newest allowed resolver
result instead of embedding one development machine's lockfile.
Do not move a tag after PyPI accepts a version. PyPI releases and their files are immutable; publish a new patch version for any subsequent correction.
The tag workflow has no cross-workflow polling or artifact lookup. Testing, building, provenance validation, and publication form one explicit dependency graph, so publication cannot start before its own tagged quality gate succeeds. The Docker workflow is part of that graph rather than a second tag listener, so one release cannot start duplicate image builds or publish an image before its GitHub Release is public.
For a non-publishing rehearsal, manually dispatch CI with Build and retain the audited wheel and source distribution enabled. That exercises the shared quality and build definition and retains its artifacts, but has no path to any external publishing job.
To retry only MCP Registry publication, dispatch Publish to MCP Registry with the immutable GitHub release tag. The workflow checks out that tag, verifies that its GitHub Release is public and that all package versions agree, then compares the exact version with the Registry. Identical canonical metadata is a successful no-op; a conflicting immutable version fails without attempting to overwrite it.
- Bugs: include a minimal reproducible example, error traceback, and
pip show chatspatialoutput - Feature requests: describe the use case and suggest which tool category it fits
Open a GitHub Discussion or check the docs.