solflow-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@solflow-mcp show me the call flow for Vault.withdraw in this Solidity repo"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
solflow-mcp
SolFlow for AI agents. An MCP server that gives agents (Claude Code, Claude Desktop, Cursor and others) the same call-flow analysis of a Solidity repository that SolFlow shows developers in the browser, as plain text instead of a web UI.
SolFlow compiles a repo with crytic-compile, extracts facts with Slither, and builds one call tree per external
entry point. Modifiers are folded in, virtual and super calls are resolved through C3 linearization, and calls
it can't follow are explicitly marked. A developer explores the result as an interactive graph. An agent can't
use a canvas, so this server returns the same trees as compact text with line numbers that a model can read, quote and reason over.
Same analysis, two views
Every example below comes from the same small project: a Vault with deposits, withdrawals, an owner check,
a reentrancy guard and a pluggable IStrategy. The project is in tests/fixture.
Each screenshot from SolFlow's web UI is followed by the server's real output for the same contract and function,
so you can compare them side by side.
1. The attack surface of Vault
What a developer sees: the Vault section of SolFlow's overview page. Entry points are split into writes and reads,
with modifier badges, call depth (d2), and a red 1 unr badge where a flow contains a call that can't be followed.

What the agent sees: list_entry_points(repo_path), the Vault section
## Vault (src/Vault.sol)
mutating:
adminMint(address,uint256) mods=[onlyOwner] depth=2 nodes=4
deposit(uint256) payable mods=[nonReentrant] depth=2 nodes=6
mintTwice(address,address) mods=[onlyOwner] depth=2 nodes=6
transferOwnership(address) mods=[onlyOwner] depth=1 nodes=2 (from Owned)
withdraw(uint256) mods=[nonReentrant] depth=1 nodes=5 unresolved=1
read-only:
balanceOf(address) depth=0 nodes=1 (from Token)
depth(uint256) depth=2 nodes=3
feeBps() depth=0 nodes=1
owner() depth=0 nodes=1 (from Owned)
position(address) depth=0 nodes=1
positions(address) depth=0 nodes=1
strategy() depth=0 nodes=1
totalSupply() depth=0 nodes=1 (from Token)These are the same 13 entries. The modifier badges become mods=[...], d2 becomes depth=2,
the red 1 unr becomes unresolved=1, and (from Owned) says where an inherited function comes from.
The full output lists every contract the same way, under the header
19 entry points in 4 contracts. Format: signature mods=[...] depth nodes unresolved (inherited-from).
2. One flow, end to end: Vault.withdraw
What a developer sees: the expanded graph of Vault.withdraw, with one card of source per call. The modifier is shown
in blue, the interface call has a "resolve IStrategy → unbound" picker, and the low-level .call is shown in red.

What the agent sees: get_flow(repo_path, "Vault.withdraw")
Vault.withdraw(uint256) [external, mutating] src/Vault.sol:40-45 uses: require(bool,string)
├─ L40 modifier ReentrancyGuard.nonReentrant() src/Base.sol:20-25 uses: require(bool,string)
├─ L41 internal → Token._burn(address,uint256) src/Base.sol:37-41 uses: require(bool,string)
├─ L42 external → IStrategy.withdraw(uint256) ⚠ interface call, body unknown (no binding) src/IStrategy.sol:5
└─ L43 UNRESOLVED [low_level_call] <address>.call(...) @ src/Vault.sol:43 (low_level/call)Each line is: call-site line, call kind, target, and file:line-range. The order of operations
(burn at L41, then the external call at L42, then ETH sent at L43) can be read straight down the tree, and every node
can be quoted in a finding. Pass include_source=true to see the code cards from the graph, inlined under each node:
├─ L41 internal → Token._burn(address,uint256) src/Base.sol:37-41 uses: require(bool,string)
│ 37 | function _burn(address from, uint256 amount) internal virtual {
│ 38 | require(balanceOf[from] >= amount, "balance");
│ 39 | balanceOf[from] -= amount;
│ 40 | totalSupply -= amount;
│ 41 | }Card by card: the blue card is the modifier line, Vault._burn (inherited from Token) is the
internal → Token._burn line, the IStrategy.withdraw card with its unbound picker is the ⚠ interface call line,
and the red <address>.call card is the UNRESOLVED [low_level_call] line.
3. Libraries, super, and dependencies: Vault.deposit
What a developer sees: Vault.deposit calls a library (FeeMath.fee), an override that calls
super._mint, and a dependency under lib/ (the dashed SafeMath2.add2 card) that is shown but not followed.

