Skip to main content
Glama
Michael-Drake

io.github.Michael-Drake/rentctl

rentctl

A dev server should never outlive the work that needed it.

rentctl rents dev environments to your AI coding sessions. Every environment is a lease: it dies when the session ends, when the lease expires, or when you say so — whichever comes first. Cleanup is owned by code, not by an agent remembering to clean up.

Ships as both a CLI (rent) and an MCP server, so an agent can start and stop environments through tools instead of shelling out to a raw npm run dev it will forget about.

Why

An AI coding session starts a dev server. Then the session ends — crashes, is closed, or just moves on — and the server keeps running. By Friday there are six of them, three are on ports you've forgotten, and one is quietly serving stale code that makes a bug look unreproducible.

Telling the agent to clean up doesn't fix this, because the failure mode is the agent not doing what it was told. rentctl makes the cleanup structural: the environment has an expiry, and something other than the agent enforces it.

Related MCP server: @reachpad/mcp

Install

uv tool install rentctl        # or: pipx install rentctl, or: pip install rentctl

Requires Python 3.12+. macOS and Linux. There is no Windows support and no Windows claim — POSIX sessions are the safety-critical mechanism here, and they have no tested Windows equivalent yet.

Upgrading from 1.0.x? Drain first. Run rent down --all in each project before you upgrade, then restart your agent sessions so their MCP servers load the new version. Skipping it is safe but degraded: a 1.0.x environment keeps working under 1.1 — rent up renews it, and rent down and rent sweep stop it by its whole session — but a 1.0.x watchdog still running from before the upgrade enforces its expiry with the old code, which can miss a child that outlived its shell.

First run

A framework-free server, so nothing but Python is involved. In a scratch repo:

mkdir hello && cd hello && git init -q
cat > rentctl.toml <<'EOF'
[project]
name = "hello"
runner = "process"

[profiles.default]
cmd = 'python3 -m http.server --bind 127.0.0.1 "$PORT"'
cwd = "."
port_env = "PORT"
EOF
rent init

init shows you exactly what it will run and waits for an explicit yes:

Enroll project 'hello' (runner: process)
  source:     /home/you/hello
  port block: 6200-6209

rentctl will run these commands on your behalf:
  [default]
    command: python3 -m http.server --bind 127.0.0.1 "$PORT"
    in:      /home/you/hello
    port via $PORT

Approve and enroll? [y/N] y

Your block will differ — blocks are drawn from 5100 upward, lowest free first. Then:

$ rent up hello
{
  "ok": true,
  "project": "hello",
  …
  "url": "http://localhost:6200",
  "port": 6200,
  …
  "lease_expires": "2026-09-28T15:50:21.327166-05:00",
  "already_running": false,
  "state": "running",
  "supervisor_pid": 24202,
  …
  "readiness": "answered"
}

$ rent ls                      # one row: hello, port 6200, "state": "running", "healthy": true
$ curl -sI http://localhost:6200/
HTTP/1.0 200 OK

$ rent down --all --cwd . --reason session-end    # what the SessionEnd hook runs
{
  "ok": true,
  …
  "downed": [ { "project": "hello", …, "port": 6200, "was_running": true,
                "stopped": null, "pending": true, "state": "stopping",
                "detail": "supervisor 24202 is completing cleanup; rent ls shows the outcome" } ]
}

$ curl -sI http://localhost:6200/ || echo gone
gone

(Output trimmed where marked ….) That last step is the whole product: in an enrolled Claude Code or Gemini CLI session you never type rent down — the session ending does. The hook doesn't wait for the server to die; it asks, and the environment's supervisor finishes the job (see How cleanup actually happens). rent down hello, typed by hand, waits and answers "stopped": true. rent events --summary afterwards shows the teardown and which cleanup layer did it.

If something doesn't behave, see Troubleshooting.

Enroll a project

A project describes itself in a rentctl.toml at its repo root (a devctl.toml left over from before the rename is still read, so nothing needs renaming to keep working):

[project]
name = "myapp"          # lowercase, no separators — it is used as a filename
runner = "process"

