port-keeper-mcp
This server is an MCP interface to port-keeper, a local ledger that gives each parallel coding agent/working copy its own block of development ports and resolves service URLs/ports by name.
current_context – identify the project, slot, and service names for the current working directory (no port numbers), plus whether the working copy is unbound and needs a slot.
resolve_url – get the full URL (e.g.
http://localhost:23417) for a service in the current or another project/slot.resolve_port – get the bare port number of a service (e.g. for TCP services or config values).
render_env – render all environment variables for the current slot (ports, derived values, project/slot vars) as dotenv, export, json, mise, direnv, or claude-env; automatically leases ports for services that lack them.
slot_new – create a new slot for the current project and lease its port block, optionally sharing infra-tier services from another slot; idempotent if the slot/working copy already exists.
status – compare the ledger against what is actually listening for each service (leased, active, stale, hijacked), including the owning process when known.
Ports are allocated from a shared pool with uniqueness guaranteed, so parallel agents never collide; slots are stable and released only explicitly.
port-keeper-mcp
A local ledger for development ports, with an MCP server on top, written in Go. It gives every parallel coding agent its own set of ports, so each one can run the whole stack at the same time as the others.
The problem
Coding agents pay off when you run them in parallel: five agents, five
branches, five working copies. git worktree gives each agent its own files.
It does not give each agent its own ports.
For a web service, that is where it stops. Every working copy carries the same
.env, so every copy's dev server, API and database ask for the same numbers.
The first agent to start gets them. The others hit address already in use,
or, worse, quietly run their tests against another agent's server and
database.
An agent that cannot bring up its own stack cannot check its own work. It can write code, run static analysis, and unit-test the pure functions. It cannot run an end-to-end test or any test that goes through a database: those need the application running, which takes one environment per agent, each holding as many ports as the stack has services. Without that, every agent queues for the single environment that works. A small CLI tool never notices. A large web service does: the work is parallel until it has to be verified, then it is serial, and the reason for running agents in parallel is gone.
Agents run at the same time and do not talk to each other, so a naming convention or a wiki table of port ranges will not hold. Handing out ports has to work like a protocol: one place that every agent asks, and that answers correctly when several ask at once.
Related MCP server: Harbor MCP Server
What port-keeper does
port-keeper is that place.
One environment per agent. Each working copy gets a slot, and each slot gets its own block of ports. No two leases ever share a port, across every project on the machine, even when several agents ask at the same moment.
Agents ask instead of guessing. Over MCP an agent gets the URL of a service by name, and a hook puts the slot's ports into the agent's shell at session start. A fresh worktree is refused the main copy's ports until it has a slot of its own.
Nothing to keep running. No daemon, no proxy, no network listener. Every command opens the ledger, does its work and exits.
A project declares its services in a small manifest (names and env-var names,
never numbers). port-keeper hands each slot (a parallel copy of the project:
one per working copy, whether that is a clone or a git worktree) a block of
ports from a pool, writes them into your .env, and answers "what is the URL
of shop/5/admin?" from the CLI or over MCP. Neither you nor your coding agent
has to remember a port number again. It runs only when called, writes nothing
outside the ledger except the files you point it at inside your project, and
never opens a network listener.
See docs/DESIGN.md for the full design (Japanese).
Small on purpose
Before writing port-keeper we surveyed what already existed. The tools fall
into a few kinds: local proxies that hide ports behind hostnames and have to
stay running; agent-coordination platforms where ports are one feature among
sessions, locks and messaging; wrappers that want to launch your dev server
for you; free-port finders that keep no record of who owns what; and port
registries for agents that know nothing about parallel working copies or
.env files. Several are good at what they do. None of them was a ledger and
nothing but a ledger.
port-keeper is the simplest thing that solves the problem above:
One job. It decides which port belongs to which service of which working copy, and answers when asked. Starting servers stays with your task runner. Pretty hostnames stay with a proxy, if you want one. port-keeper can feed both and replaces neither.
Few parts. One static binary, one SQLite file, one small manifest per project. No daemon, no proxy, no DNS, no certificates, no account.
A small surface for agents. Eight MCP tools, six of them on by default. An agent takes in the whole interface at a glance, and it costs almost no context.
Fits what you already run. It writes plain environment variables into
.env, your shell, direnv or mise. Your dev command does not change. docs/examples/ shows the wiring for mise, direnv, docker compose, Vite, Playwright and reverse proxies.Easy to leave. Remove the MCP entry and the hook, then delete the binary and the ledger file. The
.env.localit wrote is an ordinary file and keeps working.
What it leaves out is listed, with the reasons, under Non-goals.
Install
port-keeper is one static binary with no runtime dependencies. Put it on your
PATH: your shell, the agent hooks and your MCP client all call the same
port-keeper command, so one install serves all three and there is only ever
one version on the machine.
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/gridhra/port-keeper-mcp/main/scripts/install.sh | sh
# Windows (PowerShell). Built and cross-compiled in CI; not yet verified on a real Windows machine
irm https://raw.githubusercontent.com/gridhra/port-keeper-mcp/main/scripts/install.ps1 | iexThe script picks the archive for your OS and CPU from the latest
GitHub Release, refuses
to install anything unless its SHA-256 matches the release's checksums.txt,
and places port-keeper in ~/.local/bin. It never asks for sudo. Set
PORT_KEEPER_INSTALL_DIR to install elsewhere and PORT_KEEPER_VERSION to pin
a version. Run it again to update; it replaces the one binary and leaves your
ledger and config alone.
To do the same by hand, and to check that the archive was built by this repository's release workflow from the tagged source:
gh release download --repo gridhra/port-keeper-mcp --pattern '*darwin_arm64.tar.gz' --pattern checksums.txt
shasum -a 256 -c --ignore-missing checksums.txt
gh attestation verify port-keeper_*_darwin_arm64.tar.gz --repo gridhra/port-keeper-mcp
tar -xzf port-keeper_*_darwin_arm64.tar.gz port-keeper && mv port-keeper ~/.local/bin/With a Go toolchain (1.25 or newer):
go install github.com/gridhra/port-keeper-mcp/cmd/port-keeper@latestThere is deliberately no container image and no npx launcher; see
Non-goals.
Quickstart
cd your-project
port-keeper init # writes port-keeper.toml (names only) and gitignores .env.local
$EDITOR port-keeper.toml # one [[service]] per port your project needs
port-keeper env # leases a block, writes the managed section of .env.local
port-keeper url web # http://localhost:20000 (add --open to launch the browser)A second working copy of the same project gets its own slot:
git worktree add ../your-project-hotfix -b hotfix && cd ../your-project-hotfix
port-keeper slot new # leases a fresh block for this working copy
port-keeper env # writes its .env.local; nothing collides with the first copyGive your coding agent the same view:
claude mcp add --scope user port-keeper -- port-keeper mcpThen add the hook and the two-line instruction from Working with coding agents.
Why the usual workarounds fail
Offset formulas break. "Base port + (slot − 1) × 1000" works until two services sit exactly 4000 apart; then slot 5 collides with slot 1 and the project is stuck at four environments with thousands of ports unused.
Cross-project collisions are nobody's job. Every project on a laptop carves its own thousand-range, and the OS grabs a few conventional ports on top (on macOS, the AirPlay receiver in Control Center owns 5000 and 7000). Whoever starts second loses.
Numbers leak. People paste
localhost:3001into chat, docs and issues because they had to memorise it. A name (shop/3/admin) leaks nothing and resolves on every machine.Agents guess. A coding agent that needs a server picks 8000 or 3000 and tramples whatever was there. Give it a tool to ask instead.
Use cases
Fifth working copy, no arithmetic
"Spin up another copy of this project for the hotfix branch."
port-keeper slot new hotfixleases a fresh block, andport-keeper envwrites the.env.localsection; your usualmise run devstarts everything. No collisions with the other four slots or with any other project on the box.Open the right admin without remembering anything
"Open the admin screen of slot 3."
port-keeper url shop/3/admin --open, or theresolve_urlMCP tool from your agent. One round trip, one URL.Share a database between two app environments
"Slot 2 should use slot 1's MySQL and mail catcher."
port-keeper slot new 2 --infra-from 1. Services taggedtier = "infra"resolve to slot 1's ports; everything else gets its own.Find out who is squatting on your port
"The API says address already in use."
port-keeper statuscompares the ledger with what is actually listening and names the PID. When the listener's working directory is outside this working copy (and it is not a container runtime), the lease is markedhijacked; otherwise it is simplyactive. It never kills anything;port-keeper reassign <service>moves that service to a free port instead.Migrate an existing project without breaking bookmarks Keep the main environment on its historic numbers with
port-keeper pin web=3000 admin=3001 api=8080 --reason "docs and bookmarks". Pins are arbitrated across every project in the ledger, so two projects cannot both claim 3000. Every other slot moves to the pool (update those bookmarks withport-keeper url), anddoctorreminds you tounpinonce the docs sayport-keeper urlinstead.
Working with coding agents
port-keeper knows nothing about any particular agent. The contract every client uses is two commands:
port-keeper context --json: where am I (project, slot, whether this working copy is ready to use), whether the rendered.env.localis out of date (env_stale), and what to do next. No port numbers.port-keeper env --format export --if-present: the environment for the current slot asexportlines; silent outside a project.
Everything client-specific is an adapter on top of those two. port-keeper hook claude is the adapter for Claude Code's hook protocol; it lives in one
file and other agents can get their own without touching the core.
Claude Code
Register the MCP server once, user-wide (there are no numbers in the config, only the command):
claude mcp add --scope user port-keeper -- port-keeper mcpAdd the hook in ~/.claude/settings.json. On SessionStart it exports this
slot's ports into the agent's shell and adds the project and slot to Claude's
context; on CwdChanged (Claude cds into another working copy) it swaps the
exported ports for that working copy's slot. Outside a project the command
prints nothing and exits 0, so the hook never fails a session.
{
"hooks": {
"SessionStart": [
{ "hooks": [ { "type": "command", "command": "port-keeper hook claude" } ] }
],
"CwdChanged": [
{ "hooks": [ { "type": "command", "command": "port-keeper hook claude" } ] }
]
}
}Tell the agent the tool exists
The tool only helps if the agent knows it exists. Agents left to themselves
start python -m http.server 8000 or vite --port 3000 and trample whatever
was there. Two lines in your user-level instructions file (for Claude Code,
~/.claude/CLAUDE.md; other agents have an equivalent) are enough to stop
that, because they apply to every project on the machine:
## Local ports
Local dev ports on this machine are managed by port-keeper (MCP server `port-keeper`).
Never choose a port number yourself and never start a server on an ad-hoc port.
To find where something runs, call the `resolve_url` tool
(or run `port-keeper url <project>/<slot>/<service>`).
If a task needs a new port, add a service to `port-keeper.toml` and run `port-keeper env`.
To give a command this slot's ports, prefix it with `eval "$(port-keeper env --format export)"`; never paste numbers into a command or a file.
Refer to services by name (`shop/3/admin`), never by number, in docs, issues and chat.Why user-level and not per-project: the servers that cause trouble are the ad-hoc ones an agent starts in a scratch directory or in a project that has no manifest yet. A project-level file never reaches those.
If you run several agents (Claude Code, Codex, Cursor, …), put the same block in each one's global instructions. The MCP registration is per client; the ledger is shared.
Guarantees
No two leases share a port, across every project and slot on the machine, including pinned legacy ports. Enforced by a UNIQUE constraint in the ledger and a write transaction around every allocation.
Pool only. Allocation never leaves the configured pool (default 20000–31999), which avoids the conventional ports, the ports macOS services take, and the OS ephemeral ranges. The only way out of the pool is an explicit, reasoned
pin.Stable. A slot keeps its block until you release it, across reboots.
Nothing is reclaimed behind your back. A slot keeps its ports until you release it, however long its servers stay down.
port-keeper gclists the slots that look abandoned (nothing listening forstale_days, or the working copy is gone) and releases them only with--yes. A dev server that has been off for a month must not find its ports handed to another slot when it comes back.One slot per working copy. A working copy that has no slot of its own is refused the default slot's ports:
env,url,statusand the MCP tools ask you to runslot newfirst (or to pass--slot 1if sharing the main slot is what you want). That is what keeps a freshgit worktree addfrom starting servers on the main working copy's ports.No listener, no daemon. Every command opens the ledger, does its work, and exits. Nothing of port-keeper's is ever reachable over the network.
No secrets. The ledger has no column for passwords, tokens or connection strings, and
.envrendering never writes into a tracked file.
Non-goals
These are deliberate. Please read this section before opening a feature request for one of them; the reasoning is the answer, and a request that argues with the reasoning is far more useful than one that restates the feature.
No reverse proxy / named hosts (http://admin.shop.localhost)
port-keeper's goal is that you do not have to think about port numbers, not
that you never see one. Once port-keeper url shop/5/admin (or the
resolve_url tool) gives you http://localhost:23417, the job is done;
--open even opens it for you.
A proxy would add a resident process that every slot depends on. When it
stops, every environment becomes unreachable at once, which is a worse failure
mode than any port collision. It needs a privileged port (80/443) on macOS,
drags TLS and WebSocket forwarding into scope, and contradicts the rule that
port-keeper never listens. Finally, browsers scope cookies and local storage
by origin including the port, so distinct ports are exactly what keep your
slots' sessions apart; hiding them behind one hostname would remove that
isolation. If you want pretty hostnames anyway, feed
port-keeper env --format json to a proxy that already does this well
(portless, localias, devenv; docs/examples/reverse-proxy.md
shows how). port-keeper will not grow one.
No process management (start / stop / restart / kill)
port-keeper reserves a number and tells you what it is. Who starts the server
on that number, when, and under which supervisor is the job of your task
runner (mise, just, direnv, docker compose, an IDE). port-keeper will
report that a port is in use and by which PID, but it will never kill a
process: a tool that an AI agent can call should not be able to take down
another environment's dev server by mistake or through prompt injection.
No daemon
Every command opens the SQLite ledger, works inside a write transaction, and
exits. Concurrent CLI or MCP processes cannot double-allocate because SQLite
serialises the writes and the port column is UNIQUE. A daemon would buy
pub/sub and TTL leases at the cost of a second access path to the ledger
(socket or HTTP) and one more thing to keep alive. The requirements do not
need it.
No shared or synced ledger
The ledger lists which services listen on which ports on your machine. That is exactly what an attacker enumerates first, so it stays local, mode 0600, and is never synced, committed or uploaded. Teams share the manifest, which has names and templates but no numbers.
No secrets in the ledger
There is no column for passwords, tokens or connection strings, and there will not be one. If a feature seems to need a secret, it belongs in your secret manager.
No allocation outside the pool
pin exists for migrating an existing project and nothing else. It requires a
--reason, is limited to one slot per project, and is arbitrated across every
project in the ledger; doctor nags you about it until you unpin. New
projects should never pin.
No container image
port-keeper has to see four things that belong to your machine: the host's
network stack (it checks a port by trying to bind it), the host's process
table (lsof tells it who is listening), the working directory your shell or
agent is in (that is how it finds the project and the slot), and the ledger
under your home directory. A container exists to isolate exactly those four.
Inside one, port-keeper would probe an empty network namespace and report
every port free, would not find your project, and would forget its leases when
the container exits. On macOS and Windows the container runtime itself runs in
a Linux VM, so not even host networking reaches the ports your dev servers
hold.
Mounts and flags can paper over part of this on Linux, and each one hands the
container another piece of the host until nothing is left of the isolation. So
there is no image, and port-keeper will not be listed anywhere as an OCI
package. The binary is a single static file; Install puts it on
your PATH with one command.
An npx launcher is out for a related reason. A launcher that fetches the
server on demand gives your MCP client a server, but gives your hooks and your
shell no port-keeper command, and it leaves two copies of different versions
sharing one ledger.
If you still want one of these
Open an issue that starts from the reasoning above and says which part of it does not hold in your case. "It would be convenient" is already accounted for; what changes the answer is a failure mode we did not consider, or a case where the non-goal blocks the actual goal (not thinking about port numbers).
Security model
port-keeper is a local, non-network tool. The ledger is a map of what listens
where on your machine; it is stored under ~/.local/state/port-keeper/ with
mode 0600 and is never transmitted. Against another user on the same machine
that is sufficient. Against a process running as you it is not, and no
local tool can be: such a process can already run lsof -i. What port-keeper
does about that is refuse to be a richer map than lsof (no secrets, no
tenant names, no descriptions beyond a short label) and refuse to disclose
more than the current project to an agent unless asked explicitly.
port-keeper doctor checks the permissions, the .gitignore, that nothing
of port-keeper's is listening, and that your MCP client configuration carries
no port number or token; it names the file and the entry, never a value. See
SECURITY.md for the reporting policy and what is in scope.
Reference
Manifest (port-keeper.toml)
Lives at the repository root and is meant to be committed. It contains names and templates; the numbers live only in your local ledger.
[project]
name = "shop"
block_size = 32 # ports per slot; grows to a second block if exceeded
slot_default = "1" # the slot a fresh checkout resolves to; the only slot that may pin
[[service]]
name = "web"
env = "WEB_PORT"
proto = "http"
label = "Storefront" # optional, short; cut to 64 chars before it reaches an agent
[[service]]
name = "admin"
env = "ADMIN_PORT"
proto = "http"
[[service]]
name = "api"
env = "API_PORT"
proto = "http"
[[service]]
name = "db"
env = "DB_PORT"
proto = "tcp" # no URL for tcp services
tier = "infra" # shareable via `slot new --infra-from`
[[derive]] # values built from ports, never typed by hand
env = "VITE_API_BASE"
value = "${url.api}"
[[derive]]
env = "ALLOWED_ORIGINS"
value = "${url.web},${url.admin}"
[render]
dotenv_path = ".env.local" # must be gitignored; `port-keeper env` refuses tracked files
dotenv_marker = "port-keeper" # marker text of the managed block
host = "localhost" # host used in every URLTemplate variables: ${port.<service>}, ${url.<service>} (http/https
services only), ${slot}, ${slot.infra}, ${project}, ${block.base}.
CLI
Command | Role |
| Write the manifest skeleton and the |
| Create, list, release slots. |
| Render the current slot; |
| Print (or open) one URL |
| Ledger vs. what is listening; |
| Project, slot, readiness and guidance for this working copy, and |
| List (or release) slots idle for longer than |
| Permissions, |
| Migration aid; see Guarantees. The batch form pins a whole legacy layout in one command |
| Move a service to another pooled port (after |
| Serve MCP over stdio |
| Claude Code adapter for |
| Print the completion script for |
| Print the version |
| Global flag: act on that slot instead of the resolved one |
A slot is resolved from --slot, then the slot bound to the current working
copy, then PORT_KEEPER_SLOT, then the manifest's slot_default. When a
stale PORT_KEEPER_SLOT in the shell disagrees with the binding, the binding
wins and a warning says so.
MCP tools (8)
Tool | Role |
| The project and slot resolved from the working directory, and the service names. No numbers (read-only) |
| Full URL for one service, e.g. |
| The bare port number for one service (read-only) |
| Every env var for the current slot, in the requested format. Leases a port for any service that has none yet (idempotent) |
| Ledger vs. reality for the current slot: |
| Lease a block for a new slot; returns the existing slot if the name is taken or the working copy is already bound (idempotent) |
| Release a slot. Refuses while anything is listening. Requires |
| Names of every project and slot in the ledger, no numbers. Not registered unless enabled in config |
Every tool accepts an optional cwd so a session that moves between working
copies keeps getting the right slot. Only resolve_url, resolve_port and
render_env carry port numbers, and only for what was asked; the other tools
speak in names, so that numbers do not accumulate in agent transcripts.
Configuration (~/.config/port-keeper/config.toml)
All optional:
stale_days = 30 # top-level key; must come before any [table]
[pool]
ranges = [[20000, 31999]]
deny_ports = [27017, 28015, 29092]
[mcp]
enable_release = false # set to true to register slot_release
enable_list_all = false # set to true to register list_all_projectsUnknown keys are an error, so a misplaced setting cannot silently do nothing.
Development
Go 1.25 or newer (the SQLite driver and the MCP SDK require it; go downloads
the toolchain automatically when GOTOOLCHAIN is left at its default).
go test ./... # unit, property and in-process MCP tests
go vet ./... && gofmt -l .
sh scripts/readme_sync_check.sh # the three READMEs mirror each other (CI runs it too)
sh scripts/agent_eval.sh --list # the manual agent eval; see RELEASING.mdReleases are cut by pushing a v* tag; RELEASING.md has the
procedure (Japanese). sh scripts/install_test.sh tests the install script.
sh scripts/agent_eval.sh gives an MCP-connected Claude Code three everyday
tasks and counts round trips and leaked numbers; it runs a real model, so it
is a manual gate before each release rather than a CI job.
The project uses OpenSpec (openspec/) for change proposals; run
openspec list to see them.
Package layout: internal/config (pool, paths) / internal/manifest /
internal/ledger (SQLite, allocation) / internal/probe (bind checks,
listener discovery) / internal/gitx / internal/render (dotenv, export,
json, mise, direnv, claude-env) / internal/app (resolution, sync, status,
pins) / internal/cli (commands; hook_claude.go is the Claude Code
adapter) / internal/mcpserver (stdio server) / cmd/port-keeper.
Name
A keeper holds the keys and the ledger and tells you which door is which;
it does not build the doors or open them for you. The -mcp suffix follows
the naming of MCP servers; the binary is just port-keeper.
License
MIT. See LICENSE.
Available Tools
6 toolscurrent_contextARead-only
The project and slot resolved from the working directory, with the service names. Returns no port numbers.
| Name | Required | Description | Default |
|---|---|---|---|
| cwd | No | directory to resolve the project from; defaults to where the server was started |
Output Schema
| Name | Required | Description |
|---|---|---|
| slot | Yes | |
| project | Yes | |
| unbound | No | true when this working copy has no slot of its own and the default slot belongs to another working copy; call slot_new before using ports |
| services | Yes | |
| warnings | No | notes about how the slot was resolved, e.g. a stale PORT_KEEPER_SLOT in the environment |
| infra_from | No | |
| slot_exists | Yes | |
| slot_source | Yes | how the slot was chosen: explicit, env, root or default |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The annotations already declare readOnlyHint=true, so the read-only nature is covered. The description adds useful behavioral context beyond annotations, specifically that resolution is based on the working directory and that the output intentionally excludes port numbers, which is a meaningful caveat for agents selecting between context and port-resolution tools.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single concise sentence with no filler. The key clarifying statement about not returning port numbers is placed at the end, which is acceptable but slightly less front-loaded than ideal.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the simple read-only nature, an output schema, and full parameter schema coverage, the description is mostly complete. It clearly identifies what is returned and explicitly states the important negative detail about port numbers, though it could add a brief note about when to use this tool among its siblings.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema has 100% description coverage for the single optional cwd parameter, so the schema already explains the parameter. The description adds only implicit context about the 'working directory' and does not need to compensate for any schema gaps.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states that the tool returns the project and slot resolved from the working directory, along with service names, and explicitly notes that port numbers are not included. This differentiates it from sibling tools like resolve_port and resolve_url. It would be stronger with an explicit verb such as 'Gets' or 'Resolves'.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies the tool is used to retrieve the current project/slot context from the working directory, but it does not explain when to choose this over alternatives like render_env, resolve_port, or status. No exclusions or comparative guidance are provided.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
render_envAIdempotent
Every environment variable of the current slot (service ports, derived values, PORT_KEEPER_PROJECT/SLOT) in a format: dotenv, export, json, mise, direnv or claude-env. Leases ports for services that have none yet. For your own use: to write .env.local run the port-keeper env command instead of writing the file by hand, and do not echo these numbers to the user.
| Name | Required | Description | Default |
|---|---|---|---|
| cwd | No | directory to resolve the project from; defaults to where the server was started | |
| format | No | dotenv, export, json, mise, direnv or claude-env (default export) |
Output Schema
| Name | Required | Description |
|---|---|---|
| env | Yes | |
| slot | Yes | |
| text | Yes | the rendered snippet |
| format | Yes | |
| project | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description discloses an important side effect not obvious from the annotations: it leases ports for services that have none yet. It also warns agents not to echo the values to the user. This goes well beyond the annotation hints and provides meaningful behavioral context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact: it front-loads the tool's output, then notes the lease side effect, then gives a direct instruction about the correct way for the agent to handle .env.local. Every sentence earns its place without unnecessary detail.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the output schema and 100% schema coverage, the description supplies the missing behavioral context: side effects, supported formats, and the instruction not to expose the values to the user. Nothing essential for correct invocation appears to be missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema already documents cwd and format. The description restates the allowed formats but adds little parameter meaning beyond that baseline. This is acceptable but not additive.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states what the tool produces — every environment variable of the current slot, including service ports and derived values — and lists the accepted output formats. This distinguishes it from sibling tools like resolve_port or current_context, which deal with narrower or different concerns.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives an explicit agent-facing directive: write .env.local by running the `port-keeper env` command rather than by hand, and do not echo these numbers to the user. It does not explicitly contrast this tool with its sibling tools, but the context for the main intended use is clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
resolve_portARead-only
Bare port number of one service. Prefer resolve_url unless the caller needs the number itself (a tcp service, a config value).
| Name | Required | Description | Default |
|---|---|---|---|
| cwd | No | directory to resolve the current project from; defaults to where the server was started | |
| slot | No | slot name; defaults to the current slot | |
| project | No | another project's name; requires slot | |
| service | Yes | service name from the manifest |
Output Schema
| Name | Required | Description |
|---|---|---|
| host | Yes | |
| port | Yes | |
| slot | Yes | |
| project | Yes | |
| service | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, so the read-only nature is covered by structured data. The description adds 'bare port number' and scopes to 'one service,' but it doesn't describe any further behavioral traits such as error conditions or dependencies on project/slot. With annotations carrying the safety profile, this is adequate but not particularly rich.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two short sentences with no waste. The core purpose is front-loaded first, and the routing guidance follows immediately. Every word earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The tool has an output schema, so return values are already documented. All parameters are documented in the input schema. The description clearly explains the tool's purpose and when to use an alternative, which is sufficient for correct selection and invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and every parameter is described in the input schema. The description does not add parameter-level meaning beyond the schema—it implies service is the primary input but offers no additional format or interaction details. The baseline of 3 applies because the schema handles the semantic load.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states clearly what the tool does: 'Bare port number of one service.' It is specific about the verb and resource, and it differentiates from resolve_url by noting the return type (number vs URL). This distinguishes it from the sibling tool at a glance.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit routing guidance: 'Prefer resolve_url unless the caller needs the number itself (a tcp service, a config value).' It names the alternative tool and specifies the exact conditions that would make this tool the right choice, leaving no ambiguity.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
resolve_urlARead-only
Full URL (e.g. http://localhost:23417) of one service in the current slot. Pass project and slot to look up another project; both are required together.
| Name | Required | Description | Default |
|---|---|---|---|
| cwd | No | directory to resolve the current project from; defaults to where the server was started | |
| slot | No | slot name; defaults to the current slot | |
| project | No | another project's name; requires slot | |
| service | Yes | service name from the manifest |
Output Schema
| Name | Required | Description |
|---|---|---|
| url | No | empty for tcp services |
| slot | Yes | |
| proto | Yes | |
| project | Yes | |
| service | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already mark this as read-only, and the description adds behavioral context by explaining the default slot behavior and the conditional requirement that project and slot must be passed together. It does not contradict the annotations and provides useful operational detail beyond them.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two concise sentences, front-loading the core output and scope before explaining the alternate lookup mode. Every sentence contributes useful information with no redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the read-only annotation, 100% schema coverage, and presence of an output schema, the description covers the essential behavior well. It could be slightly more complete by explicitly linking to sibling tools or clarifying when the port-focused counterpart should be used, but nothing critical is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema already covers all parameters at 100%, so the baseline is 3. The description adds value by clarifying that project and slot are mutually dependent for cross-project lookups, which is not fully explicit in the schema structure.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states that the tool returns the full URL of one service, with the current-slot scope and an alternate-project lookup mode. It lacks an explicit verb like 'resolve' or 'get', and does not directly name sibling tools, so it stops short of full sibling differentiation.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives useful context: by default it uses the current slot, and passing both project and slot is required to look up another project. However, it does not mention alternatives such as resolve_port or state when resolve_url should be avoided.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
slot_newAIdempotent
Create a slot for the current project and lease its ports. Returns the existing slot when the name is taken. Omit name for the next free number.
| Name | Required | Description | Default |
|---|---|---|---|
| cwd | No | directory to resolve the project from; defaults to where the server was started | |
| name | No | slot name; omit for the next free number (or the slot already bound to this working copy) | |
| infra_from | No | share tier=infra services from this slot |
Output Schema
| Name | Required | Description |
|---|---|---|
| slot | Yes | |
| created | Yes | |
| project | Yes | |
| services | Yes | |
| infra_from | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish the safety profile, so the description adds value by disclosing concrete behavior: it leases ports, returns the existing slot instead of failing on a name collision, and falls back to the next free number. This is consistent with the idempotentHint annotation and reveals side-effectful behavior beyond a plain create.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two compact sentences carry the core action, the collision behavior, and the name-omission rule without any filler. The most important information is front-loaded and every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The definition is complete for a 3-optional-parameter tool: the description covers creation, leasing, idempotent fallback, and naming behavior, while the output schema covers return values and the annotations cover safety traits. Nothing essential for selecting or invoking the tool is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3 and the schema already documents cwd, name, and infra_from. The description mostly reiterates the name-omission rule already present in the schema and does not add new format or dependency details for the other parameters.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific action ('create a slot') and resource ('for the current project'), then adds the key behavior of leasing ports and returning an existing slot when the name is taken. This makes it clearly distinct from siblings like resolve_port, resolve_url, and current_context.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies when the tool is appropriate: when the current project needs a slot with leased ports. It also gives practical parameter guidance ('Omit name for the next free number'), but it never explicitly states when to choose this tool over alternatives or mentions any exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
statusBRead-only
Ledger versus reality for the current slot: each service's state (leased, active, stale, hijacked) and the listening process when known.
| Name | Required | Description | Default |
|---|---|---|---|
| cwd | No | directory to resolve the project from; defaults to where the server was started |
Output Schema
| Name | Required | Description |
|---|---|---|
| slot | Yes | |
| project | Yes | |
| services | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already provide readOnlyHint=true, so the description does not need to reassert safety. It adds context by framing the output as 'ledger versus reality' and noting that the listening process is included 'when known', which is useful behavioral nuance. However, it does not explain side effects, error conditions, or how current-slot scoping resolves.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single sentence with no filler. It front-loads the core concept ('Ledger versus reality') and then lists the concrete output contents. Every phrase contributes meaning.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the low parameter complexity, the readOnly annotation, and the presence of an output schema, the description covers the essential semantics well. It could be slightly more explicit about what 'current slot' means and how this tool differs from current_context, but nothing critical is missing for invoking it.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, with the sole optional parameter cwd already documented in the input schema. The description adds no additional parameter semantics, so the baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description identifies the resource ('current slot'), the subject ('each service's state'), and enumerates the state categories plus the listening process. It lacks an explicit verb like 'reports' or 'shows', and it does not name sibling tools, so it is clear but not maximally sharp.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No guidance is provided about when to call this tool versus alternatives such as current_context, resolve_port, or resolve_url. The description implies a diagnostic use case but does not state conditions, exclusions, or alternatives.
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.
6 tool updates
v0.1.0- First observed
current_context - First observed
render_env - First observed
resolve_port - First observed
resolve_url - First observed
slot_new - First observed
status
TDQS
Scored across 6 tools
Each tool has a clear, single responsibility: context, env rendering, port resolution, URL resolution, slot creation, and status. No two tools overlap; even resolve_port and resolve_url are distinguished by use case.
Most tools follow verb_noun (render_env, resolve_port, resolve_url, slot_new) but current_context and status are noun phrases. The snake_case style is consistent, but the grammatical pattern is mixed.
With 6 tools, the set is well-scoped for a port/slot management server. Each tool serves a distinct need without bloat.
The core lifecycle (create slot, resolve ports, render env, check status) is covered, but there is no explicit 'delete slot' or 'release ports' tool, which might be a minor gap for full lifecycle management.
Maintenance
Related MCP Connectors
Manage Sprites: sandboxed compute environments with exec, services, and checkpoints.
- WalleKOAuthapp.wallek
Personal finance ledger: log expenses, track bills and cards, import statements.
Your accounting ledger as typed tools: net worth, holdings, history, tax estimates, trade logging.
Durable, shareable and governed project memory with smart triage and explicit project composition.
Related MCP Servers
- FlicenseAqualityDmaintenanceA Model Context Protocol (MCP) server for managing port registrations on your computer. Keep track of which applications are using which ports, find free ports, and maintain a central registry of port allocations.5-
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to discover, configure, and manage local development servers. Provides tools for app registration, port allocation, lifecycle control, and log access without manual config editing.232 npmMIT
- AlicenseNot gradedqualityAmaintenanceLocal coordination for coding agents that share a Git working tree.19 npm2MIT
- AlicenseNot gradedqualityBmaintenanceSee and control the local dev servers your coding agents leave running. Lists listeners with provenance — which agent, terminal and git worktree started each — kills strays, and allocates collision-free ports so parallel agents stop fighting over :3000.MIT