Skip to main content

Local-first security scanner for Model Context Protocol servers

Project description

mcpscan

mcpscan is an alpha, local-first security scanner for Model Context Protocol servers.

It connects to an MCP server over stdio or tested Streamable HTTP, enumerates exposed tools/resources/prompts/metadata, runs deterministic checks, and emits terminal, JSON, or Markdown findings before an AI agent trusts that server.

Install and run in one line

Run mcpscan without cloning, in a throwaway environment, with uvx (or pipx run):

uvx orisan-mcpscan scan-config ./mcp.json --yes

uvx fetches orisan-mcpscan from PyPI into an isolated environment and runs it in one step. The installed command is mcpscan (or orisan-mcpscan). The pipx equivalent is pipx run orisan-mcpscan --help.

60-Second Quickstart

From a cloned repo:

python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
mcpscan list-checks
mcpscan scan --command ".venv/bin/python tests/fixtures/benign_server.py"

Use python -m venv .venv instead if your system exposes Python 3 as python. Editable install may need network access to fetch build dependencies such as hatchling.

The benign fixture should return grade A with no findings.

Scan Your Client Configs

Use scan-config when a review starts from an MCP client config instead of a single server command:

mcpscan scan-config ./mcp.json --yes
mcpscan scan-config ./.mcp.json --yes --output json --out /tmp/mcpscan-config-report.json
mcpscan scan-config ./.mcp.json --yes --push-envelope --control-plane-url http://127.0.0.1:8787

Supported config shape:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp/mcpscan-safe-root"]
    },
    "remote-dev": {
      "url": "http://127.0.0.1:8000/mcp"
    }
  }
}

scan-config scans explicit config paths and can also discover known local MCP config locations for Claude Desktop, Claude Code, Cursor, and Windsurf. Stdio entries prompt before local execution unless --yes is provided. Remote URL entries do not prompt because they do not execute local commands.

Environment values from config files are passed to stdio servers but are redacted from all output. Reports show env names/counts only.

Use --push-envelope to POST the shared Orisan envelope directly to the control plane. The control-plane URL defaults to ORISAN_CONTROL_PLANE_URL or http://127.0.0.1:8787; pass --ingest-token or set ORISAN_INGEST_TOKEN when the ingest endpoint requires bearer auth.

Scan A Stdio MCP Server

Stdio scans launch the command you provide, perform the MCP handshake, enumerate the server, and then run checks over the exposed definitions.

mcpscan scan --command ".venv/bin/python tests/fixtures/malicious_server.py"

Important: stdio targets execute locally. Only scan commands you are willing to run on your machine.

Cold-start npx or uvx servers can take 30+ seconds on first run. The default timeout is 90 seconds; use --timeout if your target needs more or less time.

Scan A Streamable HTTP MCP Server

Streamable HTTP is the primary tested remote transport in this release. This local fixture starts an MCP server on 127.0.0.1:8000.

Terminal 1:

.venv/bin/python tests/fixtures/remote_streamable_server.py --port 8000

Terminal 2:

mcpscan scan http://127.0.0.1:8000/mcp --transport http

Remote scans do not contact external services except the MCP server URL you provide. SSE is wired through the official MCP SDK when available, but it is not integration-tested in this release.

Write JSON, Markdown, Or SARIF Reports

mcpscan scan --command ".venv/bin/python tests/fixtures/malicious_server.py" --output json --out /tmp/mcpscan-report.json
mcpscan scan --command ".venv/bin/python tests/fixtures/malicious_server.py" --output md --out /tmp/mcpscan-report.md
mcpscan scan --command ".venv/bin/python tests/fixtures/malicious_server.py" --output sarif --out /tmp/mcpscan-report.sarif

The malicious fixture intentionally returns findings, so these commands exit 1 when findings meet the default severity threshold.

For GitHub code scanning upload, see docs/CI.md.

Purpose Profiles

Reports include a deterministic purpose profile describing what the MCP server claims to be. You can provide it explicitly:

mcpscan scan --command ".venv/bin/python tests/fixtures/benign_server.py" --purpose-category filesystem
mcpscan scan --command ".venv/bin/python tests/fixtures/benign_server.py" --purpose "filesystem server for reading and writing files"

If no purpose flag is provided, mcpscan uses server metadata such as name and instructions when available. Ambiguous or empty text resolves to unknown.

