Skip to main content

MCP ScopeCheck

PyPI version Python versions CI License: MIT

Inspect before you connect. MCP ScopeCheck is a pre-install static auditor that compares a Python MCP tool's declared contract with security-relevant behavior reachable from its source—without importing or running the server.

Quick start

Python 3.11 or newer is required; CI tests 3.11, 3.12, and 3.13. Install the dependency-free scanner from PyPI:

python -m pip install mcp-scopecheck

For an isolated CLI installation, pipx is also supported:

pipx install mcp-scopecheck

Audit a local Python file or directory:

mcp-scopecheck audit path/to/server.py

Emit SARIF 2.1.0 JSON for code-scanning integrations:

mcp-scopecheck audit path/to/server.py --format sarif > scopecheck.sarif

From this repository checkout, the bundled unsafe fixture provides a reproducible first audit:

mcp-scopecheck audit examples/unsafe_docs_server
$ mcp-scopecheck audit examples/unsafe_docs_server

[CRITICAL] MSC001 Agent-directed instruction in tool description
[CRITICAL] MSC105 Environment data reaches network egress
[HIGH] MSC101 Read-only claim conflicts with reachable behavior
[HIGH] MSC102 External network egress requires review
[HIGH] MSC103 Filesystem scope is not constrained
[HIGH] MSC104 Dangerous filesystem default

The paired hardened fixture retains its intended filesystem-read capability but returns Findings (0) and exits 0.

Exit codes are stable for local and CI use:

Code Meaning
0 Analysis complete within the documented model; no finding met the threshold
1 Analysis complete; one or more findings met the configured threshold
2 Analysis partial or failed, whether or not findings were also reported

Use --fail-on high (or low, medium, or critical) to set the exit-1 threshold.

Why this exists

An MCP tool description and its annotations are claims. They do not enforce a permission boundary. A tool named search_project_docs can still contain code that reads /, accesses environment values, starts a process, or sends data over the network.

ScopeCheck asks a narrow, evidence-backed question:

Does the tool's declared contract agree with behavior reachable from the tool's source?

That focus complements manifest scanners and runtime testing tools. It does not replace either.

Security invariant

ScopeCheck reads bounded source bytes, decodes them strictly using Python's PEP 263 encoding rules, and parses the resulting text with Python's ast module. Decode failures make the audit incomplete rather than substituting replacement characters. Target modules are never imported, decorators are never invoked, and MCP servers are never started. A regression test places a real top-level side effect in a fixture and proves it does not execute during an audit.

What v0.2 detects

Rule Severity What it means
MSC001 High/Critical Deterministic indicator families find agent-directed wording. Unambiguous directives (override, concealment, covert transfer, hidden-token markers, privileged-role impersonation) are Critical; ambiguous credential-handling and cross-call sequencing wording is High
MSC101 High/Critical readOnlyHint=true conflicts with justified state-changing behavior; process and dynamic code conflicts are Critical
MSC102 High Mandatory external-egress review for modeled network sinks: every modeled external and dynamic/computed destination is flagged, since neither prose nor a matching service hostname proves the destination; only local/loopback/private destinations are exempt. Specialized subtypes report explicit-denial contradictions and destination mismatches
MSC103 High A tool parameter reaches a filesystem operation in a path position without a recognized guard on that value; participation is decided by dataflow, not by parameter naming
MSC104 High A path-like or filesystem-reaching parameter defaults to the POSIX root or to an exact home root that code actually expands
MSC105 Critical Environment-derived data reaches a supported module or proven client-instance network sink in the same reachable function
MSC106 Critical Process or shell execution is reachable
MSC107 Critical eval or exec is reachable
MSC108 High openWorldHint=false conflicts with reachable external network interaction

Observed capabilities are reported separately from findings. Filesystem reads are not automatically vulnerabilities; the contract comparison determines whether the behavior is inconsistent or insufficiently constrained.

