Skip to main content
Glama
README.md
# Port desk MCP

Port desk is a local [MCP](https://modelcontextprotocol.io) server written in Python. It answers four questions about this machine:

- `list_listeners` shows TCP and UDP sockets this user can see.
- `who_owns` names the process on a port and reports whether this process can bind it.
- `next_free_port` picks a port this process can bind.
- `wait_until_free` waits until a port can be bound.

The process speaks stdio. It does not open a port of its own except for a bind probe that closes immediately.

The same tools are also available as a Node server in [port-desk-mcp](https://github.com/ManiaSacha/port-desk-mcp). This repository is the Python package, so it can be installed with pip.

## Tools

| Tool | Call it when | Fields that matter |
| --- | --- | --- |
| `list_listeners` | You need the listening sockets | `protocol` (`tcp`, `udp`, or `all`), optional `port`, optional exact `address`. Returns `listeners`. |
| `who_owns` | You need the process on one port | `port`, optional `protocol`, optional exact `address`, optional `probeHost`. Returns `owners`, `bind.result`, and `hidden`. |
| `next_free_port` | You need a port to listen on | `host`, `start`, `end`, `count`, `exclude`. Returns `ports` and `complete`. |
| `wait_until_free` | A port is busy and you can wait | `port`, `host`, `timeoutMs`, `intervalMs`. Returns `free` and `result`. |

`bind.result` and `wait_until_free.result` are `free`, `in_use`, `forbidden`, or `error`. `hidden` is true when nothing visible owns the port and the bind probe still finds it taken. `next_free_port` trusts the bind probe, not `lsof`.

An empty `listeners` list means nothing matched. A wait that expires is a normal result with `free: false`.

## Safety

Port desk never signals another process and never reads command lines or environments. `list_listeners` and `who_owns` use `lsof` with a fixed argument list. Port and address filters run after that output is parsed. The bind probe listens on an IP address you pass, then closes the socket before the tool returns. A hostname is rejected so a DNS name cannot choose the address.

## Requirements

- Python 3.10 or newer
- `lsof` on `PATH` for `list_listeners` and `who_owns`

`next_free_port` and `wait_until_free` do not need `lsof`.

## Install

From a checkout:

```bash
python -m pip install -e ".[dev]"
pytest
```

The console script is `port-desk-mcp`. It speaks MCP over stdin and stdout.

After the package is on PyPI:

```bash
pip install port-desk-mcp
```

`pipx` and `uvx` work the same way: `uvx port-desk-mcp`.

## Claude

Claude Code can use this checkout after the package is installed into the Python on `PATH`:

```json
{
  "mcpServers": {
    "port-desk-mcp": {
      "type": "stdio",
      "command": "port-desk-mcp"
    }
  }
}
```

Claude Desktop uses the same command in `claude_desktop_config.json`. On macOS that file is `~/Library/Application Support/Claude/claude_desktop_config.json`. On Windows it is `%APPDATA%\Claude\claude_desktop_config.json`.

## Cursor

Add this to `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "port-desk-mcp": {
      "command": "port-desk-mcp"
    }
  }
}
```

## Publish to PyPI

The package name on PyPI is `port-desk-mcp`. From a machine logged in with a PyPI API token:

```bash
python -m pip install build twine
python -m build
twine check dist/*
twine upload dist/*
```

Create the token at [https://pypi.org/manage/account/token/](https://pypi.org/manage/account/token/). Keep it out of the repository. `twine upload` asks for the username `__token__` and the token as the password.

## Example

This is an example of `who_owns`, not a reading from your machine:

```json
{
  "port": 5901,
  "protocol": "tcp",
  "owners": [
    {
      "protocol": "tcp",
      "address": "127.0.0.1",
      "port": 5901,
      "pid": 2095,
      "process": "Xtigervnc",
      "user": "ubuntu"
    }
  ],
  "bind": { "host": "127.0.0.1", "result": "in_use" },
  "hidden": false
}
```

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md). The project is licensed under the [MIT License](LICENSE).