[profiles.default]
cmd = 'npm run dev -- --port "$PORT" --strictPort'
cwd = "frontend"        # repo-relative, never absolute
port_env = "PORT"       # rentctl sets this in the child's environment

Then, from the repo:

rent init

init shows you the exact command it will run, asks you to approve it, claims a block of ports for the project, registers the MCP server, installs the cleanup hooks (unless the Claude Code plugin already supplies them), and adds a short rule to CLAUDE.md / GEMINI.md telling agents to start servers through rentctl rather than by hand. Nobody else has to edit anything.

There is no port field. Ports are drawn when a server starts, not written down in a tracked file — so two checkouts of the same repo get different ports instead of fighting, and a config file can't hand out a number the allocator never heard of. Each project owns a contiguous block of 10 ports.

Use

rent up myapp              # start (or renew) — prints the URL and the port
rent down myapp            # stop it; waits up to 15 s (--wait SECONDS, --wait 0 = don't wait)
rent down --all --cwd .    # stop everything leased to this directory
rent ls                    # every environment on this machine, with its state
rent sweep                 # reconcile: stop what's expired or dead
rent events --summary      # what happened, and which cleanup layer did it
rent sync                  # re-approve a changed rentctl.toml
rent doctor                # are the shims, registry and hooks actually working?
rent report-kill myapp --note "…"   # "you killed something I was using"

report-kill exists because rentctl cannot tell, on its own, whether a teardown was unwanted — a kill you asked for and a kill you regret look identical from the inside. The report lands in the same append-only log as everything else and is matched to the teardown it disputes, so an unwanted kill becomes a fact on the record rather than an anecdote.

Leases default to 120 minutes and are capped at 480. rent up on a live lease renews it rather than starting a second server.

Which clients get automatic cleanup

Client

How it is wired

Session-end cleanup

Claude Code

the plugin (guide), or rent init (.mcp.json + .claude/settings.local.json)

Yes — SessionEnd releases the session's claim (rent down --all --cwd … --reason session-end); SessionStart runs rent sweep

Codex

the plugin (guide)

Yes, once you trust the plugin's hooks (/hooks) — the same hooks; until then, expiry and sweep

Gemini CLI

rent init (.gemini/settings.json), when Gemini is used in the project or installed

Yes — same hooks, scoped by $GEMINI_PROJECT_DIR

Any other MCP client

register rent-mcp yourself

No — lease expiry and sweep only

A plain shell

rent

No — lease expiry and sweep only

"Expiry and sweep only" still guarantees the server dies — just at the end of its lease (120 minutes by default), not when you close the window.

Plugin-only installs need the CLI too. The plugin (Claude Code or Codex) calls the rent and rent-mcp you installed with uv tool install rentctl; it does not bundle them, and it deliberately does not fetch its own copy — a second, different rentctl acting on the same leases is worse than none. Without rent on PATH, session-end cleanup is off: every session then starts with a warning saying so and naming the install command. Install the package first, then run rent doctor, which reports whether each project's hooks come from the plugin, from settings.local.json, or from both.

From the MCP Registry, a client runs uvx rentctl@<version>: the rentctl executable is the MCP server, the same as rent-mcp (typed in a terminal it waits for a client on stdin). That gives you the tools with expiry-and-sweep cleanup only — the hooks come from the plugin or rent init. rent --version says which version you are running.

How cleanup actually happens

Every environment has its own supervisor: a small rentctl process that starts your command inside a POSIX session it owns and stays until that whole session — the command, its children, and anything they spawned — has been checked and found empty. The lease is written before the command starts, so there is no moment when something is running that rentctl has no record of.

Every stop, whatever asked for it, is the same: SIGTERM to every process in the session, SIGTERM to anything forked while it shuts down, SIGKILL to each process still there after 10 s, then a final scan. A child that ignores SIGTERM, or that outlived the shell that launched it, is stopped with the rest.

Four independent layers decide when, so no single failure leaves an orphan:

  1. You ask — rent down.

  2. The session ends — a SessionEnd hook releases that session's claim on everything leased to that directory. The server stops if no other session still holds a claim.

  3. The lease expires — the supervisor starts the stop within about a second of the lease running out, even if the session died without running its hook.

  4. The next sweep — rent sweep (which the hooks also run at session start) and rent ls clean up what the first three missed, including an environment whose supervisor was killed.

