Skip to content

kylejfrost/portclaim

Repository files navigation

portclaim

A lightweight, file-based port registry for AI agents and dev tools.

CI License: MIT


The Problem

AI coding agents (Claude Code, Codex, Gemini CLI, Cursor, Windsurf) routinely spin up dev servers, databases, and test runners — each needing a port. When multiple agents or manual processes run on the same machine, port collisions are inevitable:

  • Agent A starts Next.js on port 3000. Agent B tries the same port. One fails silently.
  • A test runner grabs port 8080. A dev server tries the same port minutes later.
  • A process crashes without releasing its port. The next agent skips it forever.

Existing solutions are either point-in-time checks with TOCTOU race conditions (get-port, portfinder), static registries with no runtime enforcement, or heavyweight daemons.

The Solution

portclaim is a CLI tool and MCP server that provides persistent, cross-process port coordination with no daemon.

  • File-based registry — one JSON file per claimed port in ~/.portclaim/claims/. Inspectable with ls and cat.
  • No daemon — coordination via flock(2) on a shared lock file. Any process can read or write atomically.
  • Crash-resilient — every acquire call reaps claims whose owning PID has exited.
  • Bind probing — actually attempts to bind on both IPv4 and IPv6 loopback before claiming.
  • Agent-native — JSON output by default when piped. MCP server for any MCP-capable agent.

Quick Start

# Install
curl -fsSL https://raw.githubusercontent.com/kylejfrost/portclaim/main/install.sh | bash

# Or from source
cargo install --path .

# Claim a port for your dev server
PORT=$(portclaim acquire --service next-dev --prefer 3000)
next dev --port $PORT

# See what's running
portclaim list

# Release when done
portclaim release $PORT

Installation

Pre-built binaries

Download from GitHub Releases, or use the installer:

curl -fsSL https://raw.githubusercontent.com/kylejfrost/portclaim/main/install.sh | bash

Supports Linux (x86_64, aarch64) and macOS (x86_64, aarch64). Installs to ~/.local/bin/ by default.

From source

Requires Rust 1.70+.

git clone https://github.com/kylejfrost/portclaim.git
cd portclaim
cargo install --path .

Note: portclaim watch requires Linux (uses inotify). On macOS the binary builds and runs, but watch exits with a friendly error — use portclaim list --json polling or the HTTP dashboard instead.


AI Agent Integration

portclaim works with any AI coding tool. It provides two integration methods:

  1. MCP Server — for tools that support the Model Context Protocol
  2. CLI — for tools that can execute shell commands

Claude Code

Claude Code supports both MCP and skills for the deepest integration.

MCP Server (recommended):

claude mcp add portclaim -- portclaim mcp-serve

Or add to your project's .mcp.json:

{
  "mcpServers": {
    "portclaim": {
      "command": "portclaim",
      "args": ["mcp-serve"]
    }
  }
}

Skill file (teaches Claude Code when to use portclaim automatically):

# User-global (all projects)
mkdir -p ~/.claude/skills/portclaim
cp skill/SKILL.md ~/.claude/skills/portclaim/SKILL.md

# Or project-local
mkdir -p .claude/skills/portclaim
cp skill/SKILL.md .claude/skills/portclaim/SKILL.md

Codex (OpenAI)

Add to your project's AGENTS.md or instruct Codex directly:

When starting dev servers or any process that needs a port, use portclaim:

PORT=$(portclaim acquire --service <name> --prefer <port>)
<command> --port $PORT

Always release with: portclaim release $PORT
Check what's running with: portclaim list

Codex can also use the MCP server. Add to your MCP configuration:

{
  "mcpServers": {
    "portclaim": {
      "command": "portclaim",
      "args": ["mcp-serve"]
    }
  }
}

Gemini CLI

Gemini CLI supports MCP servers. Add to your ~/.gemini/settings.json:

{
  "mcpServers": {
    "portclaim": {
      "command": "portclaim",
      "args": ["mcp-serve"]
    }
  }
}

Or add project-level instructions in GEMINI.md:

Use `portclaim acquire --service <name> --prefer <port>` before binding to any port.
Release with `portclaim release $PORT` when done.

