Development services
A Service is a long-running process you declare on a session: the dev server, a database, Storybook. It runs inside the session workspace, next to the agent and the worktree, and like every other session process it keeps running when you disconnect. Mend never scans the workspace for listeners and never exposes anything you did not name; a Service exists because you declared it.
Opening the app
Section titled “Opening the app”A workspace is a container with no published ports. When Vite listens on port 3000 in there, that port exists only inside the container, and your machine cannot see it. So you declare it:
mend service run --port 3000 --http -- pnpm devMend supervises the command, waits for port 3000 to answer, and opens a port on your machine (your
laptop, not the Mend host), loopback only: http://127.0.0.1:43127. Open it. That is your app, hot
reload and all, because Mend pipes raw bytes and rewrites nothing. Pass --no-connect to start the
Service without opening that port here.
Mend waits up to a minute for the port to answer. A script that starts a slow Service (a build, an
install, a cold boot) passes --wait: Mend keeps waiting while the Service’s process runs and its
port has not answered, up to --timeout (10 minutes by default), and exits 0 once it answers, 2
when the process ended first, and 124 when it is still starting at the timeout. A waited start opens
no port here; mend service connect does that afterwards. The exit codes are in the
CLI reference.
One exception: when the CLI’s server URL is localhost, mend service run treats the server’s own
forward as your machine and prints it instead of tunnelling. On the Docker deployment that forward
lives inside the application container, so run mend service connect <name> there to bring the port
to your loopback.
Behind that port, Mend bridges each TCP connection through to the dev server:
browser ──TCP──▶ Mend's listener on your machine (:43127) ──WS───▶ Sealant API ──pipe─▶ sealantd, inside the container ──TCP──▶ Vite on 127.0.0.1:3000 (an ordinary local connect)The dev server needs no configuration and no rebinding to 0.0.0.0. To it, every connection looks
like a client on its own loopback.
That is the only port in the picture, and it lives on your machine, bound to 127.0.0.1. It is not
opened on the Mend host and not on any network interface. When the server is remote, the CLI stays
running to hold the port locally and bridges the bytes over the authenticated connection it already
has to the server; Ctrl-C closes the bridge, not the Service. There is nothing to expose, nothing to
firewall, and nothing ever touches the public internet.
A Service the agent or the web started needs no command of yours. While mend attach (or
mend codex, mend rejoin, the dashboard) is attached to a session on a remote server, every live
Service of that session declared --http or --https is tunneled the same way, on its own port
when that port is free here: web → http://localhost:5173. The tunnel closes when the Service stops
or when you detach. --no-tunnel turns this off. The web app, looking at a remote server, shows
mend service connect <name> where an Open link to the server’s loopback would not load.
Declaring
Section titled “Declaring”You can declare a Service from three places, and the result is the same:
- A recipe in
mend.toml. The repository’s own declaration: every session can startwebby name, and the recipe travels with the code. - The CLI, wrapping the command you already run in
mend service run. - The agent itself. Each workspace has a scoped-down
mendon its PATH that can run, adopt, list, stop, and restart Services for its own session. When the agent starts a dev server, it can declare it properly instead of leaving a listener nobody can reach. It cannot open ports or change exposure; that authority stays on the server.
A Service can also adopt a port that something else already listens on inside the workspace. Adoption makes it reachable without supervision: there is no Mend-owned process to restart and no log beyond what started it.
Writing mend.toml
Section titled “Writing mend.toml”Recipes live in a mend.toml at the repository root, one table per Service:
[service.web]command = "pnpm dev"port = 3000browserScheme = "http"
[service.db]# no command: adopt a listener something else starts (a compose sidecar, a daemon)port = 5432
[service.game]command = "node server.js"port = 9000protocol = "udp"The fields:
portis the only required field: where the process listens inside the workspace, 1–65535.commandis the shell command Mend supervises. Leave it out for an adopt-only recipe: Mend binds the listener but supervises nothing.protocolis"tcp"unless you say"udp".browserScheme("http"or"https") is what gives a Service its Open action. TCP alone never implies HTTP, and a UDP recipe cannot declare one.
The table name is the lookup key (mend service web): lowercase letters and digits plus ., _,
and -, up to 64 characters.
Mend reads the file from the session’s worktree, not from the project’s main branch. In a captured
workspace (the default store) the worktree lives only in the workspace, so Mend reads
/workspace/repo/mend.toml from the live workspace. A session with no live workspace lists only the
project’s recipes, and mend service <name> asks you to resume it first. Two things follow. An
agent can add a recipe as part of its change, and the addition reviews like any other edit. And two
sessions on different branches can carry different recipes. A malformed file is a named error, never
a guess; a missing file means no declared recipes. Recipes declared on the project in the web app
join the same set, and on a name collision the file wins, because it travels with the code.
mend service init scaffolds the file from the package and Compose files it finds, and shows you
the result before writing it.
Records and status
Section titled “Records and status”Every start is an attempt with a recorded log you can replay and then follow live.
mend service list prints each Service’s id and its current attempt’s process id, which
mend logs --process and mend wait --process take; mend logs --service <name> reads the current
attempt’s log. Restarting adds another attempt under the same Service identity and endpoint, so
history accumulates instead of being replaced. Status words are observations: “reachable” means the
declared target answered when Mend checked, not a guarantee about the next request.
A live Service also keeps the session workspace retained after the agent settles, the same way a
detached shell does. Stopping the agent does not stop its Services. The session then reads, for
example, agent stopped · 3 services keep the workspace up, in the web app, the dashboard, and the
output of mend stop. Stop them when you are done, or let them hold the workspace deliberately:
- Stop services on the session page in the web app;
mend stop --services [session]from the terminal;⇧Kin the dashboard, on a row whose agent has stopped while its Services keep the workspace up.
Once nothing is live, the workspace closes.
Commands
Section titled “Commands”Every command is listed in the CLI reference:
mend service run, add (adopt a port something in the workspace already listens on), list,
logs, restart, stop, and connect, recipe scaffolding with mend service init,
mend stop --services for all of a session’s Services at once, and the in-workspace helper’s
smaller set.