What rent down tells you is what the lease says, in one of three shapes:

  • "stopped": true — the session was scanned empty and the lease removed.

  • "stopped": false, "state": "cleanup_incomplete", "survivors": [...] — something could not be stopped (a child that switched user with sudo, say). The lease is kept, so the survivors stay named and the port stays out of the draw; the supervisor keeps retrying (5 s, 15 s, 60 s, then every minute), rent down retries at once, and rent up refuses with CLEANUP_INCOMPLETE until it clears.

  • "stopped": null, "pending": true — the stop was accepted and is still running; rent ls shows how it ended. This is "ok": true: the supervisor finishes regardless.

rent down waits up to 15 s for the first two (--wait SECONDS; --wait 0 returns at once). The MCP tool env_down takes the same wait as wait_s — 15 s by default, 60 s at most.

Session end returns fast; cleanup finishes in the background. Claude Code gives a plugin's SessionEnd hook about 1.5 s. So the hook's rent down --all --reason session-end doesn't wait: it records a stop for every lease that no other session still holds, wakes each supervisor, and returns pending. The supervisors don't need the session to finish. A lease with no living supervisor is handed to a detached recovery process rather than stopped halfway.

A crashed session is cleaned up by layers 3 and 4, not 2. If the session dies without firing its hook, its server keeps running until the lease expires — up to the full lease length. Shorter leases (rent up myapp --lease-minutes 30) shorten that window. A sweep does not stop a live, unexpired lease; it cannot tell a crashed session from one that is still working.

If a supervisor is killed, recovery waits for the next command. The server keeps serving. The next rent ls, sweep, up or down notices, marks the lease unsupervised, and records supervisor_lost; it is not killed early. At its expiry the next of those commands stops it, and rent down stops it now. Nothing notices at the moment the supervisor dies, because nothing else is running to notice.

There is no daemon. There is one supervisor per live environment, and it exits once its environment is verified gone. State lives on disk and the OS process table is the source of truth, so there is no background service to babysit, and nothing to resurrect after a reboot.

What supervision covers, per platform

macOS. Every process that stays in the environment's session is found and stopped: orphans of a launch shell that exited, processes that ignore SIGTERM, processes that move into their own process group, children forked during shutdown. A process that calls setsid() — which is what daemonizing (double-fork plus setsid) does — leaves the session and is outside supervision. rentctl never signals it; if it holds the lease's port, the teardown reports it. When a command daemonizes during startup — its own session empties while a process outside it answers the port — rent up fails with UNSUPERVISABLE instead of reporting a start it cannot own. Run such servers in the foreground.

Linux. Each supervisor also makes itself a child subreaper (prctl(PR_SET_CHILD_SUBREAPER), no privileges needed), so orphaned descendants are reparented to it instead of to init. Every descendant of your command is captured, including a daemon that double-forks and calls setsid(). Signals to single processes go through a pidfd where the kernel has them. What still escapes is work that is not your command's descendant — handed to another manager such as systemd-run, a container runtime, an already-running tmux server, or at/cron — and the orphaned children of a process placed in another PID namespace with setns(2) (which needs CAP_SYS_ADMIN over that namespace): they go to that namespace's init. The capture also ends if the supervisor itself is killed. Where the subreaper is refused (some restricted containers), Linux gets the macOS guarantee.

rent doctor reports which one is in effect: supervision: session or supervision: session+subreaper. Neither is a sandbox — a command that sets out to escape supervision can.

Several sessions in one checkout share one server

A lease belongs to a (project, directory) pair, so two agent sessions in the same checkout share one server and one port. Each session that runs rent up holds its own expiring claim on it, and the lease lasts as long as its latest claim. A session's up renews only its own claim. rent ls and the up result list the claims, and shared_with names the other sessions.

When a session ends, its SessionEnd hook releases that session's claim. The server stops only when no other session still holds one. A session that never leased the server changes nothing when it ends. The MCP tool env_down also releases by default: it answers "stopped": false with held_by while others hold the server, and force: true stops it for everyone. rent down typed by a person always stops, and lists any other sessions' claims it overrode in overrode_claims.