Purpose profiles feed deterministic contextual verdicts and adjusted severities. The static taxonomy is documented in docs/PURPOSE_TAXONOMY.md.

Context-Aware Verdicts

mcpscan does not suppress findings. It labels each finding with a contextual verdict and keeps both original and adjusted severity when they differ.

Verdicts are deterministic:

  • expected_by_purpose: the capability is inherent to the declared purpose and the check is downgrade-eligible.
  • unexpected: the capability is outside the purpose category, but the declared text mentions it.
  • undeclared: the capability is outside the purpose category and not mentioned in declared text.
  • unadjudicated: no declared purpose was available.

Demo: expected filesystem access is reported for completeness:

mcpscan scan --command ".venv/bin/python tests/fixtures/purpose_filesystem_server.py" --purpose-category filesystem --no-color

Expected output includes:

INFO (was HIGH)  expected_by_purpose  MCP-010  read_file

Demo: hidden file access in a weather server is escalated:

mcpscan scan --command ".venv/bin/python tests/fixtures/purpose_weather_file_server.py" --purpose "weather server" --no-color

Expected output includes:

CRITICAL (was HIGH)  undeclared  MCP-010  read_file

Demo: declared URL fetching is still reported, but not escalated:

mcpscan scan --command ".venv/bin/python tests/fixtures/purpose_weather_fetch_server.py" --purpose "weather server that can fetch URLs" --no-color

Expected output includes:

HIGH  unexpected  MCP-010  fetch

What mcpscan Checks

ID Title Severity Capability OWASP MCP Status
MCP-001 Tool description prompt injection high prompt_anomaly MCP03 active
MCP-002 Tool definition drift high surface_drift MCP03 active with --baseline
MCP-010 Dangerous capability exposure high per finding MCP02 active
MCP-020 Secret exposure in metadata critical credential_access MCP01 active
MCP-021 Sensitive data or file exposure high data_exposure MCP10 active
MCP-030 Command or code injection surface high shell_exec/code_eval MCP05 active
MCP-040 Unauthenticated remote server high transport_security MCP07 active
MCP-041 Missing TLS high transport_security MCP07 active
MCP-050 Static known-name lookalike check using a curated seed list medium identity_spoof MCP09 active

Current coverage maps to OWASP MCP classes MCP01, MCP02, MCP03, MCP05, MCP07, MCP09, and MCP10. MCP04 supply chain analysis, MCP06 intent/flow issues, and MCP08 audit/telemetry gaps are out of scope for this alpha.

MCP-002 runs only when you provide a previous JSON report with --baseline or use scan-config --baseline-dir. MCP-050 is an offline heuristic that compares exposed server/tool names against a curated static seed list of common MCP server names. It does not monitor package registries and should not be treated as exhaustive ecosystem coverage.

Drift Detection

JSON reports include a surface block with hash-only snapshots of exposed tools, resources, and prompts. Description text is whitespace-normalized before hashing. Schemas are canonicalized with sorted JSON keys before hashing. The report does not add raw descriptions, schemas, or MCP responses to drift evidence.

Create a baseline report:

mcpscan scan --command ".venv/bin/python tests/fixtures/benign_server.py" --output json --out /tmp/mcpscan-baseline.json

Compare a later scan against that baseline:

mcpscan scan --command ".venv/bin/python tests/fixtures/benign_server_v2.py" --baseline /tmp/mcpscan-baseline.json

For explicit MCP config scanning, keep one baseline per server name:

mcpscan scan-config ./mcp.json --yes --baseline-dir /tmp/mcpscan-baselines

MCP-002 emits high-severity findings for added tools, removed tools, description hash changes, and schema hash changes. Evidence uses tool names and hash prefixes only.

What Findings Look Like

Each finding includes:

  • stable check ID
  • title
  • original severity
  • adjusted severity
  • contextual verdict
  • verdict reasoning
  • capability
  • OWASP MCP Top 10 ID
  • target
  • redacted evidence
  • remediation
  • reference
  • payload_stored=false

Example:

