Skip to content

Releases: BerkantACUN/guardmcp

v0.15.1 — Marketplace-valid action description

Choose a tag to compare

@BerkantACUN BerkantACUN released this 09 Sep 13:52

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

Choose a tag to compare

@BerkantACUN BerkantACUN released this 09 Sep 12:37

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 after

That 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 hU+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

Choose a tag to compare

@BerkantACUN BerkantACUN released this 08 Sep 18:04
ce31587

[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)

Choose a tag to compare

@BerkantACUN BerkantACUN released this 07 Sep 21:07
776b55f

[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/list and 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

Choose a tag to compare

@BerkantACUN BerkantACUN released this 07 Sep 20:34
d18e2a0

[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

Choose a tag to compare

@BerkantACUN BerkantACUN released this 07 Sep 20:00
7419826

[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

Choose a tag to compare

@BerkantACUN BerkantACUN released this 07 Sep 19:19
3567e07

[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

Choose a tag to compare

@BerkantACUN BerkantACUN released this 07 Sep 18:58
a29ccda

[0.9.0] — 2026-09-06

Added

  • --live now 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 at 169.254.169.254 by 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.
    --live is the point where
    a finding becomes an action this process takes.

Changed

  • The --live transparency notice now counts all servers, not only stdio ones.

v0.8.0 — inventory and init

Choose a tag to compare

@BerkantACUN BerkantACUN released this 07 Sep 17:27
34f39a2

[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. scan answers "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 json for
    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 with if: 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

Choose a tag to compare

@BerkantACUN BerkantACUN released this 06 Sep 19:58
366ef5c

[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-header copies 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-header value 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,
    and number-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. name is
    what the model calls; title is 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

  • ToolDefinition now carries title, 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.