The 5-S report

Every audit is organized around:

  • Source — what local source was inspected.
  • Surface — which MCP tools were discovered.
  • Scope — parameters and declared annotations.
  • Side effects — filesystem, environment, network, process, and dynamic-code capabilities reachable from each tool.
  • Completeness — supported registrations and resolved or unresolved reachable local call edges.
  • Snapshot — a deterministic SHA-256 digest of the extracted contract and capabilities.

For a broader manual review, use the 5-S pre-install checklist.

Current boundaries

v0.2 intentionally supports:

  • Local Python files/directories
  • Module-level @mcp.tool, @mcp.tool(), and equivalent .tool decorators
  • Direct same-file module and nested sync/async helper-call reachability
  • Static relative and absolute in-root Python imports
  • Direct imported-function calls, import aliases, qualified local-module function calls, and one explicit __init__.py re-export hop
  • Cross-module filesystem, environment, network, process, and dynamic-code capability reachability with shortest source paths
  • Module-level and function-local import aliases with statement-order shadowing
  • Explicit module-level httpx, requests, and requests.api request functions, urllib.request.urlopen/urlretrieve, socket.create_connection, and request methods on flow-proven httpx.Client, httpx.AsyncClient, requests.Session, requests.sessions.Session, aiohttp.ClientSession, urllib3.PoolManager, urllib3.HTTPConnectionPool, http.client.HTTP(S)Connection, and socket.socket values
  • Qualified builtin, pathlib, os, and shutil filesystem operations with static open-mode/flag handling

It does not prove:

  • Callback, function-alias, lambda, partial, wrapper, higher-order, class, or instance-method call paths
  • Dynamic or wildcard import resolution, installed-package behavior, or more than one explicit package re-export hop
  • Runtime-only tool registration
  • httpx.stream or aiohttp.request context-manager factories, or egress via clients outside the recognized set (for example pycurl, smtplib, ftplib, websockets)
  • TypeScript/JavaScript behavior
  • Authorization correctness
  • Whether all observed data actually leaves the process, except the narrow same-function flow implemented by MSC105
  • Safety of a running MCP server

Unsupported reachable local behavior is listed in the completeness ledger and makes the audit partial. Ordinary calls proven to target the standard library or an external package do not by themselves make an audit partial.

MSC001 is a deterministic description check, not semantic or LLM analysis. Its families are pattern-based and are not equally precise. The credential-handling instruction and cross-call instruction families match a verb near a credential noun and sequencing wording respectively, both of which also appear in accurate self-descriptions - a secrets manager that says it "reads credentials from the configured keychain", or a tool documenting a prerequisite "before any request". Those families report High so they read as evidence for a human decision rather than as proof of poisoning. The concealment instruction family remains Critical and can still fire on a safety claim phrased as a prohibition (for example "never reveal the user password in logs"); treat a lone MSC001 finding as a prompt to read the description, not as a verdict. Precision and recall have not yet been measured against a large corpus of real MCP tool descriptions; the bundled corpus is a small regression fixture, not a benchmark. MSC102 compares the statically resolved egress destination against the description and never lets prose suppress a finding; an unresolved destination is always flagged. MSC103 seeds from every declared tool parameter and correlates simple path aliases and transformations with the guarded value; only path argument positions count, so a data argument beside a path is not treated as one. .resolve() alone is only normalization. A sink is withheld only when unresolved work could own its guard - wrapper/decorator indirection, or an unresolved call that receives the path value - so unrelated indirection elsewhere in the tool no longer hides a proven traversal. MSC105 follows direct environment reads, simple value assignments, and proven local HTTP-client bindings in lexical order within one function. Reassignment and deletion kill a client binding. ScopeCheck does not model complete Python control flow, general points-to relationships, or interprocedural environment taint. Cross-module environment-to-network taint is explicitly outside v0.2.

