mcp-guard is a static, offline security scanner/linter for MCP (Model Context Protocol) servers — a CLI and GitHub Action that scans a server's source code (and, optionally, its tool manifest) for the vulnerability classes specific to MCP: tool poisoning, prompt-injection surfaces, hardcoded secrets, command injection, path traversal, and excessive permissions. It never executes the scanned code and never contacts the scanned server — it only reads files from disk and pattern-matches.
MCP servers exploded in number, and security has not kept up. A malicious or careless MCP server can:
- embed model-directed instructions inside a tool's own description ("tool poisoning" — e.g. "ignore previous instructions", "do not tell the user", hidden Unicode payloads) that hijack the agent the moment the tool list is loaded, before any tool is even called;
- accept unvalidated arguments that flow straight into
eval, a shell command, or the filesystem; - ship hardcoded API keys/tokens, leak the full process environment, or talk to the network over plain HTTP.
Most teams have no way to audit this today. mcp-guard is shift-left, CI-native tooling for exactly that: run it in CI against every MCP server you build or depend on, and fail the build on real findings.
npm install --save-dev @m8t-jacob/mcp-guardOr run it directly without installing:
npx @m8t-jacob/mcp-guard scan .mcp-guard scan <path> [options]
Options:
--tools <file> Path to a tools/list-shaped JSON manifest to scan alongside the source.
--format <fmt> Output format: text (default), sarif, or json.
--fail-on <level> Minimum severity that causes a non-zero exit code: error (default), warning, info, or none.
--help, -h Show help.Examples:
# Human-readable report, exits non-zero if any "error"-level finding exists.
mcp-guard scan ./my-mcp-server
# Also scan a tools/list manifest dumped from the running server.
mcp-guard scan ./my-mcp-server --tools ./tools.json
# SARIF for GitHub Code Scanning; never fail the step here (upload first,
# gate the build in a separate step — see the GitHub Action example below).
mcp-guard scan ./my-mcp-server --format sarif --fail-on none > results.sarif
# Machine-readable JSON for custom tooling/dashboards.
mcp-guard scan ./my-mcp-server --format jsonZero-config: point it at a directory and it runs every built-in rule, no
configuration file required. A clean project produces No findings. and
exit code 0.
| Rule ID | Severity | Detects |
|---|---|---|
hardcoded-secret |
error | Known token formats (sk-..., ghp_..., AKIA...) and long literal values assigned to key/token/secret/password-named variables, instead of read from the environment. |
eval-usage |
error | eval(...) and new Function(...) — dynamic code execution. |
command-injection |
error | child_process exec/execSync/spawn calls built from template-literal interpolation or string concatenation. |
tool-poisoning |
error | Tool descriptions embedding model-directed instructions ("ignore previous instructions", "do not tell the user", fake <IMPORTANT> tags, exfiltration instructions). |
hidden-unicode |
error | Zero-width characters, bidi overrides/isolates, and Unicode tag characters hidden in tool descriptions — a classic tool-poisoning delivery mechanism. |
injection-surface |
warning | Tool descriptions built from interpolated/dynamic data, or that instruct the model to execute/interpret argument content as commands. |
unvalidated-input |
warning | Tools with no inputSchema, an overly permissive schema (no properties), or Zod z.any(). |
broad-filesystem |
warning | Filesystem calls whose path comes from tool-argument-controlled data with no visible traversal check. |
unpinned-exec |
warning | Unpinned npx invocations and curl/wget | sh patterns. |
overbroad-env |
warning | Passing/logging the entire process.env object instead of an explicit allowlist. |
insecure-http |
info | Plain http:// URL literals instead of https://. |
Every rule has a positive (bad) and negative (clean) fixture under
tests/fixtures, each covered by a test asserting the
rule fires on the bad sample and produces zero findings on the clean one —
see Limitations for what that guarantee does and doesn't
cover.
Pass a JSON dump of an MCP server's tools/list response (or a bare array
of tool objects) to also run the description/schema-focused rules
(tool-poisoning, hidden-unicode, injection-surface,
unvalidated-input) against the manifest, in addition to whatever
description:/registerTool(...) calls are found in source:
{
"tools": [
{
"name": "read_file",
"description": "Reads a file from the workspace.",
"inputSchema": { "type": "object", "properties": { "path": { "type": "string" } } }
}
]
}See examples/tools.json for a sample containing
one clean and one poisoned tool definition.
This repo is itself a composite GitHub Action. Add a workflow like:
name: mcp-guard
on: [push, pull_request]
permissions:
contents: read
security-events: write
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: M8T-Jacob/mcp-guard@v0
with:
path: '.'
tools: 'tools.json' # optional
fail-on: 'error' # error (default) | warning | info | none
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: mcp-guard-results.sarifThe action runs mcp-guard twice: once producing SARIF (--fail-on none, so
the upload step always has a report to publish) and once in the
human-readable format enforcing your chosen --fail-on threshold (so the
job actually fails the build on real findings). See
examples/ci-workflow.yml for a full
worked example, including the equivalent raw-npx version if you'd rather
not depend on the composite action.
import { scanProject, formatText, formatSarif } from '@m8t-jacob/mcp-guard';
const result = scanProject('./my-mcp-server', { toolsPath: './tools.json' });
console.log(formatText(result));
console.log(formatSarif(result));
if (result.findings.some((f) => f.severity === 'error')) {
process.exitCode = 1;
}rules (the array of every built-in Rule) is also exported, e.g. to run
a subset via scanProject(dir, { rules: [...] }) or to build your own
reporter.
mcp-guard's rules are text/regex based, not a full static-analysis
engine with an AST and data-flow graph (an optional AST pass via
typescript is a natural follow-up — see
GOOD_FIRST_ISSUES.md). That is a deliberate
trade-off: it's fast, has zero parsing dependencies, and catches the
common, lazy forms of each vulnerability class. Be aware of the shape of
that trade-off:
- False negatives: a pattern that's been refactored, obfuscated, split
across variables, or wrapped across multiple lines in a way a given rule
doesn't account for will not be flagged. A regex is not a substitute for
taint tracking —
unvalidated-inputandbroad-filesystemin particular only see what's written at the call site, not how a value actually flows through the program. - False positives: some rules can flag benign code that merely looks
like the pattern — for example,
overbroad-envflags any bareprocess.envmention, including inside a comment (mentioningprocess.envin a comment can even suppress a genuinehardcoded-secretfinding on the same line — see that rule's module docs). Every rule's source file documents its specific known gaps. - No sandboxing, no dynamic analysis: mcp-guard never runs the scanned code. It cannot catch anything that only manifests at runtime (e.g. a secret assembled by string concatenation across a function).
Treat mcp-guard as a fast first line of defense in CI, not a substitute for manual security review of an MCP server before production use.
- Rule interface:
Rule { id, name, severity, description, check(context) }returningFinding[]; seesrc/types.ts. Adding a rule is adding one module undersrc/rules/plus a bad/clean fixture pair. scanProject(dir, options)collects.ts/.js-family source files (skippingnode_modules,dist,.git, etc.), optionally loads a tools manifest, runs every rule, and aggregatesFinding[].- Three reporters:
formatText(colorized console report),formatSarif(SARIF 2.1.0, forgithub/codeql-action/upload-sarif),formatJson(plain JSON for custom tooling). - Strict TypeScript, dual ESM + CJS builds with
.d.ts, zero runtime dependencies.
@m8t-jacob/mcp-guard to statyczny skaner bezpieczeństwa dla serwerów
MCP (CLI + GitHub Action): analizuje kod źródłowy serwera i jego manifest
narzędzi w poszukiwaniu podatności specyficznych dla MCP — zatruwania
narzędzi (tool poisoning), powierzchni prompt-injection, zaszytych
sekretów, command injection, path traversal i nadmiernych uprawnień.
Działa w pełni statycznie i offline (nigdy nie wykonuje skanowanego kodu),
generuje raport SARIF 2.1.0 dla zakładki Security na GitHubie. Reguły
oparte są o analizę tekstu/regex — to świadomy kompromis (szybkość, zero
zależności) opisany szczerze w sekcji Limitations, nie substytut ręcznego
przeglądu bezpieczeństwa.
Contributions are welcome! See CONTRIBUTING.md for the
development workflow and GOOD_FIRST_ISSUES.md for
ideas if you're looking for a place to start. This project follows the
Contributor Covenant.
MIT © 2026 Jakub Jagiełło