chokepoint is a policy-enforcing proxy for MCP tool servers, in Go, public and Apache-2.0. It sits between an agent and its tools, and it scores sessions in the request path — while the calls are happening, when refusing them still means something.
Then I measured it against the least sophisticated attacker I could imagine, and found that a quarter of the detector is dead weight.
This post is both halves. The build is the easy half.
Moving the Measurement Into the Request Path
TestInFlightRepliesSurviveClientClose now, and it exists because the unit tests were all green while the thing did not work.
Unit tests check that your components do what you think. Only an end-to-end run checks whether what you think is the system.
Thresholds Come From a Table, Not From a Feeling
| shape | score | |
|---|---|---|
| poll-one-file | 0.007 | benign |
| focused-debug | 0.214 | benign |
| build-pipeline | 0.225 | benign |
| cyclic-sweep | 0.608 | suspicious |
| exfil-crawl | 0.648 | suspicious |
| randomised-sweep | 0.666 | suspicious |
The Part Where I Measured My Own Detector
transition_novelty — the fraction of tool-to-tool transitions seen exactly once — reads 0.000 for five of the six shapes. The sixth reads 0.062.
It holds a quarter of the total weight. Across my entire calibration suite, it contributes essentially nothing.
I'd documented a soft version of this in the README: novelty is "informative for short sessions and large tool vocabularies, and close to inert otherwise." True, and vague enough to be comfortable. So I went and measured the actual shape of the failure, running the feature over synthetic sessions at increasing lengths:
| session length | distinct tool every call | vocabulary of 20, looping |
|---|---|---|
| 20 | 1.000 | 1.000 |
| 40 | 1.000 | 0.050 |
| 60 | 1.000 | 0.000 |
| 240 | 1.000 | 0.000 |
A feature that any repeating sequence sets to zero is not measuring intent. It is measuring whether the attacker bothered to write a loop.
Sloppy Versus Patient
What It Cannot Do
- 4 of 20 features aren't computed. Three need a peer or historical baseline the process doesn't have on a cold start; one, privilege deviation, has no representation in the protocol at all. That count is machine-checked by a test, because an earlier draft claimed 8 of 20, which was wrong, and it reached a CV before anyone caught it.
- Target extraction is heuristic. It keys on conventional argument names, so a server using unusual ones under-reports breadth.
- Padding defeats the score. Repeat calls lower breadth ratio and entropy together. This raises the attacker's cost; it does not close the class.
- stdio transport only. Streamable HTTP is the largest functional gap, and it's next.
Why Publish the Hole
Build the guard. Then go looking for the door it doesn't cover, and publish the door.Source: github.com/BipinRimal314/chokepoint. The calibration table regenerates with
go test ./internal/detect -run Calibration -v.