Cursor

Cursor supports MCP servers. Go to Settings > MCP and add:

{
  "mcpServers": {
    "portclaim": {
      "command": "portclaim",
      "args": ["mcp-serve"]
    }
  }
}

Or add a .cursor/mcp.json file in your project root with the same config.

Windsurf

Windsurf supports MCP. Add to your MCP configuration in Settings > Cascade > MCP:

{
  "mcpServers": {
    "portclaim": {
      "command": "portclaim",
      "args": ["mcp-serve"]
    }
  }
}

VS Code + Continue

Continue supports MCP servers. Add to your Continue config (~/.continue/config.json):

{
  "experimental": {
    "modelContextProtocolServers": [
      {
        "transport": {
          "type": "stdio",
          "command": "portclaim",
          "args": ["mcp-serve"]
        }
      }
    ]
  }
}

Cline

Add to Cline's MCP settings (VS Code: Cline > MCP Servers > Configure):

{
  "mcpServers": {
    "portclaim": {
      "command": "portclaim",
      "args": ["mcp-serve"]
    }
  }
}

Any MCP Client

The MCP server speaks JSON-RPC 2.0 over stdio:

portclaim mcp-serve

It exposes five tools: portclaim_acquire, portclaim_release, portclaim_find, portclaim_list, portclaim_reap.

Any CLI-capable Agent

For agents that can run shell commands but don't support MCP:

# Acquire a port
PORT=$(portclaim acquire --service my-service --prefer 3000)

# Find a service
portclaim find --service postgres

# List all claims
portclaim list --json

# Release
portclaim release $PORT

All commands output JSON when stdout is not a TTY, making them easy to parse programmatically.


MCP Server Tools

Tool Description Required Params
portclaim_acquire Claim an available port service
portclaim_release Release a claimed port port, service, or project
portclaim_find Look up a port by service/project service or project
portclaim_list List all claimed ports (none)
portclaim_reap Clean up stale claims (none)
Full parameter reference

portclaim_acquire:

Parameter Type Required Description
service string yes Logical service name
project string no Project identifier (defaults to cwd basename)
prefer integer no Preferred port to try first
lease string no "ephemeral" (default) or "persistent"

portclaim_release:

Parameter Type Description
port integer Specific port number to release
service string Release by service name
project string Release all ports for a project

portclaim_find:

Parameter Type Description
service string Service name to look up
project string Filter by project

portclaim_list:

Parameter Type Description
project string Filter by project
agent string Filter by agent

portclaim_reap:

Parameter Type Description
dry_run boolean Report stale claims without removing

CLI Reference

All commands support --json to force JSON output. JSON is used automatically when stdout is not a TTY.

portclaim acquire

Claim an available port for a service. Prints the assigned port to stdout.

portclaim acquire --service next-dev --prefer 3000
portclaim acquire --service postgres --lease persistent --prefer 5432
portclaim acquire --service api --project my-app --prefer 4000 --json

Port resolution: --prefer first, then project config range, then global pool.

Flag Default Description
--service (required) Logical service name
--project cwd basename Project identifier
--prefer (none) Preferred port. Falls back to services.<name>.port from config if unset.
--lease ephemeral ephemeral or persistent. Falls back to services.<name>.lease from config if unset.
--agent $PORTCLAIM_AGENT Agent identifier
--pid current process Override the PID stored in the claim. Use when registering on behalf of another process (e.g., PM2). The owning PID must stay alive — claims are reaped on PID death.

portclaim release

portclaim release 3000
portclaim release --service next-dev
portclaim release --project my-app

portclaim list

portclaim list
portclaim list --project my-app
portclaim list --json

portclaim find

portclaim find --service postgres    # prints: 5432
portclaim find --service next-dev --project my-app --json

portclaim watch

Stream real-time claim/release events via inotify (Linux only):

portclaim watch
portclaim watch --json    # newline-delimited JSON

On non-Linux platforms, portclaim watch exits with a friendly message; poll portclaim list --json instead.

portclaim reap

Clean up stale claims (dead PIDs, unbound ports):

portclaim reap
portclaim reap --dry-run
portclaim reap --max-age 1h --lease ephemeral

portclaim doctor

Run a registry health check. Reaps stale claims (dead PIDs), releases phantom claims (claim file present but port not bound), sweeps orphan .json.tmp files left by interrupted writes, and verifies the config and lock file:

portclaim doctor

portclaim http

Start embedded dashboard:

portclaim http                  # binds 127.0.0.1:8484
portclaim http --port 8484
portclaim http --bind 0.0.0.0   # opt-in LAN exposure

Defaults to 127.0.0.1 so the dashboard is loopback-only. Pass --bind 0.0.0.0 to expose it on the local network.

The dashboard exposes:

  • GET / — HTML status view
  • GET /api/claims — JSON list of all claims
  • POST /api/reap — reap stale claims (POST-only to prevent CSRF; the dashboard's "Reap Stale" button is a <form method="post">)

portclaim completions

Print a shell completion script:

portclaim completions bash >> ~/.bashrc
portclaim completions zsh  > "${fpath[1]}/_portclaim"
portclaim completions fish > ~/.config/fish/completions/portclaim.fish

Configuration

Config lives at ~/.portclaim/config.toml, created by portclaim init.

[pool]
range = "3000-9999"          # Port range for dynamic assignment
exclude = [5432, 6379]       # Skip externally managed ports

[projects.my-app]
prefer = 3000                # Try this port first
range = "3000-3009"          # Fallback range

[services.postgres]
port = 5432                  # Conventional port for this service
lease = "persistent"         # Lease default if --lease isn't passed

[services.redis]
port = 6379
lease = "persistent"

[services.<name>] entries are consulted by acquire whenever the service name matches. Both fields are defaults that the CLI can override:

  • port is used as the preferred port when --prefer is not provided.
  • lease sets the default lease when --lease is not provided.

How It Works

~/.portclaim/
├── .lock                    # flock file for atomic operations
├── config.toml              # Port pool and service definitions
└── claims/
    ├── 3000.json            # One file per claimed port
    ├── 5432.json
    └── 8080.json

Acquire flow:

  1. Take exclusive flock on ~/.portclaim/.lock
  2. Reap stale claims whose PID is dead (ephemeral and persistent)
  3. Try the effective preferred port (CLI --prefer, falling back to services.<name>.port from config), then the project range, then the global pool
  4. Verify the candidate port is free via bind() on 127.0.0.1 and [::1]
  5. Write the claim atomically (write .tmpfsyncrename)
  6. Release the lock, print the port

Crash recovery: every acquire reaps claims whose PID no longer exists, regardless of lease type. Persistent leases are protected only against age-based reaping (reap --max-age); they still go away when the owning process dies. Use --pid <pid> to record a different process's PID when registering on its behalf.

Claim file format

{
  "port": 3000,
  "pid": 48291,
  "agent": "claude-code",
  "project": "my-app",
  "service": "next-dev",
  "claimed_at": "2026-03-17T14:32:01.123456Z",
  "lease": "ephemeral"
}

Helper Scripts

The contrib/ directory includes optional helper scripts:

  • portclaim-start — wrapper that acquires a port, runs your command, and releases on exit:

    portclaim-start next-dev --prefer 3000 -- next dev --port
  • portclaim-pm2-sync — syncs PM2 process manager state with the portclaim registry:

    portclaim-pm2-sync            # register all PM2 services
    portclaim-pm2-sync --dry-run  # preview changes

Prior Art

Influence What portclaim took from it
get-port Probe-then-claim pattern, preference lists
Docker PortAllocator In-memory bitmap tracking, range-based allocation
Kubernetes NodePort Dual-band allocation (preferred vs. fallback)
systemd socket activation Crash resilience via lifecycle-based cleanup

The gap: no existing tool provides a lightweight, persistent, cross-process port registry for local dev. portclaim fills it with no daemon, crash resilience via PID reaping, and native AI agent integration via MCP.


Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Run the tests:
    cargo test --all-targets
    cargo clippy --all-targets -- -D warnings
    cargo fmt --check
  4. Submit a pull request

License

MIT

About

File-based port registry for AI agents

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors