Releases: BerkantACUN/guardmcp
Release list
v0.15.1 — Marketplace-valid action description
Fixes the action.yml description, which ran to 205 characters against the GitHub Marketplace limit of 125 and blocked the listing. Shortened to 106 without dropping what the action does.
No behaviour change — same 31 rules, same CLI, same SARIF output as v0.15.0.
v0.15.0 — a baseline you can actually create, and two rules about tool names
Added — guardmcp baseline, completing a flag that could not be used
--baseline has been able to read a baseline file since the first release,
and nothing could write one. Fingerprints appear in no output format either,
so the documented workflow was to pipe JSON through jq and hand-assemble the
file. The flag was documented, wired, tested and unusable.
guardmcp baseline # record what is already there
guardmcp scan --baseline .mcpguard-baseline.json # fail only on what came afterThat is the difference between a scanner a team adopts and one they remove
after the first red build on an existing repository.
Three things it does deliberately:
- Writes through the scan, not beside it. A baseline assembled by a second
code path could suppress a different set of findings than the scanner
produces, and the failure mode is a green CI nobody has reason to distrust. - Records rule, severity, logical path and message — not just a fingerprint.
A baseline is a list of accepted risks and it gets reviewed in a pull
request; a diff of opaque hashes cannot tell a reviewer whether it accepts a
formatting nit or a production credential, so it gets approved either way. - Refuses to overwrite without
--force, and writes no file when there is
nothing to record. An empty baseline implies a triage that never happened.
Added — MCPG-901 and MCPG-902: the model picks a tool by its name
MCP does not namespace tool names per server, so two servers offering the same
name leave the model choosing between tools it cannot tell apart.
MCPG-901 flags a tool name offered by more than one server. Which one a
call reaches depends on the client's merge order rather than on any choice the
user made. MCPG-203 already covers the loud version of this — a description
claiming to intercept another tool — but an attacker who simply registers a
colliding name writes no suspicious description at all. The collision is the
only signal there is.
MCPG-902 flags a name built from characters that render as another tool's
name. Cyrillic U+0430 reads as a in every font a terminal or an approval
dialog uses:
| Name | Bytes |
|---|---|
search |
s e a r c h — U+0061 |
seаrch |
s e а r c h — U+0430 |
Those two lines are the same word on screen and different strings to every
comparison the client makes. The rule fires only on mimicry — a name whose
ASCII skeleton equals another tool's real name while the strings differ —
never on non-ASCII alone, because araştir is a tool name and a rule that
flags one teaches people to skip it.
Checked against a real 11-server, 106-tool setup: no collisions and no false
positives, including under normalisation that ignores case and separators.
Fixed — the remote rug-pull test could talk to the server it had just killed
It went red once on windows/node20 and passed on re-run. Re-running was the
wrong response. stopFixture stopped waiting after three seconds whether or
not the process had exited, and the test then restarts a fixture on the same
port — so when the old process still held it, the scan connected to the old
server, saw the tools it had already pinned, and reported no drift. An empty
findings array looks exactly like a broken detection; the detection was fine.
It now escalates to SIGKILL and binds the port itself before restarting, which
is the only real proof the previous listener is gone. stderr: () => {} was
discarding the diagnostics that would have explained the failure; they are now
attached to the assertion.
Fixed — the README claimed nine categories over a table of ten
The invariant test pinned the rule count and nothing else, so the category
claim drifted unchecked. Counting was the wrong thing to pin — the table groups
for readability, so its row count will never equal the code's — and the test
now checks that every category the code uses is documented at all.
v0.14.0 — 'format' and 'drop' were being read as destructive verbs
[0.14.0] — 2026-09-08
Fixed — "format" and "drop" were reported as destructive verbs
Found by scanning real servers rather than by the test suite.
@upstash/context7-mcp (3.9M downloads a month) has a documentation-lookup
tool, annotated readOnlyHint: true, whose description reads "...provides a
library ID in the format '/org/project'...". MCPG-303 reported it as a
destructive tool hiding behind a read-only annotation — on the strength of the
word format. hostinger-api-mcp produced the same false positive on a
list...Attributes tool.
In prose, "format" is almost always a noun ("JSON format", "in the format X",
"date format"), and "drop" usually means drag-and-drop, a drop-down, or a drop
shadow. Both are kept, but each now has to name what it acts on:
| Reported | Not reported |
|---|---|
Drops the users table. |
Supports drag and drop of files. |
drop_index |
Renders a drop-down menu. |
Formats the disk before installing. |
Returns the result in JSON format. |
The object has to sit in the same sentence, so a verb in one sentence can't
pair with a noun in the next.
A finding like the context7 one costs more than the rule was ever going to
catch: it teaches a user to ignore the rule. Rescanning the eight servers that
surfaced it: 3 false positives, now 0, with no true positive lost.
Audit note
The same scan covered the four official reference servers
(server-filesystem, server-memory, server-sequential-thinking,
server-everything — 4.7M downloads a month between them) and eight widely
used third-party ones. No vulnerability was found in any of them. The only
substantive observation is that several servers advertising many
state-changing tools declare no logging capability, which MCPG-702 reports.
v0.13.0 — resource templates (MCPG-210) and MCP08 from the protocol (MCPG-702)
[0.13.0] — 2026-09-07
Closes the three gaps this project's own docs had been listing as uncovered.
Added — MCPG-210, resource templates
A resource points at one URI, so a reviewer can look at it. A resource
template names a shape the caller fills in, so there is nothing to review
until it is expanded — and in an agent, the thing supplying the variable is the
model. file:///{path} is arbitrary local file read, advertised as a feature.
The distinction the rule turns on is RFC 6570's expansion operators:
| Operator | Reserved chars | Consequence |
|---|---|---|
{var} |
percent-encoded | a value cannot leave its path segment |
{+var} / {#var} |
passed through | ../../etc/passwd survives |
So file:///srv/docs/{name}.md is anchored and file:///srv/docs/{+name} is
not, though they look alike. Also flags a variable in the host position
(https://{host}/api) — the caller choosing the destination is SSRF by
construction.
Added — MCPG-702, MCP08 read from the protocol
A server declares its capabilities at initialize. logging is the channel
through which it reports what it did; without it a server can still act and
simply has no way to say so.
Deliberately requires both halves: the capability is absent and the
server advertises a tool that changes something. Plenty of read-only servers
legitimately have nothing to report, and flagging every server without
logging would repeat exactly the mistake MCPG-404 was rewritten to undo — a
rule that fires on everything carries no information.
Complements MCPG-701, which reads the config: a committed kill switch is one
failure, never having the capability is another, and a config can be clean
while the server still cannot report anything.
Changed
- Live introspection now also fetches
resources/templates/listand keeps each
server's declared capabilities. A template listing that fails does not lose
the resources already collected.
v0.12.0 — three defects found by attacking the tool, not by the test suite
[0.12.0] — 2026-09-07
Three defects found by adversarial testing, not by the test suite.
Fixed — a mistyped flag looked like a crash and exited 1
Nine of thirteen argument-validation paths printed a raw Node stack trace and
exited 1. Exit 1 means "findings at or above the threshold", so a typo was
reported to CI as a security failure — wrong, and the kind of wrong that
quietly devalues every other exit code the tool produces.
parse() is synchronous while every action is async, so a rejected action
escaped as an unhandled rejection. All usage errors now print a message and
exit 2.
Fixed — --ignore-rule accepted ids that do not exist
--rules NOPE-999 errored; --ignore-rule NOPE-999 was silently accepted. A
suppression that quietly does nothing is worse in a security tool than an
error: the user believes a rule is muted and it is not. Both are validated now.
Added — MCPG-202/206/208 detect terminal control sequences
Found by pointing a deliberately hostile MCP server at --live. A description
carrying ESC[8m ("conceal"), a lone CR (overwrites the line just printed) or
BS (erases what precedes it) reads one way to a human in a terminal and another
way to the model. Same attack as a zero-width character, different mechanism.
guardmcp was already sanitising these on output, so a reviewer's terminal
was never at risk — but nothing reported that the server had sent them.
CRLF is excluded: it is a Windows line ending, and flagging it would fire on a
large share of honest descriptions.
Verification performed
| Method | Result |
|---|---|
| Deletion mutation, 27 rules | 27/27 killed |
| Always-fire mutation, 9 detectors | 9/9 killed |
| Fuzzing, 24 adversarial configs (deep nesting, 20k servers, 200MB strings, prototype pollution, BOM, surrogates, ANSI, path traversal) | 24/24 handled, no crash, no hang |
| Hostile MCP server, 6 modes (100k tools, 200MB description, ANSI injection, 50k-deep schema, garbage JSON-RPC, silent, slowloris) | 6/6 survived, timeouts honoured, 0 escape bytes reached stdout |
| Prototype pollution, in-process | Object.prototype untouched, __proto__ key dropped |
| Real-world scan, 80 official-registry servers | 0 findings |
| Determinism | three runs byte-identical, fingerprints stable |
| Idempotence | a scan writes nothing to disk |
v0.11.0 — MCPG-404 was wrong about most servers it reported
[0.11.0] — 2026-09-07
Fixed — MCPG-404 was wrong about most of the servers it reported
The rule read the config alone: a remote server with no Authorization header
was reported as unauthenticated. Measured against the official MCP registry on
2026-09-07 that was wrong two times in three — six advertised endpoints
were probed with no credentials and four answered 403. They enforce access
control while carrying no static header, because MCP's own authorization flow
is OAuth: the client obtains a token at runtime and the config holds nothing.
Scanning 80 real registry servers produced 40 findings, most of them wrong.
That is the shape of a rule people switch off, and a rule people switch off
takes the rest of the tool with it.
A config file shows which credentials are configured; it cannot show what the
far end enforces. So the rule no longer asks that of a config. Under --live
it does not have to guess: if guardmcp connected with no credentials and the
server served its tool list, the endpoint is open — an observation, not a
supposition. Confidence is now high because it is evidence.
| Situation | Before | Now |
|---|---|---|
no --live |
finding | silent |
--live, refused (401/403) |
finding | silent — that is auth working |
--live, served us with no credentials |
finding | finding, high confidence |
Re-scanning the same 80 real servers: 40 findings → 0.
Verification done for this release
- Deletion mutation on all 27 rules: 27/27 killed. Every rule is genuinely
exercised; no rule's coverage number was hollow. - Always-fire mutation on all 9 detectors: 9/9 killed. Negative-case
coverage is real — an over-eager detector breaks the suite. - Real-world scan of 80 servers taken from the official registry, which is
what surfaced the MCPG-404 defect. Mutation testing could not have found it:
the rule worked exactly as designed, and the design was wrong.
v0.10.0 — remote rug-pull coverage, and a test that was passing for the wrong reason
[0.10.0] — 2026-09-07
Fixed
- The HTTP test fixture accepted only one session per process, answering
every connection after the first with "Server already initialized". A real
Streamable HTTP server builds a transport per session; this one shared a
single pair, so any test that connected second was talking to a corpse. - One integration test passed for the wrong reason because of it. It
asserted that a forwarded credential header never appears in output — which
is trivially true when the connection fails. It now asserts the connection
succeeded first. A green light for the wrong reason is worse than a red one.
Added
- Integration coverage for remote rug-pull detection: a real Streamable
HTTP server is pinned, restarted on the same port advertising a different
tool, and rescanned. MCPG-502 fires, MCPG-501 correctly stays quiet (nothing
on disk changed), and the payload that arrived with the swap is caught on its
own merits by MCPG-201.
Notes — why that test is the one that matters
A snapshot of the official MCP registry (2026-09-07, 3,945 latest-version
servers) shows 3,544 of them reachable only as remote HTTP endpoints and
just 567 shipping an installable package. For nine servers in ten there is
no version to pin, no lockfile and no reinstall step: the provider can change
what a tool does for every user at once, silently, while the config on disk
stays byte-identical.
The same snapshot shows 59 hostnames claimed by more than one registry
namespace, one of them by 213. Namespace verification proves who published
the listing, not who controls the running service.
Hashing what a server actually advertises is therefore not a nicety. For most
of this ecosystem it is the only control there is.
v0.9.0 — remote servers, and the request guardmcp will not make
[0.9.0] — 2026-09-06
Added
-
--livenow scans remote servers. Until now every HTTP server was
skipped with a warning, which meant the servers you trust least — third-party
hosted, the enterprise deployment model — were the ones guardmcp never
looked at. They are now dialled over Streamable HTTP, with the config's own
headers forwarded so authenticated servers can be introspected at all. -
A connect policy that refuses two cases by default (
--live-allow-unsafe
overrides, and loopback is exempt):- a private-network or cloud-metadata endpoint. MCPG-403 exists to report
that a config points there; connecting anyway would make guardmcp itself
issue that request against internal infrastructure. A scanner that can be
aimed at169.254.169.254by a config file is an SSRF primitive wearing a
security tool's name. - cleartext
http://carrying credential headers. MCPG-401 exists to
report that; connecting anyway would mean guardmcp transmits the user's own
token in the clear.
The governing principle, stated once so it can be held to: guardmcp never
performs the unsafe act it exists to warn about.--liveis the point where
a finding becomes an action this process takes. - a private-network or cloud-metadata endpoint. MCPG-403 exists to report
Changed
- The
--livetransparency notice now counts all servers, not only stdio ones.
v0.8.0 — inventory and init
[0.8.0] — 2026-09-06
Added
-
guardmcp inventory— lists the MCP servers configured on this machine
and, with--live, the tools, prompts and resources each one actually
advertises.scananswers "is any of this dangerous"; this answers "what is
any of this", which is the question that comes first and the one MCP09 is
really about — you cannot review a server you do not know you have.It never exits non-zero on content: an inventory reports, it does not judge,
so it is runnable by someone with no security question at all. Three server
states are kept distinct — answered, could-not-connect, and never-asked —
because "advertises no tools" and "we did not ask" are different facts and
blurring them is what makes an inventory useless.--format jsonfor
machine consumption. -
guardmcp init— writes a working GitHub Actions workflow. The gap
between "this would help us" and "this runs on every PR" is usually one file
nobody gets around to writing.It requests
security-events: write(without it the scan runs and the
findings silently never reach the Security tab, the usual way this setup
fails), uploads the SARIF withif: always()so results survive a failing
build, and deliberately does not enable--live— that spawns each
server's launch command, which a repository owner should opt into knowingly
rather than inherit from a generator.
Changed
- Live introspection now groups prompts and resources per server key, the way
tools already were. Two configs can declare the same server name, so grouping
by name alone would merge them.
v0.7.0 — x-mcp-header leaks, header injection, deceptive titles
[0.7.0] — 2026-09-06
Added
Three rules over parts of the tool declaration guardmcp was dropping on the
floor — title, and the x-mcp-header extension added in the 2026-07-28
specification.
- MCPG-801 — a credential parameter mirrored into an HTTP header.
x-mcp-headercopies a parameter's value into an outgoing
Mcp-Param-<name>header so intermediaries can route on it without parsing
the body. The specification warns about this in its own words: "Server
developers SHOULD NOT mark sensitive parameters (passwords, API keys,
tokens, PII) with x-mcp-header, as header values are visible to network
intermediaries." Nothing enforced that SHOULD NOT. Now something does. - MCPG-802 — an
x-mcp-headervalue the spec forbids. CR/LF in the header
name is HTTP header injection into the request the client is about to send;
also catches empty names, non-token characters, case-insensitive duplicates,
andnumber-typed parameters, which the spec excludes explicitly. A
conforming client MUST reject such a tool outright. - MCPG-803 — a display title that conceals the invoked name.
nameis
what the model calls;titleis what the client shows a human. Both working
as specified is what makes{ "name": "delete_all_files", "title": "View Documentation" }possible — the confirmation dialog says one thing, the call
says another.
Changed
ToolDefinitionnow carriestitle, and tool input properties carry
xMcpHeader. Both were being parsed and discarded.- The destructive-verb list moved to
src/detectors/destructive-verbs.ts, so
MCPG-303 and MCPG-803 cannot drift on what "destructive" means.
Notes
MCPG-801 excludes maxTokens, tokenCount, numTokens and tokenizer
before consulting its credential patterns. In this domain "token" usually
means an LLM token, and a scanner that reports maxTokens as a leaked secret
is one people switch off on the first run.