Skip to main content
Glama

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.

SolFlow overview: the Vault contract's entry points

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.

SolFlow flow graph for Vault.withdraw

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.

SolFlow flow graph for Vault.deposit

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.

SolFlow Interface Bindings panel with IStrategy bound to AaveStrategy

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

SolFlow flow graph for Vault.withdraw after binding IStrategy to 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, Vault

Then 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 → 1

After 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 Kind

Mistakes 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

list_entry_points

Flow graph with code cards

get_flow (include_source=true for code)

Red unr badges, unbound interface cards

list_unresolved

Interface Bindings panel and Save to solflow.toml

set_interface_bindings (save=true)

CLI flags --exclude-path, --stub-path, --inline-library

analyze_repo scope arguments

Clicking through cards to read code

get_function_source, get_type_definition

Searching flows by hand for a function

find_paths_to

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

analyze_repo

Compile or recompile (force=true) and set scope (excludes, stubs, inlined libs, bindings). Returns a summary. Optional: other tools compile automatically.

list_entry_points

All external entry points, grouped by contract and split into mutating and read-only, with modifiers, depth, node count and unresolved count.

get_flow

The call tree for one entry point as an indented text tree. include_source=true inlines line-numbered code.

get_function_source

Source and metadata of any function or modifier, plus its outgoing calls.

find_paths_to

Reverse reachability: which entry points reach X, and by which chain?

list_unresolved

Calls that can't be followed statically (interfaces, .call, Yul) and the entry points that hit them, plus candidate implementations to bind.

set_interface_bindings

Bind an interface to a concrete contract and rebuild instantly. Optionally save to solflow.toml.

get_type_definition

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 solc matching 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 install or npm install before 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-mcp

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

  1. list_entry_points(repo_path): map the attack surface.

  2. get_flow(repo_path, "Vault.withdraw", include_source=true): read one flow end to end.

  3. find_paths_to(repo_path, "_mint"): find who can reach a sensitive internal function.

  4. list_unresolved(repo_path), then set_interface_bindings(...): follow calls through interfaces.

  5. After editing .sol files, run analyze_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. Pass use_default_excludes=false to include them all.

  • Dependencies: code under lib/ and node_modules/ is shown as external-dep and not followed, unless you pass inline_libraries. The same applies to dependency folders outside the project root, such as a monorepo package with libs = ["../../lib"].

  • Monorepos: point repo_path at the package that has the foundry.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 through FOUNDRY_LIBS / FOUNDRY_REMAPPINGS for its own build. Your foundry.toml is never modified.

  • No silent gaps: if Slither still fails on part of a build, analyze_repo prints ⚠ ANALYSIS GAPS, list_entry_points names the missing contracts, and affected tree nodes are marked NO 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 tool

The 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 tools
analyze_repoA
Idempotent

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_toA
Read-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_flowA
Read-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_sourceA
Read-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_definitionA
Read-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_pointsA
Read-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_unresolvedA
Read-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_bindingsA
Idempotent

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

    A4.2/5.0

    Scored across 8 tools

    Disambiguation5/5

    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.

    Naming Consistency5/5

    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.

    Tool Count5/5

    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.

    Completeness4/5

    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

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      B
      maintenance
      Provides 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 npm
      10
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      Provides coding agents with a Python repository's call graph and impact analysis, enabling them to see callers, callees, and affected tests before making changes.
      5
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables 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 npm
      2
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables 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 npm
      MIT