← Back to Articles

Something is squatting on port 3000

On agent-started processes that nobody can see, stop, or clean up — and the MCP server that puts them in your overseer.nvim task list

I run my AI agent in a terminal split inside my IDE. Most of the time, it's a good arrangement. The DX beats the dedicated editor extensions, since the AI companies release console-first and the CLI is where new features land. The agent edits files, I watch the diffs and see what it's doing and why, and everyone stays in their lane.

Then the agent decides it needs a dev server. It runs pnpm dev with its shell tool, the way it runs everything else, and at that moment a process comes into existence that my editor knows nothing about. It's not in my task list. <leader>ow shows my overseer tasks, humming along with their output buffers and their proper lifecycle — and this thing is not among them. I can't stop it from the editor. I can't tail it. If the agent's session dies, the process doesn't die with it; it just loses its parent and keeps listening on port 3000, along with whatever child processes it spawned along the way.

I found this out the usual way: a fresh pnpm dev refusing to bind, a ps aux | grep node full of processes whose sessions had ended an hour ago, and me killing PIDs at some hour when I should have known better.

The irritating part is the asymmetry. The tasks I start go through overseer.nvim, which is the piece of my config I'd defend in a fight. My tasks appear in a list. They have output I can open. When I stop one, its whole process tree gets torn down properly. The agent, sitting in a terminal buffer inside the same editor, gets none of that. It has a shell and a prayer.

The fix, stated plainly

So I built overseer-nvim-mcp, an MCP server that gives the agent the same view of overseer that I have. Seven tools: list tasks, list templates, run, tail, restart, stop, dispose. When the agent needs a dev server now, it starts an overseer task — a real one, in the same list as mine, with the same teardown semantics. I can watch it. I can stop it. When the session ends, nothing is orphaned, because overseer owns the process, not the agent's shell.

An agent starts a dev server as an overseer task; the user watches its output in the task list and stops it themselves

That's the whole idea. The rest of this post is the details I cared about getting right, because the idea is obvious and the details are where it either works or annoys you forever.

The transport is just an environment variable

My least favorite genre of developer tool is the one that "discovers" things. It greps process lists, hashes working directories, scans for sockets, and works right up until it confidently connects to the wrong instance.

This server does none of that, because Neovim already solved the problem. Every process spawned inside a Neovim terminal buffer inherits $NVIM — the address of that editor's RPC socket. The MCP client (Claude Code, in my case) inherits it from the terminal. The server is a child of the client, so it inherits it too. Everything the server does is nvim_exec_lua over msgpack-RPC to that one address.

There is no configuration for this. There couldn't be — there's nothing to configure. The variable either points at the Neovim you're sitting in, or it isn't set, in which case the server registers zero tools and gets out of the way. Run the agent outside Neovim and the server simply has nothing to say, which is the correct amount.

Sharing a task list without stepping on it

The part that took actual thought: the agent's tasks and my tasks live in the same list. That's the point — one list, one view. But it means an agent deciding to "clean up" could kill the dev server I started, and an agent restarting "its" task could restart mine.

So every task is tagged with who started it. stop, restart, and dispose refuse to touch a running task the agent didn't start, unless the agent explicitly passes a force flag. The reasoning is about failure costs: a wrong refusal costs the agent one extra tool call to say "yes, really." A wrong stop kills a process I was using. Those aren't symmetric, so the default isn't either.

The tasks the agent starts, it can manage freely. Mine, it has to ask about. This has so far produced exactly the behavior I wanted: the agent restarts its own watcher without asking and leaves my servers alone.

Agents are terrible at waiting

An agent that starts a dev server immediately wants to know when it's ready. The naive pattern is polling — tail the output, sleep, tail again, burn tokens reading the same Turbopack banner five times.

overseer_tail takes a wait_for parameter instead: a regex, and the call blocks until a line matches it. The agent says "wake me when you see ready - listening" and gets exactly one response, when it matters. The call returns on a match, on task exit, or at the timeout — and the result says which of the three happened, so a timeout can't be misread as a server that came up fine. The blocking happens in the server process, outside Neovim, so the editor never freezes while somebody waits on a webpack build.

This is a small feature that changes how the whole thing feels. The agent starts a server, waits on the ready line, and moves on, instead of guessing at sleep durations in a loop.

Templates, or: your repo already knows its commands

Overseer discovers runnable tasks through providers — npm scripts, make targets, just recipes, go-task, VS Code task definitions. Whatever your setup discovers, the agent sees through overseer_list_templates, so instead of reconstructing npm run test:e2e -- --headed from a README, it runs the task by name, the same one I'd pick from the menu.

Plenty of repos return an empty template list, and that's fine. A raw command is the primary path, not a fallback; templates are a bonus when the repo declares them.

And when the task doesn't exist yet, it can create one

A raw command is a one-off, though. It lives in the task list until the session ends, and next time the agent has to reconstruct it. Some commands deserve better — every project has that one incantation you run daily that never made it into an npm script, a Makefile, or a Taskfile.

For those, the server publishes a slash command (/mcp__overseer__directory_local_task in Claude Code) that has the agent write the task into the project itself, using overseer's own register_template in a .nvim.lua at the project root. It's generated against live editor state rather than being a canned recipe: it checks whether exrc is actually enabled and whether a .nvim.lua already exists, and it warns about the two things that make a correct setup look broken — Neovim will ask you to :trust the file, and the new task won't appear until a restart.

The part I find interesting: the agent isn't limited to running the tasks my project already declares. It can author new ones — real overseer templates that show up in <leader>ow next to everything else and outlive the session that created them. The AI stops being a guest that borrows my task runner and starts leaving useful things behind in it.

Who this is for

Any MCP client running inside a Neovim terminal works: Claude Code, Codex CLI, Gemini CLI, opencode. If you're on codecompanion or avante, mcphub.nvim gets you there.

If you're on LazyVim, you're one :LazyExtras toggle away — enable the overseer extra, then:

bash
1claude mcp add overseer -- npx -y overseer-nvim-mcp

That's the entire setup. The package is a single file on npm with zero runtime dependencies, which I mention because the median MCP server seems determined to install half of npm before it says hello.

There's a demo gif and the full tool reference in the README, and the server is in the official MCP registry as io.github.impossiblecode/overseer-nvim-mcp.

The general problem, briefly

Stepping back: the orphaned dev server is one instance of something more general. Agents run in our editors now, but they interact with our machines like SSH guests — a shell, a working directory, no awareness of the tooling we've spent years arranging around ourselves. Every long-running process an agent starts through a bare shell is state that our editor can't see and the agent can't reliably clean up.

The answer, at least here, wasn't to build the agent its own parallel infrastructure. It was to hand it mine. Overseer already knew how to run tasks, show them, and tear them down. The missing piece was a protocol adapter and some manners.

It works. My task list finally tells the truth about what's running on my machine, including the parts an AI started. And the next time something is squatting on port 3000, at least it will have a name.