modal-compose is a library for declaratively defining dev-boxes: single
Modal Sandboxes built from layered container images and
exposed over an MCP server, so an agent (Claude, or anything else that speaks
MCP) can spin one up, work in it with file and shell tools, and tear it down
when it's done. You describe each dev-box as a Repo made of Layers — where
the code comes from, how the image is built, what runs on start/stop — and
modal-compose turns that into a running Modal Sandbox plus the MCP tools to
drive it.
modal-compose is a uv-managed project and
requires Python 3.12+.
uv add modal-composeor, to install the CLI as a tool:
uv tool install modal-composeScaffold a new dev-box project with the CLI:
uv run modal-compose init my-devbox
cd my-devbox
uv syncThis copies a small, fully-yours project template into my-devbox/:
my-devbox/
├── pyproject.toml
└── devbox/
├── registry.py # mounts each Repo into a Registry
├── repos/
│ └── modal.py # example Repo: clones modal-labs/modal-client
└── services.py # the Modal app: MCP server + image-prebake cron
Add a repo to the registry with modal-compose add, which scaffolds a new
Repo module under devbox/repos/ and mounts it in devbox/registry.py for
you:
uv run modal-compose add owner/my-serviceThat generates devbox/repos/my_service.py:
from modal_compose.layer import Layer, Repo
from modal_compose.remote import GitHubRemote
layer = Layer(
name="my_service",
source=GitHubRemote(
repo="owner/my-service", ref="main", working_directory="/workspace/my_service"
),
)
repo = Repo(layers=[layer])and mounts it:
from .repos import my_service
registry.mount("my_service", my_service.repo)Attach build steps or lifecycle hooks directly on the Layer:
from modal import Image
from modal_compose.layer import LayerContext
@layer.build
def install(image: Image, ctx: LayerContext) -> Image:
return image.run_commands(f"cd {ctx.working_directory} && pip install -e .")Deploy the app (the HTTP MCP server plus the image-prebake cron):
uv run python -m devbox.servicescreate_sandbox(repo=...) only accepts repos you've mounted — the choices
are generated from the registry and baked into the tool's JSON schema as an
enum. It launches the repo's prebaked named image, falling back to building
on the request path if no image has been published yet.
Use --force with either init or add to overwrite existing files, and
--name / --ref / --directory on add to control the module name, git
ref, and target project directory.
-
Registry— holds the namedRepos available to launch, a shared basemodal.Imagecommon to all of them, and a set of commonmodal.Secrets injected into every sandbox.registry.mount(name, repo)registers aRepounder a name;registry.image_for(name)resolves its full image. -
Layer/Repo— aLayeris one stage of a dev-box: it requires asource(aRemote), an optional list ofbuildsteps that transform themodal.Image, andon_start/on_terminateruntime hooks. ARepois an ordered list of one or moreLayers; its image is built by applying each layer'ssource.provision(...)andbuildsteps in declaration order (not a topological sort). -
Remote— the source strategy aLayerprovisions from.GitHubRemoteclones a GitHub repo into the image at build time andgit pulls it when a sandbox starts; for private repos, it uses a GitHub App installation token (minted viamodal_compose.github_appand brokered through a sidecar credential vault,_github_vault.py) so no long-lived credential is baked into the image. -
Engine— owns the single-sandbox lifecycle for aRepo: building its image (image()/build()), creating aRun(aSandboxplus any sidecars, withon_starthooks fired) viacreate(), and tearing it down (on_terminatehooks, thensandbox.terminate()) viaterminate(). -
The MCP server —
create_server(registry, ...)assembles a FastMCP server. Every keyword argument you pass is forwarded straight through toFastMCP(...), so the server's name, instructions, auth, and providers all come from FastMCP itself. It wires up two lifecycle tools —create_sandbox(repo=...), which builds/launches a registered repo's sandbox and returns itssandbox_id, andkill_sandbox(sandbox_id=...), which terminates it — and auto-discovers the file and shell tools bundled inmodal_compose/tools/:bash,edit,glob,grep,read, andwrite, the primitives an agent uses to actually work inside the sandbox once it's running.
Apache License 2.0 — see LICENSE.