This depends on knowing which session is asking. Claude Code hooks send the session id on stdin, and rentctl also reads DEVCTL_SESSION_ID, CLAUDE_CODE_SESSION_ID and GEMINI_SESSION_ID. rent doctor says which one it found. A caller with none of them is unknown, and every unknown caller shares one claim. A release can never remove another session's claim, so an unidentified session end leaves the server running until it expires. It never stops a server another session is using.

Separate git worktrees still give each session its own server, port and lease.

Where rentctl has to run: somewhere it can see processes

Supervision works by reading the process table: every process in a workload's session is found by its session id, and a stop is only reported as verified once that session is provably empty. So rentctl must run where it can list processes and read their session ids.

  • Works: the MCP server launched by your agent host (Claude Code, for example), and rent typed in an ordinary terminal.

  • Does not work: rent invoked from inside an agent's command sandbox that blocks process inspection. Codex's macOS sandbox is one (Operation not permitted (originated from sysctl())).

Where inspection is denied, rent up refuses with UNSUPPORTED_ENVIRONMENT and starts nothing. If inspection is lost after a server started, rentctl treats ownership as unknown: it keeps the lease, signals nothing, and never reports cleanup as verified. The record shows cleanup_incomplete with identity_ambiguous until a later attempt can see the session again. Denied inspection never reads as "no processes". The fix is to run rentctl from the host side, not to grant the sandbox wider privileges.

It won't kill things it doesn't own

Every lease records its supervisor's PID and start time, and the supervisor's PID is the environment's session id (a lease from 1.0.x uses its server's instead). A process is the environment's only if it is in that session (or, on Linux, descends from the supervisor), and each one's identity is re-checked immediately before it is signalled. If the session's PID has been recycled onto some unrelated process, nothing under it can be proved ours: the lease is kept, marked identity_ambiguous, and nothing is signalled — a stale lease can't get your database killed.

A listener inside an enrolled project's port block with no lease behind it is a squatter. What happens to it depends on the machine's enforcement mode:

  • advisory (the default, and the only mode any command sets) — rent up routes around it, and rent ls / rent sweep report it. It is never signalled.

  • strict — rent sweep sends each squatter SIGTERM, and reports what it killed. Since the session-start hook runs rent sweep, under strict that happens at every session start. Strict is set by the enforcement field in rentctl's machine-local registry; nothing turns it on for you. See Development machines only before you do.

When it genuinely cannot tell whether a port is in use — no usable probe on the host — it says so, rather than reporting the port as free. "No squatters found" means something looked.

What it can see is limited to what you can see. The port probe (lsof, or psutil on Linux) runs as your user, and an unprivileged probe can miss sockets owned by other users. So "free" means free as far as this user can see. A real collision still surfaces: the server's own bind() fails, and rent up reports the failed start (with the log tail) instead of writing a lease.

Security: what you are trusting

Read this part. rentctl runs a command out of a config file in your repo.

That is arbitrary code execution, and no tool can make it not be. If you can run npm run dev, you can run anything. What rentctl guarantees is narrower and more useful: it adds no silent path to it.

  • The command from rentctl.toml is shown to you and approved once, explicitly, at rent init.

  • On approval it is copied into rentctl's own registry along with a hash of the execution-determining fields: runner, cmd, cwd and port_env.

  • If the repo's rentctl.toml later changes any of them, the next start stops (CMD_CHANGED) and shows you a diff of approved-versus-current. It does not run the new command. You re-approve with rent sync or you don't.

The hash deliberately covers only the fields that determine execution, not the whole file — hashing everything trains you to click through re-approvals for comment edits, which defeats the point.

Approval pins the command, not the code it runs. npm run dev is the same string whatever package.json says dev means. An edit to a package.json script, a Makefile, or the application code itself changes what actually executes without tripping CMD_CHANGED. The pin stops a changed rentctl.toml from quietly running something new; it is not a review of the repo. Treat pulling someone else's branch the way you already would before running its dev server by hand.

