This guide is for contributors who want to develop M4 locally.
git clone https://github.com/hannesill/m4.git
cd m4
uv venv
uv syncuv run m4 init mimic-iv-demo# Initialize a dataset (downloads demo data if needed)
uv run m4 init mimic-iv-demo
# Materialize derived concept tables (MIMIC-IV only)
# Requires a database initialized with current M4 schema mapping.
# If you get a "Required schemas not found" error, reinitialize first:
# uv run m4 init mimic-iv --force
uv run m4 init-derived mimic-iv
# List available derived tables without materializing
uv run m4 init-derived mimic-iv --list
# Switch active dataset
uv run m4 use mimic-iv
# Show active dataset status (detailed view)
uv run m4 status
# List all datasets (compact table)
uv run m4 status --all
# Show per-table derived materialization status
uv run m4 status --derived# Auto-configure Claude Desktop
uv run m4 config claude
# Generate config for other clients
uv run m4 config --quick# Run all tests
uv run pytest -v
# Run specific test file
uv run pytest tests/test_mcp_server.py -v
# Run tests matching pattern
uv run pytest -k "test_name" -v
# Lint and format
uv run pre-commit run --all-files
# Lint only
uv run ruff check src/
# Format only
uv run ruff format src/Point your MCP client to your local development environment:
{
"mcpServers": {
"m4": {
"command": "/absolute/path/to/m4/.venv/bin/python",
"args": ["-m", "m4.mcp_server"],
"cwd": "/absolute/path/to/m4"
}
}
}The active backend is configured via m4 backend duckdb (or bigquery), not through the MCP env block.
M4 has three main layers:
MCP Layer (mcp_server.py)
│
├── Exposes tools via Model Context Protocol
└── Thin adapter over core functionality
Core Layer (src/m4/core/)
│
├── datasets.py - Dataset definitions and modalities
├── tools/ - Tool implementations (tabular, notes, management)
├── backends/ - Database backends (DuckDB, BigQuery)
└── derived/ - Derived concept tables (vendored mimic-code SQL)
Infrastructure Layer
│
├── data_io.py - Download, convert, initialize databases
├── cli.py - Command-line interface
└── config.py - Configuration management
Tools declare required modalities to specify which data types they need:
class ExecuteQueryTool:
required_modalities = frozenset({Modality.TABULAR})The ToolSelector automatically filters tools based on the active dataset's modalities. If a dataset lacks a required modality, the tool returns a helpful error message instead of failing silently.
The Backend protocol defines the interface for query execution:
class Backend(Protocol):
def execute_query(self, sql: str, dataset: DatasetDefinition) -> QueryResult: ...
def get_table_list(self, dataset: DatasetDefinition) -> list[str]: ...Implementations:
DuckDBBackend- Local Parquet files via DuckDB viewsBigQueryBackend- Google Cloud BigQuery
M4 uses a protocol-based design (structural typing). Tools don't inherit from a base class - they simply implement the required interface.
- Create the tool class in
src/m4/core/tools/:
from dataclasses import dataclass
from m4.core.datasets import DatasetDefinition, Modality
from m4.core.tools.base import ToolInput, ToolOutput
# Define input parameters
@dataclass
class MyNewToolInput(ToolInput):
param1: str
limit: int = 10
# Define tool class (no inheritance needed!)
class MyNewTool:
"""Tool description for documentation."""
name = "my_new_tool"
description = "Description shown to LLMs"
input_model = MyNewToolInput
output_model = ToolOutput
# Modality constraints (use frozenset!)
required_modalities: frozenset[Modality] = frozenset({Modality.TABULAR})
supported_datasets: frozenset[str] | None = None # None = all compatible
def invoke(
self, dataset: DatasetDefinition, params: MyNewToolInput
) -> ToolOutput:
"""Execute the tool."""
# Implementation here
return ToolOutput(result="Success")
def is_compatible(self, dataset: DatasetDefinition) -> bool:
"""Check if tool works with this dataset."""
if self.supported_datasets and dataset.name not in self.supported_datasets:
return False
if not self.required_modalities.issubset(dataset.modalities):
return False
return True- Register it in
src/m4/core/tools/__init__.py:
from .my_module import MyNewTool
def init_tools():
ToolRegistry.register(MyNewTool())- Add the MCP handler in
mcp_server.py:
@mcp.tool()
@require_oauth2
def my_new_tool(param1: str, limit: int = 10) -> str:
dataset = DatasetRegistry.get_active()
result = _tool_selector.check_compatibility("my_new_tool", dataset)
if not result.compatible:
return result.error_message
tool = ToolRegistry.get("my_new_tool")
return tool.invoke(dataset, MyNewToolInput(param1=param1, limit=limit)).result- Formatter: Ruff (line-length 88)
- Type hints: Required on all functions
- Docstrings: Google style on public APIs
- Tests: pytest with
asyncio_mode = "auto"
Tests mirror the src/m4/ structure:
tests/
├── test_mcp_server.py
├── core/
│ ├── test_datasets.py
│ ├── tools/
│ │ └── test_tabular.py
│ └── backends/
│ └── test_duckdb.py
Run the full test suite before submitting PRs:
uv run pre-commit run --all-filesThe derived table SQL in src/m4/core/derived/builtins/mimic_iv/ is vendored from the mimic-code repository. When mimic-code releases updated SQL (e.g., bug fixes or new concept tables), follow these steps to update:
-
Check upstream changes: Review the mimic-code repository for changes to the
mimic-iv/concepts_duckdb/directory. -
Copy updated SQL files: Replace the corresponding files under
src/m4/core/derived/builtins/mimic_iv/. Preserve the existing directory structure (score/, sepsis/, medication/, etc.). -
Update the orchestrator: If new tables were added or execution order changed, update
duckdb.sqlto reflect the new.readdirectives from mimic-code's orchestrator. -
Test materialization: Run
m4 init-derived mimic-ivagainst a local MIMIC-IV database to verify all tables build successfully. -
Update documentation: If new table categories or tables were added, update
docs/TOOLS.md(Derived Table Categories section) andREADME.md.
The vendored approach means M4 works offline and ensures reproducibility -- users get the exact SQL version bundled with their M4 release, regardless of upstream changes.
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Run
uv run pre-commit run --all-files - Submit a PR with a clear description
For containerized development:
Local (DuckDB):
docker build -t m4:lite --target lite .
docker run -d --name m4-server m4:lite tail -f /dev/nullBigQuery:
docker build -t m4:bigquery --target bigquery .
docker run -d --name m4-server \
-e M4_BACKEND=bigquery \
-e M4_PROJECT_ID=your-project-id \
-v $HOME/.config/gcloud:/root/.config/gcloud:ro \
m4:bigquery tail -f /dev/nullMCP config for Docker:
{
"mcpServers": {
"m4": {
"command": "docker",
"args": ["exec", "-i", "m4-server", "python", "-m", "m4.mcp_server"]
}
}
}