{
  "id": "MCP-030",
  "title": "Command or code injection surface",
  "severity": "high",
  "original_severity": "high",
  "adjusted_severity": "high",
  "contextual_verdict": "unadjudicated",
  "verdict_reasoning": "No declared purpose available; pass --purpose or --purpose-category to enable contextual adjudication.",
  "capability": "shell_exec",
  "owasp_mcp": "MCP05",
  "target": "run_command",
  "evidence": "Tool 'run_command' accepts unconstrained string parameter 'command' and appears to execute commands or code.",
  "remediation": "Constrain executable inputs with enums, patterns, length limits, allowlists, and server-side validation.",
  "reference": "OWASP MCP Top 10: Injection",
  "payload_stored": false
}

Privacy And Evidence Model

By default, mcpscan runs locally. It does not upload source code, prompts, secrets, raw MCP responses, or findings to Orisan or any external service.

Findings store safe, redacted evidence only. They identify the location and class of risk without storing full raw payloads. Every finding sets payload_stored=false.

Do not place secrets directly in --command, headers, or report output paths. Reports may include user-provided target strings such as the stdio command for traceability.

Exit Codes

Code Meaning
0 Scan completed and no finding met the severity threshold
1 Scan completed and at least one finding met the severity threshold
2 User input or CLI usage error
3 Connection or enumeration error
4 Internal scanner error

Use --severity-threshold low|medium|high|critical to control when findings return exit 1.

Supported Transports

Transport Status
stdio Tested with local fixture servers
Streamable HTTP Tested with a local fixture server
SSE Wired through the installed MCP SDK when available, but not integration-tested

Limitations And Non-Goals

mcpscan does not secure the model, enforce runtime policy, block agent actions, modify the target server, monitor registries, upload findings, or use LLM verdicts.

Dynamic probing, HTML reports, registry monitoring, GitHub Action packaging, SaaS dashboards, and runtime enforcement are not part of this alpha release.

scan-config only scans MCP config files that you explicitly pass or known local config paths it discovers. It does not scan arbitrary home-directory contents, source trees, browser profiles, or secrets stores.

Development And Verification

Run the local quality gates:

ruff format --check .
ruff check .
pytest
python -m mcpscan --help
python -m mcpscan scan-config --help
python -m mcpscan list-checks

Network-dependent stdio checks are excluded from default pytest. To run them manually:

pytest -m network

Release-readiness smoke checks:

mcpscan scan --command ".venv/bin/python tests/fixtures/benign_server.py"
mcpscan scan --command ".venv/bin/python tests/fixtures/malicious_server.py" --severity-threshold high
mcpscan scan --command ".venv/bin/python tests/fixtures/malicious_server.py" --output json --out /tmp/mcpscan-smoke.json || test $? -eq 1
mcpscan scan-config tests/fixtures/configs/mixed.json --yes --output json --out /tmp/mcpscan-config-smoke.json || test $? -eq 1

For Streamable HTTP, start the local fixture and scan it:

.venv/bin/python tests/fixtures/remote_streamable_server.py --port 8000
mcpscan scan http://127.0.0.1:8000/mcp --transport http

Field Validation

After the alpha release, use docs/VALIDATION_PROTOCOL.md for repeatable real-world MCP server testing and record sanitized notes in docs/VALIDATION_RESULTS.md. Validation notes must not include raw MCP responses, prompt payloads, source code, credentials, or secrets.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

orisan_mcpscan-0.1.0.tar.gz (93.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

orisan_mcpscan-0.1.0-py3-none-any.whl (55.1 kB view details)

Uploaded Python 3

File details

Details for the file orisan_mcpscan-0.1.0.tar.gz.

File metadata

  • Download URL: orisan_mcpscan-0.1.0.tar.gz
  • Upload date:
  • Size: 93.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for orisan_mcpscan-0.1.0.tar.gz
Algorithm Hash digest
SHA256 87440b0f09c6547bb96ca3e9ac4e15fb8db7dbe05f8aaa3d603df7eb6c0ed987
MD5 a54a33cef07e9e931af999406e7e5144
BLAKE2b-256 fd7177a082ad5b3e51be1ea17efce86576a8e6ab214c201a9a8fd8a48d6af49d

See more details on using hashes here.

File details

Details for the file orisan_mcpscan-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: orisan_mcpscan-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 55.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for orisan_mcpscan-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 289e765c99ffa323a7aa4e0a057352501fbaa51ae3c4d45d62a1cd5bc4f063d0
MD5 fe1c8c5f68a0a7508b1f6e89b9bbe603
BLAKE2b-256 a92607891bf3e366400a46b4581bbb748fa30a5994cb339819f5d837598f5b2b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page