Consequences worth being explicit about:

  • git pull cannot change the approved command. It can change the file, and it can change the code that command runs; it cannot change which command rentctl runs.

  • A repo cannot walk rentctl out of its own directory. cwd is repo-relative and rejected if it is absolute, contains .., or symlinks outside the repo. Project names are filename-safe or refused.

  • Non-interactive enrollment is explicit. CI passes --trust-repo, which is recorded as trust-on-first-use rather than being the quiet default.

  • rentctl only manages what you enrolled. Test-framework servers (pytest fixtures, Playwright's webServer) own their own lifecycle and use ephemeral ports; rentctl never touches them.

To report a vulnerability, see SECURITY.md.

Development machines only

Install this on machines where dev servers are meant to be disposable. Not on a host running anything you would mind losing.

A default install is safe: enforcement is advisory, and rentctl only scans the port blocks of projects you enrolled. Nothing else on the machine is examined and nothing unenrolled is ever signalled.

But rentctl's whole job is killing processes that outlive a session, and strict enforcement exists to make that unavoidable. A tool built to reap servers you forgot about is the wrong tool to arm on a host serving traffic — there, the servers outliving their session are supposed to. Keep it on development machines.

Troubleshooting

Agent-client problems (missing tools, hooks, approval, cleanup that did not happen): docs/troubleshooting.md. Which clients and surfaces were tested, and how: docs/integrations.md.

  • rent doctor checks that the rent shim runs, the registry loads, and each enrolled project's hooks are wired, and says which supervision level this host gets and where it found this session's id (session-identity). If rent itself is missing, the same check runs as python3 -m rentctl.doctor.

  • Server logs are in ~/.local/state/devctl/logs/<project>-<timestamp>.log (under $XDG_STATE_HOME if set). A failed rent up includes the log's tail in its output.

  • What happened, and why: rent events lists every start and teardown with the layer that did it; the raw log is ~/.local/state/devctl/events.jsonl. A teardown's down event says whether the stop was verified and whether it needed SIGKILL ("escalated": true). stop_requested means a stop was asked for (for example by the session-end hook) and is not itself a teardown. Neither is claim_released: a session let go of a server that others still hold.

  • rent ls shows each lease's state and whether its supervisor is alive:

    • stopping or "pending": true — a stop is under way; look again in a few seconds.

    • unsupervised — its supervisor died. The server is still yours and still serving; it is stopped at expiry by the next ls/sweep/up/down, or now by rent down. rent up will not renew it. (A lease started by 1.0.x also reads unsupervised, with "legacy": true; rent up does renew that one.)

    • cleanup_incomplete with survivors — those PIDs would not die. Find out why (a different user? stuck in the kernel?) before reusing the port; rent down retries.

  • UNSUPERVISABLE from rent up — the command daemonized, so its server left the session rentctl supervises. Run it in the foreground. The stray server is left alone; stop it yourself.

  • The supervisor notes what it did in the server's log, on lines prefixed [rentctl supervisor <pid>].

  • The registry of enrolled projects and approved commands is ~/.config/devctl/registry.json. It is rentctl's own record — change it with rent init / rent sync, not by hand.

(The directories say devctl, the project's former name. That is deliberate — renaming them would orphan every live lease.)

Known gaps

Stated plainly, because a tool making safety claims should be honest about its edges:

  • The approval pin covers the command, not the code — see Security.

  • Sessions in one checkout share one server — see above. A session that rentctl cannot identify releases nothing when it ends, so its server lives until the lease expires.

  • Crash cleanup waits for expiry — a session that dies without its hook leaves its server up until the lease runs out.

  • Recovery waits for the next command — if a supervisor is killed, nothing acts until the next rent ls/sweep/up/down.

  • Daemonizing commands are not supervised on macOS, and on Linux work handed to another process manager is not — see What supervision covers.

  • Only Claude Code, Codex (with its hooks trusted) and Gemini CLI get session-end hooks. Other clients get expiry and sweep. Cloud agent sessions (claude.ai/code cloud, Codex Cloud) are not supported: they cannot reach your machine's processes.

  • runner = "compose" is designed but not implemented. Asking for it fails with a clear error rather than doing nothing.

  • The only environment beyond the port is port_env. Anything else a server needs has to ride inside cmd today.

  • Windows: see Install. Not supported, not claimed.

Contributing

See CONTRIBUTING.md.

A note on the comments in the source

The source cites its own design decisions — ADR-0008, WI-0040, spec §10. Those refer to this project's design log, which is kept privately; the reasoning around each citation is written out where it is cited, so nothing is missing if you can't follow the pointer.

License

Apache License 2.0 — see LICENSE and NOTICE.

Copyright 2026 Michael Drake.

Available Tools

4 tools
env_downA
Idempotent

Release this session's hold on a project's environment; it stops when no other session holds it.

Other agent sessions in the same checkout may be using the same server. By default this releases only YOUR session's claim: if another session still holds one, the server keeps running and the result says stopped: false with held_by naming them. If you held the last claim, it stops. force=true stops it for everyone and lists overrode_claims — use it only when the user asked to stop the server outright. Omit project to act on everything leased to this cwd.

Waits up to wait_s seconds (default 15, at most 60) for verified cleanup. Past that the result is pending: true — still ok: the environment's supervisor finishes the stop, and env_ls shows the outcome.

ParametersJSON Schema
NameRequiredDescriptionDefault
forceNo
wait_sNo
projectNo

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare idempotent/non-destructive/non-read-only; the description adds the multi-session claim model, the fact that a non-final release returns stopped:false with held_by, that force overrides other sessions and reports overrode_claims, and that a timeout yields pending:true with ok status while the supervisor finishes. That is substantial behavior the structured fields cannot convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose is front-loaded in sentence one; the remaining sentences each carry a distinct rule (default vs force, project omission, wait/pending). Dense but free of filler or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 3-parameter tool with no output schema, the description still names the return fields an agent must interpret (stopped, held_by, overrode_claims, pending) and covers every parameter's behavior and limits. Nothing needed to invoke or interpret it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% with no titles beyond property names, yet the description explains all three: force (override semantics plus a usage warning), wait_s (default 15, cap 60, and what pending:true means past it), and project (omit to target everything leased to the cwd). Nothing is left to guesswork.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The first sentence states a precise verb and resource: releasing this session's hold on a project's environment, with the key conditional (it stops when no other session holds it). That scope is distinct from env_up/env_sweep without needing to open either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit when-to-use guidance for each mode: default releases only your claim, force=true 'only when the user asked to stop the server outright', omit project to act on everything leased to this cwd. It also names env_ls as the way to see the eventual outcome, so the follow-up path is clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

env_lsA
Read-only

List every broker-owned environment on the machine (live, reconciled inventory).

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A3.5/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and openWorldHint=false, so the safety profile is covered. The parenthetical '(live, reconciled inventory)' adds genuine value by signaling results are current and internally consistent rather than cached, but return format and ordering remain undisclosed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence with no filler. The scope qualifier comes before the freshness qualifier, which is the right priority order.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter read-only list tool with no output schema, the description is essentially sufficient. Only the absence of any hint about result shape or volume keeps it short of a 5.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, which is the baseline-4 case; there is no parameter semantics to clarify and the schema is empty by design.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb ('List') and resource ('environment') with clear scope qualifiers: 'broker-owned' and 'on the machine'. It is easy to tell apart from the lifecycle siblings env_up/env_down/env_sweep, though the description never names them explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No when-to-use or when-not-to-use guidance is given, and no alternative tool is referenced. The agent must infer from the name that this is a discovery step preceding the lifecycle operations.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

env_sweepB
Destructive

Reconcile every lease: stop expired/dead, report (or under strict, reclaim) squatters.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

B3.1/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare destructiveHint=true, readOnlyHint=false and openWorldHint=false, so the description's job is to add detail, and it does say what is affected: expired/dead leases are stopped and squatters are reclaimed. The gap is the undefined 'strict' mode — the description implies two behavioral regimes without saying how either is selected, which matters for a destructive tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single tight sentence with the primary action front-loaded and the subordinate behaviors after the colon. It is dense with unexplained terms, but there is no padding or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter, no-output-schema, destructive tool the description covers the main outcome, but it leaves two things unresolved: what a 'squatter' is and how 'strict' mode is triggered when no input exists. An agent can call it, but cannot predict which of the two described behaviors will occur.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With zero parameters and 100% schema coverage there is nothing for the schema to document, so the baseline is high. The mention of a 'strict' mode is the only wrinkle, since it hints at a switch that no parameter can set, but no parameter is misdocumented.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

It names a specific action (reconcile leases) and enumerates sub-actions (stop expired/dead, report or reclaim squatters), which is more than a restatement of the name. However, the core nouns are jargon ('leases', 'squatters') and it never distinguishes itself from env_up/env_down/env_ls, so an agent must infer that this is the cleanup/reaper tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no statement of when to reach for env_sweep versus env_down or env_ls, and no prerequisites or safety conditions. The only routing hint is the unexplained 'under strict' clause, which is not actionable guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

env_upA
Idempotent

Start (or renew) this checkout's leased dev environment and return its URL.

Omit project to use the one this checkout declares in its rentctl.toml. Use the returned url/port — never a conventional port — and check readiness. Calling it again renews your session's lease. Errors come back as ok: false with an error code: NOT_A_PROJECT / UNKNOWN_PROJECT (not enrolled — a person runs rent init), CMD_CHANGED (a person must re-approve with rent sync), START_TIMEOUT (read log_tail).

ParametersJSON Schema
NameRequiredDescriptionDefault
profileNodefault
projectNo
lease_minutesNo

TDQS

A4.1/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare the safety profile (readOnlyHint=false, idempotentHint=true), but the description goes well beyond them: lease-renewal semantics on repeat calls, the instruction to use the returned url/port rather than a conventional port, the readiness field, and a full error-code taxonomy (NOT_A_PROJECT, UNKNOWN_PROJECT, CMD_CHANGED, START_TIMEOUT) with remediation hints (log_tail, rent init, rent sync).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads purpose, then usage, then the error contract. Every clause carries information, though the error-code block is dense and the sentence about url/port could be tightened.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description usefully documents the returned url/port/readiness and the ok/error shape, which is exactly what an agent needs. The gap is the unexplained profile and lease_minutes parameters.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must carry the load, and it only explains one of three parameters (`project`'s default-to-rentctl.toml behavior). `profile` and `lease_minutes` are left entirely to their self-explanatory names and defaults, so compensation is partial.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (Start/renew) and resource (this checkout's leased dev environment) plus the return value (its URL). It is clearly distinguishable from env_down/env_ls/env_sweep by contrast, but never names those siblings explicitly, so it stops short of a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives concrete when-to-use context: omit `project` to use the checkout's rentctl.toml declaration, and calling it again renews the session lease. It does not, however, state when NOT to use it or point to a specific alternative sibling.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 4 tool updatesv0.1.0
    • First observedenv_down
    • First observedenv_ls
    • First observedenv_sweep
    • First observedenv_up

TDQS

A3.7/5.0

Scored across 4 tools

Disambiguation4/5

env_up (start/renew) and env_down (release) are clearly opposed, and env_ls (list inventory) versus env_sweep (reconcile leases) are distinguishable, though an agent could plausibly reach for env_sweep when it means env_down since both stop environments. The descriptions do enough work to keep boundaries clear.

Naming Consistency4/5

All four names use snake_case with a consistent env_ prefix, so the family is immediately recognizable. The action tokens mix directional (up/down) with verb/CLI-style (ls/sweep) forms, a minor deviation from a strict verb_noun pattern but still readable.

Tool Count4/5

Four tools is a tight, plausible set for a lease manager covering start, release, inventory, and reconciliation, with no obvious filler. It sits at the low end of the recommended range, and a dedicated status/logs tool could arguably earn a fifth slot.

Completeness3/5

The core lifecycle (start, stop, list, reconcile) is covered, but there is no way to fetch logs for a specific environment even though errors reference log_tail, and no single-environment status/get beyond the full inventory listing. These are workable-but-notable gaps for debugging failed starts.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers