|
| 1 | +# usvm-mcp |
| 2 | + |
| 3 | +An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that exposes the USVM |
| 4 | +symbolic execution engine for TypeScript (`usvm-ts`) as a set of tools for LLM agents |
| 5 | +(Claude Code, Claude Desktop, MCP Inspector, or any other MCP client). |
| 6 | + |
| 7 | +The goal is **hybrid analysis of dynamic languages**: the LLM reads and writes code, |
| 8 | +while the symbolic machine provides ground-truth facts about it — concrete inputs per |
| 9 | +execution path, reachability witnesses, crashing inputs, and counterexamples to |
| 10 | +hypotheses formulated by the LLM. |
| 11 | + |
| 12 | +## Prerequisites |
| 13 | + |
| 14 | +1. **JDK 11+** — the MCP Kotlin SDK requires JVM 11 (the rest of USVM targets 1.8; |
| 15 | + this leaf module overrides the target). |
| 16 | +2. **Node.js** — used by ArkAnalyzer to convert TypeScript into ETS IR. |
| 17 | +3. **ArkAnalyzer** — a built checkout, pointed to by the `ARKANALYZER_DIR` environment variable: |
| 18 | + |
| 19 | + ```bash |
| 20 | + git clone https://gitee.com/openharmony-sig/arkanalyzer |
| 21 | + cd arkanalyzer && npm install && npm run build |
| 22 | + export ARKANALYZER_DIR=$(pwd) |
| 23 | + ``` |
| 24 | + |
| 25 | +## Build and run |
| 26 | + |
| 27 | +```bash |
| 28 | +./gradlew :usvm-mcp:installDist |
| 29 | +``` |
| 30 | + |
| 31 | +This produces a launcher at `usvm-mcp/build/install/usvm-mcp/bin/usvm-mcp`. |
| 32 | +The server speaks MCP over **stdio**: stdout carries JSON-RPC, all logging goes to stderr. |
| 33 | + |
| 34 | +### Connect from Claude Code |
| 35 | + |
| 36 | +```bash |
| 37 | +claude mcp add usvm \ |
| 38 | + -e ARKANALYZER_DIR=/path/to/arkanalyzer \ |
| 39 | + -- /path/to/usvm/usvm-mcp/build/install/usvm-mcp/bin/usvm-mcp |
| 40 | +``` |
| 41 | + |
| 42 | +### Connect from MCP Inspector (debugging) |
| 43 | + |
| 44 | +```bash |
| 45 | +ARKANALYZER_DIR=/path/to/arkanalyzer \ |
| 46 | + npx @modelcontextprotocol/inspector usvm-mcp/build/install/usvm-mcp/bin/usvm-mcp |
| 47 | +``` |
| 48 | + |
| 49 | +### Run from Gradle (debugging) |
| 50 | + |
| 51 | +```bash |
| 52 | +./gradlew :usvm-mcp:run |
| 53 | +``` |
| 54 | + |
| 55 | +## Tools |
| 56 | + |
| 57 | +All tools take a `file` argument — a path to a single `.ts` file. Converted scenes are |
| 58 | +cached in memory (keyed by path and mtime), so repeated calls on the same file are fast. |
| 59 | +Analysis tools also accept `timeoutMs` (default 30000, max 300000). |
| 60 | + |
| 61 | +| Tool | What it does | |
| 62 | +|------|--------------| |
| 63 | +| `list_methods` | Lists classes and methods visible to the machine. Call it first to discover exact `class`/`method` argument values. Top-level functions live in a synthetic `%dflt` class. | |
| 64 | +| `get_method_ir` | Dumps the CFG of a method: IR statements with indices and successor indices. **Statement indices are the only way to address a statement** (source line numbers are not preserved in the IR); pass them as `stmtIndex` to `check_reachability`. | |
| 65 | +| `generate_tests` | Symbolically executes a method and returns one test case per explored path: concrete `parameters`/`thisInstance` plus the expected return value or exception. | |
| 66 | +| `check_exceptions` | Same exploration, but reports only the paths that throw, together with the inputs that trigger them. | |
| 67 | +| `check_reachability` | Directed (targeted) search towards a given statement. Returns `REACHABLE` with a witness (concrete inputs) or `NOT_REACHED_WITHIN_BUDGET`. | |
| 68 | +| `find_unreachable_code` | Reports `if` branches never taken during exploration — dead-code candidates (statement indices match `get_method_ir`). | |
| 69 | +| `find_counterexample` | Tries to **falsify a boolean property function** written by the LLM: searches for inputs where it returns `false` (counterexamples) or throws (crashes). | |
| 70 | + |
| 71 | +### Result format |
| 72 | + |
| 73 | +Analysis results are JSON. Concrete values map to JSON naturally; JS-specific values are |
| 74 | +tagged objects so nothing is ambiguous: |
| 75 | + |
| 76 | +```json |
| 77 | +{ |
| 78 | + "kind": "SUCCESS", |
| 79 | + "thisInstance": { "$kind": "object", "class": "BasicConditions", "properties": {} }, |
| 80 | + "parameters": [ 12.0, { "$kind": "number", "value": "NaN" }, { "$kind": "undefined" } ], |
| 81 | + "returnValue": 1.0 |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +Exceptional paths use `"kind": "EXCEPTION"` with an `exception` object instead of `returnValue`. |
| 86 | + |
| 87 | +## Hybrid workflows |
| 88 | + |
| 89 | +**Test generation.** `generate_tests` yields per-path inputs and outcomes; the LLM turns |
| 90 | +them into a unit-test file with real assertions and can immediately execute it to validate. |
| 91 | + |
| 92 | +**Reachability querying.** `get_method_ir` → pick the index of an interesting statement |
| 93 | +(a `return`, a branch) → `check_reachability`. A witness is a ready-made regression input. |
| 94 | + |
| 95 | +**Hypothesis falsification** (`find_counterexample`). The LLM writes a property function |
| 96 | +into a `.ts` file and asks the machine to break it: |
| 97 | + |
| 98 | +```typescript |
| 99 | +function abs(x: number): number { |
| 100 | + if (x < 0) return -x; |
| 101 | + return x; |
| 102 | +} |
| 103 | + |
| 104 | +// Hypothesis: "abs is always non-negative" |
| 105 | +function propAbsNonNegative(x: number): boolean { |
| 106 | + return abs(x) >= 0; |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +`find_counterexample(file, method="propAbsNonNegative")` → `COUNTEREXAMPLE_FOUND` with |
| 111 | +concrete falsifying inputs, or `NO_COUNTEREXAMPLE_WITHIN_BUDGET`. |
| 112 | + |
| 113 | +**Equivalence checking of a refactoring.** A special case of the above: the LLM puts the |
| 114 | +original `f`, its refactored `g`, and `function equiv(x: number): boolean { return f(x) === g(x); }` |
| 115 | +into one file and falsifies `equiv`. A counterexample is an input where the refactoring |
| 116 | +changed behavior. |
| 117 | + |
| 118 | +## Interpreting verdicts honestly |
| 119 | + |
| 120 | +- Exploration is **bounded by a time budget**. `NOT_REACHED_WITHIN_BUDGET`, |
| 121 | + an empty `check_exceptions` result, or `NO_COUNTEREXAMPLE_WITHIN_BUDGET` are |
| 122 | + *evidence*, not proofs. Retry with a larger `timeoutMs` when it matters. |
| 123 | +- The symbolic model **over-approximates JavaScript semantics** in places |
| 124 | + (e.g., number comparisons involving `NaN` and untyped values), so a `REACHABLE` |
| 125 | + witness may occasionally be spurious. The recommended hybrid loop is to |
| 126 | + **validate every witness by actually running the code** with the reported inputs |
| 127 | + (node/ts-node) — the tool responses remind about this. The same over-approximation |
| 128 | + can make `find_unreachable_code` miss dead branches. |
| 129 | +- The property/code for `find_counterexample` must live in a **single `.ts` file** |
| 130 | + (project-level scenes are not wired up yet), and must stay within the TS subset |
| 131 | + supported by `usvm-ts` (numbers, booleans, objects, arrays; strings partially). |
| 132 | + |
| 133 | +## Module layout |
| 134 | + |
| 135 | +``` |
| 136 | +src/main/kotlin/org/usvm/mcp/ |
| 137 | +├── Main.kt # stdio transport, stdout guard |
| 138 | +├── UsvmMcpServer.kt # server construction, tool registration |
| 139 | +├── McpErrors.kt # expected-failure handling (isError results) |
| 140 | +├── scene/ # EtsScene cache (ArkAnalyzer), method lookup |
| 141 | +├── exec/ # UMachineOptions presets, serialized machine runs |
| 142 | +├── json/ # DTOs and TsTestValue -> JSON rendering |
| 143 | +└── tools/ # one file per MCP tool |
| 144 | +``` |
| 145 | + |
| 146 | +Design notes: |
| 147 | + |
| 148 | +- **stdout discipline**: stdout is the JSON-RPC channel. `Main.kt` re-points `System.out` |
| 149 | + to stderr before anything else, and `logback.xml` sends all logging to stderr |
| 150 | + (`org.usvm`/`org.jacodb`/`io.ksmt` are capped at `WARN`). |
| 151 | +- **One analysis at a time**: machine runs are serialized with a mutex; MCP clients may |
| 152 | + issue concurrent calls, but the solver and the machine are heavyweight. |
| 153 | +- Concrete values are resolved from symbolic states by `TsTestResolver` |
| 154 | + (`usvm-ts`, `org.usvm.util`), shared with the usvm-ts test infrastructure. |
| 155 | + |
| 156 | +## Tests |
| 157 | + |
| 158 | +```bash |
| 159 | +./gradlew :usvm-mcp:test # unit tests (no ArkAnalyzer required) |
| 160 | +``` |
| 161 | + |
| 162 | +For an end-to-end check, use the MCP Inspector recipe above on |
| 163 | +`usvm-ts/src/test/resources/reachability/BasicConditions.ts`. |
0 commit comments