MSC101, MSC102, MSC106, MSC107, and MSC108 may consume unambiguous cross-module capability reachability. MSC103 consumes cross-module evidence only when supported argument lineage and guard state are proven; otherwise it is suppressed and incompleteness is reported. MSC105 remains same-function.

Audits fail with exit 2 when fixed safety limits are exceeded: 1 MB per file, 5,000 Python files, 20 MB total source, 500,000 AST nodes, 200 AST levels, 100 retained diagnostics, 2,000 participating local modules, 20,000 resolved local edges, 256 reachable functions per tool, 32 cross-module hops per tool, 1,000 capability paths per tool, 1,000 unresolved edges, or 1,000 potential registrations. A symlink supplied as the target is rejected; symlinked files and directories encountered inside a directory target are skipped without following them.

A clean report means complete within this bounded model and no threshold-matching finding; it is not proof of safe runtime behavior. See architecture and threat model and limitations.

For SARIF field semantics and a SHA-pinned GitHub code-scanning example, see SARIF output.

Develop

For an editable source installation, create a virtual environment and install the pinned development tools. The test suite uses only the Python standard library, and the installed scanner still has no runtime dependencies.

python3 -m venv .venv
.venv/bin/python -m pip install --disable-pip-version-check -r requirements-dev.txt
.venv/bin/python -m pip install --no-deps -e .
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src .venv/bin/python -m unittest discover -s tests -v
PYTHONPATH=src .venv/bin/python -m mcp_scopecheck audit examples/unsafe_docs_server
PYTHONPATH=src .venv/bin/python -m mcp_scopecheck audit examples/hardened_docs_server

Before a public release, activate that environment and run scripts/preflight.sh. It runs the same test, compile, Ruff, strict mypy, and build gates as CI; it additionally requires Gitleaks and fails closed if the scanner is unavailable.

For the source-evidence, unsafe/hardened, and exit-code demo:

scripts/demo.sh

Deliberate non-goals for v0.2

  • General interprocedural, field-sensitive, or points-to data flow
  • Cross-module taint
  • Dynamic execution or import of audited targets
  • Installed-package inspection
  • TypeScript/JavaScript analysis
  • LLM-assisted analysis

License

MIT

Download files

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

Source Distribution

mcp_scopecheck-0.2.1.tar.gz (112.0 kB view details)

Uploaded Source

Built Distribution

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

mcp_scopecheck-0.2.1-py3-none-any.whl (51.7 kB view details)

Uploaded Python 3

File details

Details for the file mcp_scopecheck-0.2.1.tar.gz.

File metadata

  • Download URL: mcp_scopecheck-0.2.1.tar.gz
  • Upload date:
  • Size: 112.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for mcp_scopecheck-0.2.1.tar.gz
Algorithm Hash digest
SHA256 d163a18d850803c943bed9b0e91485cf1de4b62bd4250d484b811ce342d64aab
MD5 2d230636d8ab5371c0e4ab00c917a64d
BLAKE2b-256 0f0e72d13e7a2e20cf4f1b66f26539395df3d12500ae87ff0491207f235d6ec8

See more details on using hashes here.

Provenance

The following attestation bundles were made for mcp_scopecheck-0.2.1.tar.gz:

Publisher: release.yml on iamudayrathore/mcp-scopecheck

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mcp_scopecheck-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: mcp_scopecheck-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 51.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for mcp_scopecheck-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 de2117790496b8e099b44628c9a991e955ba415273b86ef48c516b67ceb8330f
MD5 30abcdc06a75515e6c8bd100fec637d4
BLAKE2b-256 8a2a57e84d91c78fa3cef2d9a337f4434de609b002fa1e18fd88f70d1845a213

See more details on using hashes here.

Provenance

The following attestation bundles were made for mcp_scopecheck-0.2.1-py3-none-any.whl:

Publisher: release.yml on iamudayrathore/mcp-scopecheck

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.3

2 files

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.2

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page