Skip to content

feat(mcp): add a dry run to kill_port - #67

Merged
Mapika merged 1 commit into
mainfrom
feat/mcp-kill-dry-run
Aug 1, 2026
Merged

Mapika merged 1 commit into
mainfrom
feat/mcp-kill-dry-run

Conversation

@Mapika

@Mapika Mapika commented Aug 1, 2026

Copy link
Copy Markdown
Owner

First of Tier 3. kill_port is the one MCP tool with consequences, and an agent had no way to check what it was about to do — inspect_port answers a different question, since it lists every process on the port including non-listeners, while a kill targets only listeners with a resolvable PID, deduplicated across tcp/tcp6.

The preview cannot disagree with the kill

That is the one bug a dry run must not have, so target resolution moved into kill_targets(port) and both paths call it. A preview computed any other way could drift from what actually gets signalled.

It reports what would really happen

Windows has no signal equivalent — kill_process ignores force and always calls TerminateProcess. A dry run reporting SIGTERM there would promise a graceful shutdown that cannot happen, so planned_kill_action mirrors the platform split:

force=false force=true
unix SIGTERM SIGKILL
windows TerminateProcess TerminateProcess

Still withheld under --read-only

A dry run is read-only in effect, so allowing it there was tempting. I did not: that server's contract is that no destructive tool is listed, and a kill_port in the list that refuses half its arguments is a worse contract than an absent one. inspect_port already answers what is on the port.

Verification

Driving the real server over stdio, against a live listener:

==> web.js is pid=7 on port 3000

==> 1. dry run
  {"port":3000,"dry_run":true,"would_kill":[{"pid":7,"process":"node","signal":"SIGTERM"}]}
  port 3000 still listening after dry run: 1
  pid 7 still alive: yes

==> 2. dry run with force=true
  {"port":3000,"dry_run":true,"would_kill":[{"pid":7,"process":"node","signal":"SIGKILL"}]}

==> 3. read-only server refuses the dry run
  kill_port is disabled: this server was started with --read-only

==> 4. the real kill then works, on the PID the dry run named
  {"port":3000,"killed":[{"pid":7,"process":"node","signal":"SIGTERM"}]}
  port 3000 listening now: 0

The dry run named pid 7 and the real call signalled pid 7 — same target, which is the point.

One test asserts the description advertises dry_run: the description is what the model actually reads, and an undiscoverable preview is no safer than none.

185 tests, fmt/clippy clean, macOS + Windows type-check.

Next in Tier 3: process tree in inspect_port, and a change-diff tool.

The preview resolves its targets through the same code the real call uses,
so the two cannot disagree. It also reports the action that would actually
be taken: Windows ignores force and always terminates hard, so promising a
SIGTERM there would be a lie.

kill_port stays withheld under --read-only, dry run included: that server's
contract is that no destructive tool is listed at all.
@Mapika
Mapika merged commit 5c03bbd into main Aug 1, 2026
6 checks passed
@Mapika
Mapika deleted the feat/mcp-kill-dry-run branch August 1, 2026 07:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant