Metadata-Version: 2.4
Name: mcp-scopecheck
Version: 0.1.2
Summary: Static claim-versus-capability analysis for Python MCP servers
Author: Uday Rathore
License-Expression: MIT
Project-URL: Issues, https://github.com/iamudayrathore/mcp-scopecheck/issues
Project-URL: Repository, https://github.com/iamudayrathore/mcp-scopecheck
Keywords: mcp,security,static-analysis,ai-agents,appsec
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# MCP ScopeCheck

[![PyPI version](https://img.shields.io/pypi/v/mcp-scopecheck.svg)](https://pypi.org/project/mcp-scopecheck/)
[![Python versions](https://img.shields.io/pypi/pyversions/mcp-scopecheck.svg)](https://pypi.org/project/mcp-scopecheck/)
[![CI](https://github.com/iamudayrathore/mcp-scopecheck/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/iamudayrathore/mcp-scopecheck/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/iamudayrathore/mcp-scopecheck/blob/main/LICENSE)

**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:

```bash
python -m pip install mcp-scopecheck
```

For an isolated CLI installation, `pipx` is also supported:

```bash
pipx install mcp-scopecheck
```

Audit a local Python file or directory:

```bash
mcp-scopecheck audit path/to/server.py
```

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

```bash
mcp-scopecheck audit examples/unsafe_docs_server
```

```console
$ 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 Network egress is not disclosed
[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` | No finding met the configured threshold |
| `1` | One or more findings met the configured threshold |
| `2` | Invalid input, no supported tools, diagnostics, or another audit error |

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.1 detects

| Rule | Severity | What it means |
| --- | --- | --- |
| `MSC001` | Critical | Deterministic indicator families find agent-directed override, concealment, covert transfer, or related high-risk wording |
| `MSC101` | High/Critical | `readOnlyHint=true` conflicts with justified state-changing behavior; process and dynamic code conflicts are Critical |
| `MSC102` | High | Reachable egress lacks a clear external-interaction disclosure, contradicts an explicit denial, or conflicts with a supported static destination check |
| `MSC103` | High | A correlated path-like input reaches a filesystem operation without a recognized guard on that value |
| `MSC104` | High | A path/root 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.
- **Snapshot** — a deterministic SHA-256 digest of the extracted contract and capabilities.

For a broader manual review, use the [5-S pre-install checklist](docs/review-checklist.md).

## Current boundaries

v0.1 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
- 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`,
  and `requests.sessions.Session` values
- Qualified builtin, `pathlib`, `os`, and `shutil` filesystem operations with
  static open-mode/flag handling

It does **not** yet prove:

- Cross-module, callback, function-alias, lambda, or class-method call paths
- Runtime-only tool registration
- `httpx.stream` or `aiohttp.request` context-manager factories, `http.client`
  connection methods, or raw-socket instance traffic
- 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

`MSC001` and `MSC102` are deterministic description checks, not semantic or LLM
analysis. `MSC103` correlates simple path aliases and transformations with the
guarded value; `.resolve()` alone is only normalization. `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.

Audits fail incomplete 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, or 100 retained diagnostics. 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 is not proof of safe runtime behavior. See [architecture and threat model](docs/architecture.md).

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

```bash
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:

```bash
scripts/demo.sh
```

## Roadmap

- Cross-module call graph and argument-aware data flow
- TypeScript parser backed by a real syntax tree
- JSON/SARIF output and stable rule schema
- Snapshot comparison for tool-definition drift
- Optional semantic description analysis with an explicit privacy boundary
- Reproducible benchmark corpus with measured precision and recall

## License

MIT
