Skip to main content

Port Authority

Tests PyPI version License: MIT Python 3.9+

A small local daemon that hands out ports on request instead of every project guessing or scanning for a free one. One project asks "give me a port for web," another asks for api, and Port Authority makes sure they never collide — including with things it didn't allocate (Docker containers, stray processes, anything already bound).

Features

  • Centralized allocation: one registry instead of every project picking its own port
  • Real availability checks: before handing out a port, the daemon actually tries to bind it — not just "not in my registry"
  • Token auth: the daemon generates a local secret on first start; the CLI/library read it automatically — no plaintext-anyone-on-the-box access
  • Stale-allocation cleanup: a background sweep reclaims ports whose owner crashed and never released them, after a configurable grace period — plus port gc to preview or force it on demand
  • Pool management: configurable ranges per category (web, api, database, ...)
  • Known services: port myproject postgres tries port 5432 first (falling back to the normal pool scan if it's taken) — covers common databases, caches, and brokers out of the box, extendable via config
  • REST API: plain HTTP on 127.0.0.1:8888
  • CLI + Python library: use from shell scripts or import directly
  • Persistent state: allocations survive daemon restarts (~/.local/share/port-authority/allocations.json)
  • Agent-friendly: ships a Claude Code skill + CLAUDE.md snippet (no extra dependencies) and a real MCP server (optional dependency) so any MCP-compatible agent — not just Claude Code — can call it as a native tool
  • Cross-platform auto-start: systemd user service on Linux, launchd agent on macOS, both installed and started by install.sh; CI runs the full suite on both ubuntu-latest and macos-latest

Quick Start

Installation

git clone https://github.com/tyy130/port-authority.git
cd port-authority
./install.sh

install.sh installs the Python deps, symlinks the CLI into ~/.local/bin, writes a default config, and installs + starts the daemon as a background service — a systemd user unit on Linux, a launchd agent on macOS. Falls back to printing manual-start instructions if neither is available.

Make sure ~/.local/bin is on your PATH.

Prefer pip? pip install portauth gets you the CLI and library (published as portauth — see why), but you're on your own for starting the daemon (port-authority-daemon &) and keeping it running, since install.sh is what wires up the systemd/launchd service.

Usage

# Friendly wrapper — request a port
port myproject myservice
# -> 3005

# Same thing via the full CLI
port-request request myproject myservice --pool web

# Release it when the service stops
port-request release myproject myservice

# See everything currently allocated (with live active/inactive status)
port status
port-request status --project myproject

# Preview allocations that would be reclaimed as stale, or force it now
port gc
port gc --force

Project and service names must match [A-Za-z0-9_-]+ — no colons or slashes, since names get embedded in the registry key.

Configuration

Edit ~/.config/port-authority/config.yaml:

pools:
  web:
    range: [3000, 4000]
    description: "Web services, APIs"
  database:
    range: [5000, 6000]
    description: "Database ports"
  internal:
    range: [8000, 9000]
    description: "Internal services"

default_pool: web

# How long a port can sit allocated-but-unbound before the background
# sweep (and `port gc --force`) reclaim it.
stale_after_minutes: 60

# Extend or override the built-in known-service ports (postgres, redis,
# mysql, mongodb, and a couple dozen other common dev services -- see
# DEFAULT_KNOWN_SERVICES in port_authority/daemon.py for the full list).
# Requesting a port for one of these names tries the given port first,
# falling back to normal pool scanning if it's taken.
known_services:
  postgres: 5432 # overrides the built-in default
  my-internal-tool: 7777 # adds a new one

Changes require restarting the daemon (systemctl --user restart port-authority on Linux, launchctl kickstart -k gui/$(id -u)/com.portauthority.daemon on macOS).

Integration

Bash

PORT=$(port myproject myservice)
echo "Starting on port $PORT"

Python

from port_authority import request_port, release_port

port = request_port("myproject", "myservice")
print(f"Running on port {port}")

# When done:
release_port("myproject", "myservice")

Claude Code / Agents

Copy .claude/CLAUDE.md from this repo into your project (or @-reference it) so agents working in that codebase default to requesting a port instead of hardcoding 3000. See INTEGRATION.md for the full setup, including an optional pre-commit hook that flags hardcoded ports in diffs.

MCP server (any MCP client)

The .claude/ convention above only means anything to Claude Code — it's a file another tool has no reason to look at. For every other MCP-compatible client (Claude Desktop, other agent frameworks), Port Authority also ships as a real MCP server exposing request_port, release_port, port_status, and port_gc as native tools, over the standard stdio transport.

This is an optional dependency (requires Python 3.10+; the base install works on 3.9+ and stays stdlib-only otherwise):

pip install -r requirements-mcp.txt   # or: pip install "portauth[mcp]"

Register it with any MCP client by pointing at the script (adjust the path to your clone):

{
  "mcpServers": {
    "port-authority": {
      "command": "python3",
      "args": ["/path/to/port-authority/port_authority/mcp_server.py"]
    }
  }
}

The tools call the exact same HTTP client the CLI uses (port_authority.request_port etc.) — same auth, same error messages, same behavior, just exposed over MCP instead of the command line.

Architecture

  • Daemon: port-authority-daemon — a single-threaded http.server on 127.0.0.1:8888 (not exposed beyond localhost), plus a background thread that sweeps for stale allocations every 60s
  • Auth: a random token minted on first daemon start, stored at ~/.config/port-authority/token (0600), required as a Bearer token on every request
  • State: ~/.local/share/port-authority/allocations.json
  • CLI: port (friendly wrapper) / port-request (full subcommands: request, release, status, gc)
  • MCP server: port_authority/mcp_server.py (port-authority-mcp once installed) — optional, thin adapter over the same client the CLI uses

How allocation ownership works

The registry is the source of truth for who owns a port, not whether it's currently bound. A healthy running service IS bound to its port — that's expected, not a conflict. So:

  • Looking up an existing allocation (port myproject myservice called again) always returns the same port, regardless of whether it's currently active or idle.
  • Picking a brand-new port for a key that isn't in the registry tries the service's canonical port first if it's a known one (see Configuration), then skips anything currently bound — whether Port Authority knows about it or not — while scanning the pool range.
  • Reclaiming an abandoned allocation (owner crashed, never called release) only happens after the port has been continuously free for stale_after_minutes — a service that just hasn't started yet is never mistaken for a dead one.

Known limitations

  • Single machine. There's no clustering or shared state across hosts — each machine runs its own daemon and registry.
  • Trust model is "anyone who can read your dotfiles." The token is a local secret file, not real per-caller identity — it stops a stray unprivileged process from guessing its way in, not a determined actor already on your account.

License

MIT

Metadata

Release files for portauth 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for portauth 0.1.1
File Size Uploaded
portauth-0.1.1.tar.gz 23.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for portauth 0.1.1
File Interpreter ABI Platform
portauth-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 40.2 kB

Release files / portauth-0.1.1.tar.gz

Download URL portauth-0.1.1.tar.gz
Size 23.1 kB
Tags Source
SHA-256 checksum
How to use checksums
3ddfc05fa3c635629e8eec762d4e53cc5b77e095828ac59009a5b8eb60c9312f
BLAKE2b-256 checksum
How to use checksums
cdcc53e223af127c15c130e4d69f65a71dbf89ce698e52ef621e148ba3a140f1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.3

Release files / portauth-0.1.1-py3-none-any.whl

Download URL portauth-0.1.1-py3-none-any.whl
Size 17.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a8edd7b8dbedb3a9e9c088c4c4c6f3f012c90b854723c902d71a2a03c08b885f
BLAKE2b-256 checksum
How to use checksums
9f8a679ef2e2f534cccc9417ccbc4986616fddcacf305f168f7598ebbfbd5b76
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page