What the agent sees: get_flow(repo_path, "Vault.deposit")
Vault.deposit(uint256) [external, payable] src/Vault.sol:33-38
├─ L33 modifier ReentrancyGuard.nonReentrant() src/Base.sol:20-25 uses: require(bool,string)
├─ L34 library → FeeMath.fee(uint256,uint256) src/Vault.sol:9-11
├─ L35 internal → Vault._mint(address,uint256) src/Vault.sol:69-72 uses: require(bool,string)
│ └─ L71 internal → Token._mint(address,uint256) (via super) src/Base.sol:32-35
└─ L36 external-dep (not followed) → SafeMath2.add2(uint256,uint256) [lib/dep/src/SafeMath2.sol]4. Following IStrategy into AaveStrategy
What a developer sees: the Interface Bindings panel on the overview page, with IStrategy set to
AaveStrategy. The same choice can be made from the picker on the interface card, and it re-renders every flow.

After binding, the Vault.withdraw graph replaces the IStrategy.withdraw stub with AaveStrategy.withdraw's real body,
labeled bound: IStrategy → AaveStrategy:

What the agent sees: first, list_unresolved(repo_path) reports what couldn't be followed and suggests candidates:
Unresolved call sites: 1
## low_level_call (1)
src/Vault.sol:43 <address>.call(...)
reached from: Vault.withdraw(uint256)
## Interface calls with no known body (bind with set_interface_bindings)
IStrategy: reached from 1 entry point; candidates (inherit it or match all its methods): AaveStrategy, VaultThen set_interface_bindings(repo_path, {"IStrategy": "AaveStrategy"}) rebuilds every flow without recompiling:
Active bindings: {'IStrategy': 'AaveStrategy'}
Across all flows (before → after):
interface calls with no body: 1 → 0
calls followed via a binding: 0 → 1
other unresolved nodes: 1 → 1After that, get_flow(repo_path, "Vault.withdraw") follows the call into the implementation. The node says the
binding was chosen, not proven:
Vault.withdraw(uint256) [external, mutating] src/Vault.sol:40-45 uses: require(bool,string)
├─ L40 modifier ReentrancyGuard.nonReentrant() src/Base.sol:20-25 uses: require(bool,string)
├─ L41 internal → Token._burn(address,uint256) src/Base.sol:37-41 uses: require(bool,string)
├─ L42 external → AaveStrategy.withdraw(uint256) (BOUND IStrategy→AaveStrategy: asserted, not proven) src/AaveStrategy.sol:9-12
└─ L43 UNRESOLVED [low_level_call] <address>.call(...) @ src/Vault.sol:43 (low_level/call)5. Questions an agent asks directly
A developer answers these by clicking around the graph and reading code. An agent gets a tool for each.
Who can reach this function? find_paths_to(repo_path, "_mint")
3 entry points reach '_mint' (definitions: Token._mint(address,uint256), Vault._mint(address,uint256)). Shortest path each; [Ln] = call-site line in the caller.
• Vault.adminMint(address,uint256) mods=[onlyOwner] (2 matching calls in its tree)
[L48] Vault._mint(address,uint256)
• Vault.deposit(uint256) mods=[nonReentrant] (2 matching calls in its tree)
[L35] Vault._mint(address,uint256)
• Vault.mintTwice(address,address) mods=[onlyOwner] (4 matching calls in its tree)
[L53] Vault._mint(address,uint256)Show me this function. get_function_source(repo_path, "Vault._mint")
function Vault._mint(address,uint256)
location: src/Vault.sol:69-72
attributes: internal, mutating
params: address to, uint256 amount
69 | function _mint(address to, uint256 amount) internal override {
70 | require(to != address(0), "zero");
71 | super._mint(to, amount);
72 | }
outgoing calls (static, before virtual dispatch):
L71 internal → Token._mint(address,uint256)What is this type? get_type_definition(repo_path, "Position")
struct Position (src/Vault.sol:14-17)
14 | struct Position {
15 | uint256 shares;
16 | Kind kind;
17 | }
references: enum KindMistakes come back as instructions, not stack traces:
No entry point 'Vault.withdrw'. Use list_entry_points to see valid names.How the UI maps to the tools
In the SolFlow UI | Tool the agent calls |
Overview page: contracts, writes and reads, badges |
|
Flow graph with code cards |
|
Red |
|
Interface Bindings panel and Save to solflow.toml |
|
CLI flags |
|
Clicking through cards to read code |
|
Searching flows by hand for a function |
|
Why text and not images or JSON?
Images: reading code from screenshots is slow and lossy for a model, and an image can't be quoted.
JSON: carries the same information at roughly 1.5–1.9× the tokens. This was measured on real flows from a 35k-line protocol: a 177-node flow is ~5.5k tokens as a text tree and ~10.3k as JSON. Each line of the text tree stands on its own and its depth is visible from the indentation. In deep JSON the model has to track brackets across thousands of tokens to know which list of calls it's in.
Related MCP server: ripple
Tools
Tool | Purpose |
| Compile or recompile ( |
| All external entry points, grouped by contract and split into mutating and read-only, with modifiers, depth, node count and unresolved count. |
| The call tree for one entry point as an indented text tree. |
| Source and metadata of any function or modifier, plus its outgoing calls. |
| Reverse reachability: which entry points reach |
| Calls that can't be followed statically (interfaces, |
| Bind an interface to a concrete contract and rebuild instantly. Optionally save to |
| Struct, enum or user-defined value type definitions. |
Compilation runs once per repo and is cached: a few seconds for a small repo, and about a minute for a 35k-line via_ir protocol. All later calls are instant.
Install
Requirements
uv. It installs Python 3.12, SolFlow, Slither and the MCP SDK.
A
solcmatching the target, set up with solc-select:solc-select install 0.8.x && solc-select use 0.8.x.The target project must build on its own first. Run
forge installornpm installbefore analyzing.
Claude Code
git clone https://github.com/fuzious/solflow-mcp.git
cd solflow-mcp && uv sync
claude mcp add solflow -- uv run --directory "$PWD" solflow-mcpTo make it available in every project, add --scope user.
Other MCP clients (Claude Desktop, Cursor and so on)
{
"mcpServers": {
"solflow": {
"command": "uv",
"args": ["run", "--directory", "/ABS/PATH/TO/solflow-mcp", "solflow-mcp"]
}
}
}If the client can't find solc, add "env": {"PATH": "<your PATH including solc-select's bin>"}.
Agent skill
skills/solflow-audit/SKILL.md teaches an agent how to use these tools:
the audit workflow and the order of calls;
how to read every marker (
⚠,UNRESOLVED,BOUND,external-dep,NO IR);scope controls;
playbooks for access control, reentrancy and external-call ordering, and external trust boundaries;
how to report what the analysis can't see.
Install it for Claude Code:
mkdir -p ~/.claude/skills && cp -R skills/solflow-audit ~/.claude/skills/A typical agent session:
list_entry_points(repo_path): map the attack surface.get_flow(repo_path, "Vault.withdraw", include_source=true): read one flow end to end.find_paths_to(repo_path, "_mint"): find who can reach a sensitive internal function.list_unresolved(repo_path), thenset_interface_bindings(...): follow calls through interfaces.After editing
.solfiles, runanalyze_repo(repo_path, force=true).
Notes
Default scope: excludes
**/*.t.sol,**/test/**,**/tests/**,**/mocks/**,*Mock*contracts and dependency contracts (lib/**,node_modules/**). Project contracts still get the entry points they inherit from dependencies. Passuse_default_excludes=falseto include them all.Dependencies: code under
lib/andnode_modules/is shown asexternal-depand not followed, unless you passinline_libraries. The same applies to dependency folders outside the project root, such as a monorepo package withlibs = ["../../lib"].Monorepos: point
repo_pathat the package that has thefoundry.toml. When its libs or remappings reach outside the package (../../lib), solc sees some files under two names, and Slither then silently drops whole contracts. The server avoids this by passing absolute paths to forge throughFOUNDRY_LIBS/FOUNDRY_REMAPPINGSfor its own build. Yourfoundry.tomlis never modified.No silent gaps: if Slither still fails on part of a build,
analyze_repoprints⚠ ANALYSIS GAPS,list_entry_pointsnames the missing contracts, and affected tree nodes are markedNO IR. The agent is never shown an incomplete tree as if it were complete.Not a vulnerability scanner: SolFlow shows structure. Finding bugs is the agent's job.
Local only: analysis makes no network calls.
Development
uv sync
uv run pytest # compiles the fixtures with solc and checks every toolThe tests compile tests/fixture, a single-package Foundry project, and
tests/fixture_monorepo, a package whose libraries live outside its root, and check every tool's output against the source.
License and credits
AGPL-3.0-only (see LICENSE), because this package imports SolFlow, which is AGPL-3.0-only. All of the analysis is SolFlow's work. This project is a text interface to it for agents.
Available Tools
8 toolsanalyze_repoAIdempotent
Compile a Solidity repo (Foundry/Hardhat/etc. via crytic-compile + Slither) and build one call-flow per external entry point. Returns a summary.
Other tools call this implicitly with defaults, so only call it to (a) change
scope, or (b) recompile after source edits (force=true).
Each call sets the WHOLE scope from defaults + solflow.toml + these arguments;
options and unsaved set_interface_bindings from earlier calls are dropped.
Args:
repo_path: Absolute path to the project root (where foundry.toml etc. lives).
force: Recompile even if cached (needed after editing .sol files).
exclude_paths: Gitignore-style globs; contracts in matching files get no flows.
exclude_contracts: Globs on contract names to exclude (e.g. "*Harness*").
inline_libraries: Names of lib/<NAME>/ dependencies to recurse into instead
of stopping at them (e.g. "openzeppelin-contracts").
stub_paths: Globs for in-tree call targets NOT to recurse into (e.g. dense
math libs: "src/libraries/FullMath.sol").
bindings: Interface -> concrete contract, e.g. {"IOracle": "ChainlinkOracle"},
so calls through the interface are followed into that implementation.
use_default_excludes: Default excludes are tests (*.t.sol, test/, tests/),
mocks/, *Mock* contracts and dependency contracts (lib/, node_modules/,
including such folders outside the repo root). Excluded contracts get no
entry points of their own; project contracts that inherit dependency
code still get those entry points. Set false to include them.
config_path: Optional solflow.toml path (default: <repo>/solflow.toml if present).
Inputs: repo_path (required), force, bindings, stub_paths, +5 more.
find_paths_toARead-onlyIdempotent
Reverse reachability: which entry points can reach a given function (or dependency call), and through which call chain. E.g. "who can reach _mint?", "which external functions end up in Pool.swap?".
Args:
repo_path: Project root.
target: Function as "Contract.fn(types)", "Contract.fn" or bare "fn".
Bare names match every contract's function with that name.
include_read_only: Also search view/pure entry points.
max_chars: Output size cap.
Inputs: target (required), repo_path (required), max_chars, include_read_only.
get_flowARead-onlyIdempotent
Show the full execution call tree for one entry point as an indented text tree: modifiers, internal/library/external calls (with the parent line that makes each call), inherited/virtual dispatch resolved through C3, builtins used (require, keccak256, ...), dependency calls not followed, and UNRESOLVED calls with the reason.
Args:
repo_path: Project root.
entry_point: "Contract.fn(type1,type2)", "Contract.fn", or bare "fn" if unique.
max_depth: Limit tree depth (root = 0). Omit for the full tree.
include_source: Inline each function's source (line-numbered) the first
time it appears. Great for reading a flow end-to-end in one call.
collapse_repeats: Print a function's subtree only once; later occurrences
say "(subtree already shown above)". Keeps output small.
max_chars: Output size cap.
Inputs: repo_path (required), entry_point (required), max_chars, max_depth, +2 more.
get_function_sourceARead-onlyIdempotent
Get the source (line-numbered) and metadata of any function or modifier, including internal/private ones and those in libraries, plus its outgoing calls.
Args:
repo_path: Project root.
function: "Contract.fn(type1,type2)", "Contract.fn", or bare "fn" if unique.
Inputs: function (required), repo_path (required).
get_type_definitionARead-onlyIdempotent
Get the definition of a struct, enum, or user-defined value type, plus the definitions of user types its members reference (one level per call).
Args:
repo_path: Project root.
name: Type name, bare ("PoolKey") or qualified ("Morpho.Position").
Inputs: name (required), repo_path (required).
list_entry_pointsARead-onlyIdempotent
List every externally callable entry point, grouped by contract and split into mutating vs read-only, with modifiers, call-tree depth, node count and number of unresolved calls. Use the printed names with get_flow.
Args:
repo_path: Project root.
contract: Only this contract (exact name).
query: Case-insensitive substring filter on "Contract.fn(types)".
mutating_only: Hide view/pure entry points.
max_chars: Output size cap.
Inputs: repo_path (required), query, contract, max_chars, +1 more.
list_unresolvedARead-onlyIdempotent
List every call site static analysis cannot follow (unbound interface calls, low-level .call/.delegatecall, Yul dynamic dispatch, abstract with no impl), which entry points hit each one, and interfaces that could be bound to a concrete implementation via set_interface_bindings (with candidates).
Args:
repo_path: Project root.
contract: Only flows whose entry point is on this contract.
max_chars: Output size cap.
Inputs: repo_path (required), contract, max_chars.
set_interface_bindingsAIdempotent
Bind interfaces to concrete contracts so flows follow calls like IOracle(addr).price() into the chosen implementation's body. Rebuilds flows instantly (no recompile). Bound edges are marked as asserted, not proven.
Args:
repo_path: Project root.
bindings: {"IInterface": "ConcreteContract", ...}. Map an interface to ""
to remove its binding.
replace: Replace all current bindings instead of merging.
save: Persist the resulting bindings to the [bindings] table of
solflow.toml (only that table is rewritten).
Inputs: bindings (required), repo_path (required), save, replace.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 8 tools
Each tool has a clearly distinct role in the Solidity call-flow analysis workflow: setup, entry-point listing, single-flow inspection, function source retrieval, reverse reachability, unresolved-call diagnostics, and interface binding. Overlap between get_flow and get_function_source is minimal because one targets entry-point call trees while the other retrieves arbitrary function source and metadata.
All tool names use consistent snake_case and begin with an action verb. The convention is predictable across setup, retrieval, listing, search, and configuration tools.
Eight tools is a well-scoped set for a Solidity static-analysis server. Each tool covers a distinct stage of the analysis workflow, and none appears to be filler.
The surface covers compilation, entry-point discovery, flow inspection, source retrieval, reverse paths, unresolved calls, and interface binding, which supports the core analysis workflow well. Minor gaps remain, such as no direct enumeration of internal functions or contracts beyond external entry points, which agents may need to infer through flows or known names.
Maintenance
Related MCP Connectors
Read-only smart-contract security intelligence for autonomous agents.
Solidity vulnerability scan (Slither) for AI agents. Free preview; full report 1 USDC on Solana x402
Smart-contract risk reports for AI agents on 20 EVM chains, paid per call via x402 ($0.02 USDC).
Pay-per-call (x402/USDC-Base) web + crypto data tools for AI agents: audit, extract, crypto, DeFi.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceProvides local-first repository architecture analysis with file-level proof, enabling agents to map codebases, locate implementations, trace call paths, and assess change impact with deterministic evidence.4 npm10MIT
- AlicenseAqualityBmaintenanceProvides coding agents with a Python repository's call graph and impact analysis, enabling them to see callers, callees, and affected tests before making changes.51MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents and terminal users to query codebases for callers, impact, tests, dependencies, and refactor previews using static AST analysis, with every match accounted for and uncertain relationships clearly marked.844 npm2MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI coding agents to query a local code graph over stdio, so they can see what a Git change may affect, find related tests, routes and nodes, and cite evidence paths — read-only and fully on-machine.6 npmMIT