# Stop Guessing localhost Ports with portmap and TypeScript

> Source: <https://dev.to/paladini/stop-guessing-localhost-ports-with-portmap-and-typescript-4876>
> Published: 2026-10-24 12:31:02+00:00

AI coding agents are very good at remembering common conventions. That is also why they can be very confident and completely wrong about `localhost:3000`.

One project may expose a Vite frontend on port 5173. Another may run an API on 8080. A Docker mapping can make the host port different from the container port, while an `.env.local` file may still point to a service that is not running today. The resulting error often looks like CORS, authentication, or a broken API client when the real problem is a stale URL.

[portmap](https://github.com/paladini/portmap) is a small MIT-licensed TypeScript CLI and read-only MCP server for this local-development problem. It combines declared configuration, optional OS listener data, and environment references into a `.portmap.json` report with findings such as `PRT-04` for an environment URL that points at a port with no listener.

This tutorial uses the current repository configuration and the checked-in mismatch fixture. You will build portmap from source, run a deterministic static scan, inspect the generated findings, and understand where the tool deliberately stops.

```
git clone https://github.com/paladini/portmap.git
cd portmap
npm ci
npm run build
node dist/cli.js declare fixtures/mismatch --json
```

The output identifies a declared Vite port and two unresolved localhost environment URLs. That gives an agent or a developer concrete evidence before changing application code.

You need:

`engines.node` as `>=20`.` declare` command does not need a running service.
The repository is public, non-archived, non-forked, and uses the MIT license. At the time of writing, the repository reports version `0.1.0` in `package.json` and does not provide a GitHub release, so the reproducible path below pins the source checkout by its current default branch rather than claiming a released package.

Clone the project and install the lockfile exactly:

```
git clone https://github.com/paladini/portmap.git
cd portmap
npm ci
npm run build
```

The build runs TypeScript with the repository's `tsconfig.json` and produces the `dist` directory. The CLI entry point is `dist/cli.js`; the package metadata also exposes a `portmap` binary when the project is installed as a package.

The current README documents `npm ci`, `npm run build`, and `node dist/cli.js` as the source-based installation path. It also documents `npx portmap` as a future npm option, so do not use the latter as if a published npm package were already verified.

The safest first check is `declare`. It reads project files without requiring the application's processes to be running:

```
node dist/cli.js declare fixtures/mismatch --json
```

The fixture is intentionally broken. Its configuration declares a Vite server on port 5173, while `.env.local` contains references to `http://localhost:3000` and `http://localhost:8080`. Because no matching service is declared for those references, the report contains unresolved environment edges.

The important part of the JSON is the relationship between the declared service, references, and findings:

```
{
  "services": [
    {
      "id": "vite",
      "declared": {
        "port": 5173,
        "sources": ["vite.config.ts:server.port"]
      },
      "status": "down"
    }
  ],
  "findings": [
    {
      "id": "PRT-01",
      "severity": "error"
    },
    {
      "id": "PRT-04",
      "severity": "error",
      "file": ".env.local:1"
    }
  ]
}
```

The complete report includes the second `PRT-04` for the port 8080 reference. The fixture has no listener data because this static command does not inspect the operating system.

Run `declare` against your application first:

```
node /path/to/portmap/dist/cli.js declare /path/to/your-app --json
```

If the report is useful as persistent agent context, write it to the project root:

```
node /path/to/portmap/dist/cli.js declare /path/to/your-app --write
```

This creates `.portmap.json`. The repository's schema documentation describes it as the artifact agents can read instead of guessing a URL. Whether you commit it is a project decision: committing it can make a known local topology available to future sessions, while ignoring it avoids storing machine-specific runtime data.

When processes are running, the full command adds an OS listener scan:

```
node /path/to/portmap/dist/cli.js scan /path/to/your-app --json
```

The full scan can report declared ports that are not listening, listeners without declared configuration, mismatched Docker host and container ports, and environment references that do not resolve. The README also documents `workspace` for projects in sibling folders and `listen` for inspecting listeners directly.

portmap includes a read-only MCP stdio server. After building, the command is:

```
node /path/to/portmap/dist/cli.js mcp
```

For Cursor or Claude Code, the documented configuration points the MCP command at the built CLI:

```
{
  "mcpServers": {
    "portmap": {
      "command": "node",
      "args": ["/path/to/portmap/dist/cli.js", "mcp"]
    }
  }
}
```

The exposed tools are intentionally narrow: retrieve a full report, get a smaller service graph, resolve a URL, or filter findings. The server does not start or stop services, modify application files, or infer ports with an LLM.

The core design separates three kinds of evidence:

That separation makes a failure actionable. `PRT-04` means an environment URL points at a port with no listener, while `PRT-02` warns about a listener without a corresponding declaration. Those are different repair paths, even though both can surface as a browser network error.

The CLI can turn findings into a non-zero exit status. For example, this command fails when at least one error remains:

```
node dist/cli.js scan . --min-findings 1 --min-severity error --json
```

Use this as a local-development guard, not as production monitoring. The project is designed for local service topology and explicitly does not provide uptime or production health checks.

There are several important boundaries:

`PRT-02` warning or remain undiscovered.`ports:` patterns, not every Compose feature.
The tool reads local configuration and socket information. It is not a secret manager, a process supervisor, or a security guarantee. Keep `.env` values out of reports and repositories when they contain credentials; the useful signal is the referenced host and port, not the secret itself.

From the clean checkout, verify the source path and fixture again:

```
npm ci
npm run build
node dist/cli.js declare fixtures/mismatch --json > mismatch-report.json
```

Confirm that the JSON parses and contains `PRT-01` plus the unresolved `PRT-04` findings. For your own project, change one environment URL to the actual declared port, rerun the command, and compare the findings. That gives you a small, reproducible experiment instead of relying on whether an agent happens to guess correctly.

The repository also includes `npm test`, `demo:mismatch`, and `demo:workspace` scripts. In a clean Windows checkout, the build and static fixture declaration are useful smoke checks; if a runtime listener check behaves differently on your platform, treat that as an environment-specific result and inspect the generated report rather than assuming the listener model is universal.

No. It reports topology. It does not start, stop, or restart services.

No. The repository describes the scanner and MCP server as deterministic and zero-LLM. The MCP interface is for letting an agent consume evidence, not for outsourcing discovery to a model.

The README lists npm installation as a future option. The verified current path is cloning the repository, running `npm ci`, and building locally.

`.portmap.json` be committed?
Only if the values are stable and safe to share. A committed static report can help future agents, but machine-specific listeners and environment details may be inappropriate for a repository.

The practical lesson is simple: make local topology an input to debugging. Build portmap from the documented source path, run `declare` before starting a long investigation, and let the report show which URLs are declared, which are actually reachable, and which assumptions are unsupported.

If you try this in a monorepo, which mismatch would be most valuable to catch first: stale environment URLs, Docker port remapping, or cross-workspace references?

AI assistance disclosure: AI assistance was used to organize and edit this tutorial. Repository behavior, commands, metadata, limitations, and verification claims were checked against the current public source and the clean checkout described above.
