script-decoder-mcp
# Script Decoder MCP
A defensive, static-analysis MCP server that explains what a script does — in plain
English and with technical evidence — **without ever executing it**.
Paste a Python, PowerShell, shell, JavaScript, batch, or VBScript script (or a single
command line) and get back: a behavior breakdown (network, file, process, persistence,
credential-access, discovery, defense-evasion, execution), safely decoded obfuscated
content, extracted indicators of compromise, a transparent risk score, and a contextual
MITRE ATT&CK mapping — all with source-line evidence and explicit observed/inferred/
possible classifications.
> **Defensive-use statement**: this project performs static analysis only. It never
> executes, imports, compiles-for-execution, or otherwise runs submitted content. It
> never sends submitted scripts or decoded content to any third-party service. It is
> intended for SOC analysts, incident responders, and anyone who needs to understand an
> unfamiliar or suspicious script before deciding what to do with it — not to enable
> offensive use.
## Features
- **Five MCP tools**: `analyze_script`, `decode_blob`, `extract_indicators`,
`explain_command`, `compare_scripts`.
- **A local browser GUI** (see [Browser GUI](#browser-gui)) for the same five analyses,
with no MCP client required.
- **Six language analyzers**: Python (AST-based), PowerShell, shell (bash/sh/zsh),
JavaScript, batch, and VBScript (rule-based).
- **Bounded recursive decoding**: Base64, Base32, hex, URL encoding, Unicode escapes,
HTML entities, gzip/zlib/bz2, ROT13, reversed strings, PowerShell
`-EncodedCommand`, integer/char-code arrays, simple string concatenation, and
explicit-key XOR — chained safely up to a configurable depth, with decompression-bomb
protection.
- **IOC extraction**: URLs, domains, IPv4/IPv6, emails, file paths, registry keys,
hashes, mutex-like strings, user agents, named pipes, scheduled-task/service names, and
common cloud resource identifiers — including defanged-notation recognition
(`hxxp`, `example[.]com`, `10[.]0[.]0[.]1`).
- **Transparent, documented risk scoring** (0-100) with contributing factors listed in
every response — never a black box, never a malware verdict.
- **Conservative, local, offline MITRE ATT&CK mapping.**
- **Prompt-injection safe**: all submitted and decoded content is treated as inert data,
never as instructions, and is explicitly labeled as untrusted in evidence excerpts.
- **Optional, local, disabled-by-default CyberChef-style adapter** for a small set of
additional decode operations, with no network access and an operation allowlist.
- **No outbound network access is required or performed during analysis.**
## Architecture
See [`docs/architecture.md`](docs/architecture.md) for the full breakdown. In short:
```
MCP client (e.g. Claude Desktop)
│ stdio (default) or localhost HTTP
▼
server.py (5 MCP tools)
│
├─ analyzers/ language detection + per-language static analysis (AST for
│ Python, regex/rule-based for the rest)
├─ decoding/ bounded recursive decoding engine, native decoders, optional
│ local CyberChef-style adapter
├─ extraction/ IOC extraction
├─ classification/ risk scoring + ATT&CK mapping
├─ reporting/ assembles the final structured response + summaries
└─ security/ sanitization and trust-boundary helpers
```
Nothing in this pipeline executes, imports, or evaluates submitted content. See
[`docs/threat-model.md`](docs/threat-model.md) for the full threat model and
[`SECURITY.md`](SECURITY.md) for the security policy.
## Installation
Requires Python 3.12+.
```bash
python -m venv .venv
# Windows: .venv\Scripts\activate macOS/Linux: source .venv/bin/activate
pip install -e ".[dev]"
```
Optional: Node.js 18+ if you want to enable the local CyberChef-style adapter (disabled
by default; not required for any core functionality).
## Local development
```bash
pip install -e ".[dev]"
python -m pytest
ruff check .
ruff format --check .
mypy src
```
## Running the server
Default transport is stdio, which is what MCP clients like Claude Desktop expect:
```bash
python -m script_decoder_mcp
# or, after `pip install -e .`:
script-decoder-mcp
```
Set `SDMCP_TRANSPORT=http` (see [`.env.example`](.env.example)) to run over localhost
HTTP instead; it binds `127.0.0.1` by default and is never exposed remotely without
explicit additional configuration, which this project does not provide.
## Browser GUI
Prefer a browser over an MCP client? There's a local web GUI that calls the exact same
analyzer/decoder code as the MCP tools -- it's a thin FastAPI wrapper around
`reporting/formatter.py`, not a separate implementation, so results never drift from the
MCP server.
```bash
pip install -e ".[web]"
python -m script_decoder_mcp.web.app
# or: make web
```
Then open **http://127.0.0.1:8787**. It has one tab per tool (Analyze Script, Decode
Blob, Extract Indicators, Explain Command, Compare Scripts), plus "Load good/bad example"
buttons that load [`examples/good_script_benign.py`](examples/good_script_benign.py) and
[`examples/bad_script_suspicious.py`](examples/bad_script_suspicious.py) for a quick
before/after. Like the MCP server, it binds to `127.0.0.1` only by default
(`SDMCP_WEB_HOST`/`SDMCP_WEB_PORT` to change), has no authentication, is not meant to be
exposed beyond localhost, and never sends anything you paste anywhere beyond your own
machine.
## Claude Desktop / MCP client configuration
Add to your MCP client config (see
[`examples/claude-desktop-config.example.json`](examples/claude-desktop-config.example.json)
and [`examples/mcp-client-config.example.json`](examples/mcp-client-config.example.json)):
```json
{
"mcpServers": {
"script-decoder": {
"command": "script-decoder-mcp",
"args": []
}
}
}
```
If you haven't installed the console script, point `command` at the venv's Python and
`args` at `["-m", "script_decoder_mcp"]` instead.
## Tools
### `analyze_script`
Statically analyzes a full script. Input: `content` (required), `language` (default
`auto`), `filename`, `decode_embedded_content` (default `true`),
`include_line_references` (default `true`), `analysis_depth` (`quick`/`standard`/`deep`,
default `standard`), `map_to_attack` (default `true`).
Returns a `schema_version`-stamped response with `summary`, `plain_language_summary`,
`analyst_summary`, `verdict`, `risk_score`, `risk_factors`, `behaviors` (each with
`category`, `title`, `description`, `evidence`, `source_lines`, `confidence`,
`classification` — `observed`/`inferred`/`possible` — and `severity`),
`decoded_artifacts`, `indicators`, `imports`/`functions`/`commands`,
`suspicious_constructs`, `attack_techniques`, `limitations`, `errors`, and
`analysis_metadata`. `execution_performed` is always `false`.
### `decode_blob`
Decodes a single encoded/obfuscated value. Input: `content` (required),
`encoding_hint`, `max_depth`, `use_cyberchef` (default `false`). Returns the detected
encodings, every attempted decode chain, the successful chain (if any), decoded
text/size/hashes, any indicators found in the decoded text, and warnings. Never claims to
decode arbitrary encryption and never brute-forces a password or key.
### `extract_indicators`
Extracts IOCs from arbitrary text or source. Input: `content` (required). Returns a
dictionary of indicator categories, each entry carrying `value`, `normalized_value`,
`type`, `source_lines`, `context`, `confidence`, and `defanged_value`.
### `explain_command`
Explains a single command line. Input: `command` (required), `shell`
(`auto`/`cmd`/`powershell`/`bash`/`zsh`/`python`, default `auto`), `include_tokens`.
Returns executable, arguments, pipelines, redirections, environment changes, possible
side effects, and suspicious features — never runs the command.
### `compare_scripts`
Diffs two versions of a script. Input: `original_content`, `new_content`, `language`
(default `auto`), `decode_embedded_content` (default `true`). Returns added/removed
behaviors, changed indicators, newly decoded artifacts, the risk-score delta, and
line-level change summary.
## Example requests
See [`examples/sample_requests.md`](examples/sample_requests.md).
## CyberChef integration
Disabled by default (`SDMCP_CYBERCHEF_ENABLED=false`). When enabled, `decode_blob`
(with `use_cyberchef: true`) can fall back to a small local Node.js worker
(`cyberchef-worker/index.mjs`) for a fixed, allowlisted set of operations: `From Base64`,
`From Base32`, `From Hex`, `URL Decode`, `HTML Entity Decode`, `Gunzip`, `Zlib Inflate`,
`ROT13`, `Reverse`.
**Deviation from the original design**: rather than depending on the `cyberchef` npm
package (a large dependency tree), the worker reimplements these named operations using
only Node.js built-ins, in a single auditable file. It performs no network access, no
filesystem access beyond stdin, and no dynamic code loading. See
[`cyberchef-worker/README.md`](cyberchef-worker/README.md) for details and how to extend
it. It is never called against a public CyberChef instance, and native Python decoders
remain fully functional with or without it.
## Docker
```bash
docker compose build
docker compose run --rm script-decoder-mcp
```
The image runs as a non-root user, uses a multistage build, and does not require network
access at runtime. See [`Dockerfile`](Dockerfile) and
[`docker-compose.yml`](docker-compose.yml).
## Security model
- Static analysis only: no `eval`/`exec`/`compile`-for-execution of submitted content, no
`os.system`/`shell=True` on submitted content, no importing of submitted scripts, no
outbound network requests, no DNS resolution, no reputation-service calls.
- Every resource limit (input size, decode output size, decompression size, recursion
depth, candidate branches, analysis duration, indicators per category, evidence excerpt
length) is documented in [`.env.example`](.env.example) and enforced in
[`src/script_decoder_mcp/limits.py`](src/script_decoder_mcp/limits.py); a threshold
being hit is always reported as a limitation, never silently ignored.
- All submitted and decoded content is treated as untrusted data, never as instructions —
see [`src/script_decoder_mcp/security/`](src/script_decoder_mcp/security/).
- No secrets are read, stored, or logged. Logging is JSON-structured and never emits full
submitted content (see
[`src/script_decoder_mcp/logging_config.py`](src/script_decoder_mcp/logging_config.py)).
- Full details: [`docs/threat-model.md`](docs/threat-model.md), [`SECURITY.md`](SECURITY.md).
## Limitations
- Static analysis cannot always determine runtime behavior. What a downloaded payload or
a dynamically constructed command will actually do when run is not knowable from source
alone, and responses/results of any network call are never fetched.
- Dynamically constructed values (string building, indirect calls) lower analysis
confidence and are flagged as `inferred` or `possible`, not `observed`.
- Risk scoring is a heuristic, not a malware verdict; ATT&CK mapping is contextual
evidence, not proof of malicious intent.
- Batch and VBScript analysis is rule-based (no safe parser exists for either), so it is
less thorough than the Python AST analyzer.
- Encoding/administrative-tool use alone is never enough to mark something malicious.
## Supported languages
Python, PowerShell, Bash/sh/zsh, JavaScript, Batch, VBScript, and single command lines
(`auto`, or specify explicitly).
## Extending
See [`docs/adding-an-analyzer.md`](docs/adding-an-analyzer.md) and
[`docs/adding-a-decoder.md`](docs/adding-a-decoder.md).
## Testing
```bash
python -m pytest -v
```
Test fixtures under `tests/fixtures/` are inert text — no functional malware,
credential-stealing code, persistence payloads, destructive scripts, or live malicious
URLs. They use reserved example domains/IPs (`example.com`, `example.invalid`,
`192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`).
Two ready-to-use manual test scripts are also included:
[`examples/good_script_benign.py`](examples/good_script_benign.py) (should score low/no
risk) and [`examples/bad_script_suspicious.py`](examples/bad_script_suspicious.py) (an
inert, non-functional script that *exhibits* several suspicious static patterns —
encoded payload, `exec`, outbound request construction — and should score meaningfully
higher). Neither script does anything if actually run; `bad_script_suspicious.py` is
deliberately written so it does not function even if a reader ignored this warning and
executed it.
## Troubleshooting
- **"No dedicated analyzer is available for language..."** — the language could not be
detected or was not one of the six supported languages; indicator extraction and
decoding still run.
- **CyberChef warnings in `decode_blob`** — expected when
`SDMCP_CYBERCHEF_ENABLED=false` (the default) and `use_cyberchef: true` was requested
anyway; native decoders already cover the same operations.
- **`errors` populated in `analyze_script`** — usually a Python `SyntaxError`; the
response still includes a best-effort text-based fallback analysis.
## Privacy
No submitted script, decoded artifact, or any derived content is ever sent to a
third-party service. The optional CyberChef worker runs locally with no network access.
No telemetry is collected by default.
## Deployment
Intended for local, single-user use via stdio (the MCP-recommended local transport) or
containerized via Docker for isolation. The optional HTTP transport binds `127.0.0.1` by
default; exposing it beyond localhost is out of scope for this project and would require
adding authentication, which is not currently implemented — see
[`docs/threat-model.md`](docs/threat-model.md) for the residual risk this leaves if you
choose to do so anyway.
TDQS
Scored across 5 tools
Each tool has a clearly distinct purpose: analyzing a full script, decoding a single blob, extracting indicators, explaining a command line, and comparing script versions. Although analyze_script returns decoded artifacts and indicators, the dedicated tools serve standalone use cases without ambiguity.
All tool names follow a consistent verb_noun pattern: analyze_script, decode_blob, extract_indicators, explain_command, compare_scripts. The naming convention is uniform and predictable.
Five tools is well-scoped for a static script decoding and analysis server. Each tool covers a distinct capability without redundancy or bloat.
The tool surface covers the full static analysis workflow: analyzing scripts, decoding blobs, extracting indicators, explaining commands, and comparing versions. There are no obvious missing operations for the stated purpose of safely decoding and analyzing scripts without execution.