Skip to content

M8T-Jacob/mcp-guard

Repository files navigation

@m8t-jacob/mcp-guard

CI npm version npm downloads license: MIT

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.

The problem

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.

Install

npm install --save-dev @m8t-jacob/mcp-guard

Or run it directly without installing:

npx @m8t-jacob/mcp-guard scan .

CLI usage

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 json

Zero-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.

Rules

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.

Tool manifest (--tools)

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.

GitHub Action

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.sarif

The 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.

Programmatic use

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.

Limitations

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-input and broad-filesystem in 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-env flags any bare process.env mention, including inside a comment (mentioning process.env in a comment can even suppress a genuine hardcoded-secret finding 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.

Design notes

  • Rule interface: Rule { id, name, severity, description, check(context) } returning Finding[]; see src/types.ts. Adding a rule is adding one module under src/rules/ plus a bad/clean fixture pair.
  • scanProject(dir, options) collects .ts/.js-family source files (skipping node_modules, dist, .git, etc.), optionally loads a tools manifest, runs every rule, and aggregates Finding[].
  • Three reporters: formatText (colorized console report), formatSarif (SARIF 2.1.0, for github/codeql-action/upload-sarif), formatJson (plain JSON for custom tooling).
  • Strict TypeScript, dual ESM + CJS builds with .d.ts, zero runtime dependencies.

🇵🇱 Po polsku

@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.

Contributing

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.

License

MIT © 2026 Jakub Jagiełło

About

Static security scanner for MCP servers: tool poisoning, prompt injection, secrets, command injection. CLI + GitHub Action, SARIF output.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors