Back to Browse

Stratigraph MCP Server

Developer ToolsUse Caution4.8MCP RegistryLocal
Free

Server data from the Official MCP Registry

Cited structure of Java/Spring/Angular codebases: dependencies, tables, endpoints, hotspots.

About

Cited structure of Java/Spring/Angular codebases: dependencies, tables, endpoints, hotspots.

Security Report

4.8
Use Caution4.8High Risk

stratigraph is a local, read-only code-analysis CLI and MCP server whose visible code shows no exfiltration, backdoor, or obfuscation patterns, and API keys are refused in the committed config file. The main concerns are that structural metadata goes to the Anthropic API by default when the LLM layer is enabled, and that the upgrade command runs project Maven or Gradle builds and can send source to Claude Code when opted in. The provided source is truncated, so the review of the shell-spawning and network modules is incomplete. Supply chain analysis found 7 known vulnerabilities in dependencies (0 critical, 3 high severity). Package verification found 1 issue.

6 files analyzed Β· 12 issues found

Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.

Permissions Required

This plugin requests these system permissions. Most are normal for its category.

File System Read

Reads files on your machine. Normal for tools that analyze or process local data.

File System Write

Writes or modifies files on your machine. Check that this is expected for the tool.

HTTP Network Access

Connects to external APIs or services over the internet.

process_spawn

Check that this permission is expected for this type of plugin.

env_vars

Check that this permission is expected for this type of plugin.

database

Check that this permission is expected for this type of plugin.

How to Install

Add this to your MCP configuration file:

{
  "mcpServers": {
    "io-github-imashishsharma-stratigraph": {
      "args": [
        "-y",
        "stratigraph"
      ],
      "command": "npx"
    }
  }
}

Documentation

View on GitHub

From the project's GitHub README.

stratigraph

Upgrade Spring Boot with your tests as the judge, and map large Java, Spring and Angular codebases with every claim cited.

npm CI license: Apache-2.0 MCP Registry

Website: https://imashishsharma.github.io/stratigraph/ Β· open source, runs locally

An ER diagram stratigraph read from a Spring Boot application's JPA mappings and migrations

πŸ” Upgrade Spring Boot (2.7 β†’ 3.5, 3.x β†’ 4.0), Maven or Gradle

npx stratigraph upgrade plan --to 3.5               # read-only: what will break, with file:line citations
npx stratigraph upgrade run  --to 3.5 --ai claude-code

OpenRewrite's recipe does most of an upgrade. The upgrade agent handles what it leaves broken, on Maven and Gradle builds alike.

  • Your tests are the judge. It records every test's result before touching the code. A change is kept only if the build gets further and no passing test turns red. Disabling or deleting a test is rejected outright.
  • Layered, reviewable commits on a branch of their own: the recipe, exact known fixes, and AI fixes from Claude Code (opt-in). Application code changed to make a test pass is flagged for review.
  • It knows when to stop. An authorisation rule or an HTTP contract is never chosen for you. It hands those back in upgrade-report.md with the evidence and the options.
Benchmark (details)OpenRewrite alonestratigraph
10 public Spring Boot apps: every previously passing test still passes, no human edits0 / 105 / 10 (+2 stopped at a real decision)
9 apps it had never seen (details)β€”8 / 9
Gradle: realworld-example-app, JHipster 7.9 (details)β€”68/68 parity; JHipster stopped at 1 error (needs JHipster 8)
Time and AI cost per appβ€”1–14 min, $0–2.54

πŸ—ΊοΈ Map a codebase nobody remembers

npx stratigraph scan .        # extract, mine git history, analyse, write the report: one command
  • Architecture: deployables, packages and their dependencies, cycles, and C4 diagrams at all four levels.
  • Data model: an ER diagram from JPA and Liquibase/Flyway, with the drift between them.
  • HTTP API and risk: endpoints, Angular β†’ Spring call links, hotspots by recent churn Γ— complexity, and knowledge silos.
  • Every claim cites a file and line or a commit. Every view states how much of the code it was built from, and is withheld when that's too little.
  • MCP server (stratigraph mcp): 13 read-only tools such as what_breaks_if, who_knows and where_is_table_written, for Claude Code, Cursor or any MCP client.

Measured on 20 public repositories against hand-labelled ground truth: entity and table recall 100%, endpoint recall 99%, DI edges resolved 95% (scorecard).

Why "stratigraph"

Stratigraphy is how archaeologists read a site: layer by layer, deducing the order things happened from what sits on top of what. stratigraph does the same to a large codebase β€” it reads the source, reads the git history, and reconstructs how the thing came to be shaped the way it is.

Aimed at monoliths and multi-module builds of 100k+ LOC where nobody remembers why things are the way they are. Java, Kotlin and Angular first. It runs on repositories that do not compile, have never had node_modules installed, and use layouts nobody has used since Ant β€” because those are exactly the repositories nobody remembers.

The codebase map, in detail

  • A fact graph β€” packages, classes, methods, endpoints, DI edges, routes, O/R mappings β€” in a local SQLite file, every row citing a file and line, a commit sha, or the build file it was read from.
  • History you can act on β€” hotspots ranked by churn Γ— complexity, files whose history is one person, and files that change together over and over with nothing in the code connecting them.
  • C4 diagrams at all four levels, an ER model read out of the O/R mappings, the HTTP surface, and a ranked findings list β€” as Structurizr DSL, as Mermaid, and as one self-contained HTML page: no script, no network, no CDN.
  • A diff between two runs, so CI can fail a pull request for the cycle it added and stay quiet about the six hundred that were already there β€” the only gate a legacy monolith can switch on today.
  • An MCP server, so an agent working in the codebase can ask structural questions instead of grepping for them.
  • Optionally, model-written names and descriptions for the package clusters β€” under a citation check, enforced in code, that rejects any sentence naming something the model was not shown.

Sixty seconds

npm install -g stratigraph      # or run every command through: npx stratigraph

cd ~/work                       # the fact store lands here, not in the repo
stratigraph init    --repo ../some-monolith   # create the fact store
stratigraph extract --repo ../some-monolith   # parse the source into facts
stratigraph history --repo ../some-monolith   # mine git: churn, coupling, authors
stratigraph analyze --repo ../some-monolith --no-llm  # cycles, clusters, hotspots
stratigraph report  --repo ../some-monolith --out ./arch
open ./arch/index.html

TypeScript and Angular analysis works out of the box β€” the extractor ships in this package and runs on the Node you already have. Java and Kotlin need a JDK 17+ and one more command, stratigraph fetch-extractor, which downloads the extractor jar and verifies it against a checksum pinned in this package. No API key is needed for any of the above: --no-llm is the whole structural report, and a model only ever adds prose on top of it.

stratigraph doctor    # says which of these your machine can do right now

Contents

The rule that shapes everything

Static analysis produces facts. The LLM produces interpretation. The LLM never invents a fact.

Every node and edge comes from a parser, from git log, or from a build file. Every claim in a generated report carries a provenance reference β€” a file and line, a commit sha, or a fact-table row. When the model wants to assert something it cannot cite, the correct output is "no evidence found".

This is enforced in the schema, not only in prompts: interpretation lives in different tables from facts, edges are marked fact or inferred, and a citation row has a CHECK constraint forcing it to point at something real. See ADR-0002.

The whole pipeline runs with --no-llm and still produces a useful report.

Measured on real repositories

Quality is measured, not asserted (ADR-0035). stratigraph bench runs the whole pipeline over 20 public repositories, each pinned to a commit in bench/corpus.yaml, and scores the output against ground truth labelled without the tool β€” from each repository's source, build files, migrations and git log β€” in bench/truth/. Every number below is from bench/scorecard.md, which lists every miss by name.

MetricTarget2.0
Non-source files (lockfiles, manifests, tests, generated) in the hotspot top 2000
Hotspot top-10 overlap with a senior engineer's labelled listβ‰₯ 60%66% (92/138)
JPA entity recall, with the physical table nameβ‰₯ 90%100% (146/146)
Table recallβ‰₯ 90%100% (274/274)
HTTP endpoint recallβ‰₯ 95%99% (519/520)
Injection edges resolvedβ‰₯ 85%95% (215/226)
Deployables found as containers / containers that are deployables100%100% (67/67) / 100% (67/67)
File roles matching the labelsβ‰₯ 95%96% (535/557)
Views rendered without a coverage statement00
Repositories the pipeline completed on20/2020/20

The corpus spans Spring Boot monoliths and microservices (petclinic and its Kotlin, REST, legacy-XML and Angular variants, JHipster in Maven, Gradle and OAuth2 flavours, eladmin, piggymetrics, killbill, initializr, OpenMRS, nacos, Spring Cloud Gateway, Spring Boot Admin), Apache Dubbo at 4,000 Java files, and two large TypeScript workspaces (bitwarden/clients at 6,400 files, analog). The labels are only as good as their labelling: where a miss turned out to be a label contradicting the source at the pinned commit, the label was corrected and the evidence recorded in the truth file.

Run it yourself:

stratigraph bench --fetch     # clones the corpus into ~/.cache/stratigraph/corpus
stratigraph bench --private ../your-repo   # aggregates only, nothing that names a file

Install

npm install -g stratigraph

Requires Node 18.18 or newer. Nothing else, until you analyse Java β€” the Java extractor needs a JDK 17+ available, and tells you so rather than crashing. stratigraph doctor is the honest inventory of what your machine can do:

$ stratigraph doctor
ok   stratigraph     v1.4.0, fact-store schema v1
ok   node            v20.11.1 on darwin-arm64
ok   git             git version 2.50.1
warn java            1.8.0_432 from JAVA_HOME is below JDK 17; the Java extractor
                     will not run (this limits the analyser, not the code it can analyse)
warn jvm extractor   jar not found β€” see the README for the two commands that build it
ok   ts extractor    dist/extractors/typescript/main.js (built, no JDK required)
ok   config          defaults (no stratigraph.config.json found)
warn model           claude-opus-5, but no credential found
--   database        .stratigraph/my-repo.db does not exist yet β€” run `stratigraph init`

Every warn above still leaves a working tool: that machine can analyse any TypeScript or Angular repository, mine any git history, and produce the full structural report. stratigraph fetch-extractor clears the second warning; the first needs a JDK 17+, which the tool will find wherever it is installed rather than making you export JAVA_HOME.

For environments where you would rather not think about toolchains at all, the image carries its own JDK, git and extractor:

docker run --rm -v "$PWD:/repo:ro" -v "$PWD/out:/work" \
  ghcr.io/imashishsharma/stratigraph doctor

Set your API key

Only if you want cluster names and ADR candidates. Everything structural β€” the dependency graph, cycles, clusters, hotspots, coupling, the intent-vs- structure findings β€” runs with no key and no configuration at all. Skip this section entirely and use analyze --no-llm.

Pick one. They are tried in this order:

stratigraph config set-key sk-ant-...     # writes ~/.config/stratigraph/config.json, chmod 600
export ANTHROPIC_API_KEY=sk-ant-...       # this shell; put it in ~/.zshrc to keep it
echo 'ANTHROPIC_API_KEY=sk-ant-...' >> .env   # this directory; gitignore it
ant auth login                            # no key at all, if you use the Anthropic CLI

A Claude Pro or Max subscription is not API credit. Those cover Claude.ai and Claude Code; this tool calls api.anthropic.com, which is billed separately from the Console. A key from an account with no API balance authenticates fine and then fails with "Your credit balance is too low". ant auth login does not change this β€” the OAuth token carries a user:inference scope but the organisation still needs credit. Interpretation is the only part affected; everything structural runs regardless.

Then confirm β€” it prints where the key came from, never the key:

$ stratigraph doctor
ok   model        claude-opus-5, credential from ~/.config/stratigraph/config.json

Not sure what is picked up? stratigraph config lists every file that can affect a run, whether or not it exists, and which one won:

$ stratigraph config
Files that configure a run, weakest first. Later ones win.

found   /home/me/.config/stratigraph/config.json    you, every repository
absent  /home/me/work/stratigraph.config.json       this project, committed  (no key here)
absent  /home/me/work/stratigraph.config.local.json this project, your machine
absent  /home/me/work/.env                          environment, e.g. ANTHROPIC_API_KEY

model        claude-opus-5
credential   /home/me/.config/stratigraph/config.json

A key never goes in stratigraph.config.json. That file is meant to be committed, and the tool refuses to load one that contains a key β€” by the time anyone notices, it has to be rotated rather than deleted. The full reference, including per-project settings and apiKeyFile, is under Configuration.

Use

stratigraph init    --repo ../some-monolith   # create the fact store
stratigraph extract --repo ../some-monolith   # run every applicable extractor into it
stratigraph history --repo ../some-monolith   # mine git: churn, complexity, authors
stratigraph analyze --repo ../some-monolith   # cycles, clusters, coupling, hotspots, ownership
stratigraph report  --repo ../some-monolith --out ./arch   # C4 diagrams, HTML, ranked findings
stratigraph diff    --repo ../some-monolith   # what changed since the previous run
stratigraph mcp     --repo ../some-monolith   # serve it all to an agent over MCP
stratigraph prune   --repo ../some-monolith --keep 2   # drop old runs, reclaim the space

analyze --no-llm is the whole report minus the prose: clusters, mismatches, cycles, coupling, hotspots and ownership all come out of algorithms. The model adds names and readings on top of that, and never replaces any of it.

history attaches to the run extract opened, so both halves share one run_id β€” which is what lets analyze say exactly which co-changing files have no dependency between them. Either half works alone: on a machine with no JDK, init + history + analyze still produces a full history report, and analyze says which sections it could not fill rather than leaving them out.

analyze prints each cycle as a path with the edges that justify every hop:

1. [high] com.example.web β†’ com.example.service β†’ com.example.repo β†’ com.example.web
   com.example.web β†’ com.example.service
     imports  web.OrderController β†’ service.OrderService  [src/web/OrderController.java:9]
   ...

Every hop names a file and a line, because a dependency cycle you cannot check is not worth reporting. Cycles are stored as findings with citations into the edges that produced them, never as facts β€” see ADR-0008.

extract runs every extractor the repository needs, detected from what is on disk, and writes them all into one run β€” which is what lets an Angular service and the Spring endpoint it calls be joined at all, since nodes are scoped by run_id. --lang java|ts|all overrides the detection. A missing JDK skips the Java half and keeps going rather than failing the command.

stratigraph extract --emit writes the raw NDJSON to stdout instead of storing it, and stratigraph ingest --from facts.ndjson replays a captured stream.

Angular, and the endpoint it might be calling

The TypeScript extractor emits components, injectables, NgModules, DI edges, routes with their lazy-loaded boundaries, and the component-to-component edges that only exist inside a template. Directories become package nodes, so cycle detection, clustering and every MCP query work on Angular with no change β€” a package cycle in app/admin/metrics reads exactly like one in com.example.web, with the template line cited as evidence where a template is what created it.

Then analyze matches Angular HTTP calls against Spring endpoints:

1 cross-stack HTTP call(s) β€” INFERRED, not observed:

  GET /api/orders/{}  ->  GET /api/orders/{id}
    web/src/app/core/order.service:OrderService#findOne()
      web/src/app/core/order.service.ts:15

  Matched by URL pattern against a declared endpoint. Nothing in either file
  says these are connected; check the cited lines before relying on one.

Every one of those edges is stored with confidence = 'inferred' and is excluded from the package graph, so no cycle can be assembled out of a string match. Only literal and template-literal URLs are matched; a computed one gets a diagnostic and no edge, and a URL matching two endpoints equally well gets a diagnostic naming both rather than a coin toss (ADR-0018). On a real JHipster monolith that honesty is visible: its Angular services build every URL dynamically, so the six candidate calls match eight endpoints equally well each β€” and the run records six diagnostics naming all eight, not six guessed edges.

It also reports subscriptions nothing can end β€” no takeUntil, no retained Subscription, no ngOnDestroy on the class. All three have to hold, so a teardown it cannot see produces silence rather than an accusation.

History, and the coupling nobody wrote down

The output worth having is the second section: files that change together over and over with nothing in the code connecting them. The static graph cannot see that by construction.

Files that change together with no dependency between them (top 20):

1. src/main/java/com/example/order/OrderService.java
   src/main/resources/db/migration/V12__order_status.sql
   31 shared commits β€” strength 0.86, 9.4x chance (36 and 41 commits respectively)

Every such pair is stored as a finding citing the commit shas that produced it, so git show settles any disagreement. Nothing is filtered silently: the report says how many commits were considered, how many were too broad to pair, and how many pairs each threshold removed β€” an empty section reads like a clean repository unless it says what was examined.

Three things decide whether that output is signal or noise, and all three are written down in ADR-0011: merge commits are excluded, commits touching more than 50 files take no part in pairing (one repo-wide reformat otherwise couples everything it touched), and a pair must co-change more often than chance, not merely often.

A fourth decides how loudly it is reported. gradle-wrapper.jar and gradlew.bat change together in 11 of 11 commits β€” a perfect co-change score that means one tool regenerates both. A pair is rated on its strength only when a dependency between the two files could have been observed at all: the run has a static graph, and both files are ones an extractor parses. Otherwise it is low, because the finding's own detail already says no dependency could have been seen either way, and a finding must not be rated as strongly as one whose evidence does not disclaim it. On petclinic that is the difference between 23 high findings and 7 β€” and between a --fail-on high gate that fires on every repository with a build wrapper and one worth turning on (ADR-0028).

Renames are followed, so a file moved three years ago has one history rather than two halves. git log --follow cannot do this β€” it takes exactly one pathspec β€” so the miner makes one whole-repository pass and resolves the rename chains itself (ADR-0009).

Alongside it: hotspots ranked by churn Γ— complexity, and files whose history is concentrated in one person. Complexity is total indentation β€” a proxy, named as one, chosen because it needs no parser and therefore also scores the XML, SQL and properties files that turn up in coupling pairs constantly.

stratigraph history --repo ../some-monolith --since '3 years ago'
stratigraph analyze --repo ../some-monolith --top 40

Clusters, and the packages whose name is a lie

analyze groups packages into communities over one graph built from both layers: the dependency edges the extractor observed, and the co-change the history miner measured. History is allowed to move a package into the group it actually belongs to, which is the point of combining them at all.

3 package clusters (modularity 0.412, coupling weight 1):

1. Order handling (com.example.shop.order) β€” 7 packages
   Serves the order lifecycle and persists it.
     com.example.shop.order.api
     ...

coupling weight is printed because it decides the answer: at 0 the grouping is purely structural, and raising it lets history outvote the imports. Run it again with --coupling-weight 0 and see what moves. The algorithm is Louvain with every source of randomness removed, so the same facts always give the same clusters β€” a partition that shifted between runs would make every finding built on it unfalsifiable (ADR-0012).

The finding worth the milestone is the next one:

1. [high] shop.billing.report is named under shop.billing but clusters with shop.admin
   All of the 3 packages named under shop.billing sit in shop.billing:
   shop.billing.invoice, shop.billing.ledger, shop.billing.payment.
   This one sits in shop.admin instead.
   connected to shop.admin.role: imports shop.billing.report.A β†’ shop.admin.role.A
     (src/shop/billing/report/A.java:201)

A package name is a path, so the packages sharing its parent path are its declared neighbourhood, and "named alongside these three, clustered with those three" is arithmetic β€” not a model's opinion about what report sounds like. The model may later describe what the two responsibilities appear to be; it never decides that a mismatch exists (ADR-0014).

Interpretation, and what stops it inventing things

Names, descriptions and ADR candidates are the only things a model writes, and they are written under a contract enforced in code rather than in the prompt.

Each cluster is packed into a numbered list of evidence β€” packages, edges with file and line, files with their churn, commits β€” where every item carries an opaque, pack-local id (e12, not the database's edge id). A model that guesses a database id lands on a real row, so guessing fails open. A model that guesses e99 in a pack of twelve lands on nothing.

Four rules reject a response outright:

  1. a citation that is not an id in the pack;
  2. a claim that cites nothing;
  3. any identifier, path or commit sha in the prose that the pack did not contain β€” this is the one that catches a real citation attached to a sentence about a class that does not exist;
  4. an ADR candidate whose evidence does not resolve.

A rejected response is retried once with the violations attached, then discarded. Nothing rejected is ever stored, and every give-up leaves a diagnostic row β€” because a cluster nobody could describe must not read like one nobody tried to describe (ADR-0013).

Interpretation by claude-opus-5 β€” 12 of 14 clusters described.
  Names and descriptions above this line are inference, not observation.
  2 description(s) failed the citation check and were discarded β€” see the diagnostic table.

What the check cannot do is verify that the evidence supports the sentence. A model can cite seven genuine imports and draw the wrong conclusion from all of them. That is exactly why the mismatch claim above stays algorithmic, and why everything model-authored is stored with authored_by = 'model', the model id that answered, and a report that says which lines are inference.

Without a credential β€” or with --no-llm β€” analyze says so in one line and prints the structural report unchanged.

The report you would actually show someone

stratigraph report --repo ../some-monolith --out ./arch
arch/index.html                 self-contained: no script, no network, no CDN
arch/workspace.dsl              Structurizr, all three views, with provenance
arch/c4-context.mmd             Mermaid, level 1
arch/c4-container.mmd           level 2
arch/c4-component-<module>.mmd  level 3, one per container
arch/c4-code-<package>.mmd      level 4, one class diagram per package
arch/data-model.mmd             the ER model, from the declared O/R mappings
arch/findings.md                the ranked list, for pasting into an issue

The HTML page carries all of that plus the HTTP surface, a package dependency matrix and ranked hotspots β€” with a contents list, and a panel of the numbers worth knowing before you read the rest.

C4 is a projection of the fact graph, not a new model. A container is a deployable β€” a module whose build file applies the Spring Boot plugin, packages a WAR or declares an Angular/Nx application, or that holds a @SpringBootApplication class β€” and cites that proof; aggregator POMs, BOMs and libraries are not containers, and a library's packages are drawn inside each container that uses it (ADR-0040). A component is a package β€” in an Angular workspace, a project, NgModule or lazy route boundary rather than a directory (ADR-0042); a data store exists because a @Table mapping was read; an external system exists because an extractor read an absolute URL out of a literal. Where C4 asks for something no fact supplies, the diagram omits the box and says so on the page:

No fact in this run identifies a person, a role or a user, so no actor is drawn. C4 normally puts one here; this diagram shows only what a parser read.

Every C4 diagram you have seen has a stick figure in the top left. Drawing one here would take four lines of code and nobody would question it β€” and it would be a box in an architecture map that no parser ever produced (ADR-0019).

The HTML renders its own SVG rather than bundling a renderer or shelling out to a headless browser, so a 40 KB page opens from a file share, survives being emailed, and works with scripting disabled. The layout is deterministic to the pixel β€” the same run produces the same bytes, which is what lets a fixture test assert the whole SVG rather than that it contains a <rect> (ADR-0020).

Findings are ranked by severity, then observation before inference, then how much evidence there is, then rule name, then id β€” arithmetic over stored columns, total, and reproducible. No weighted score, because a constant invented in the report layer is a judgement nobody can argue with. And this is where the promise the schema has carried since M0 is finally kept: a finding with no citation is not published, it is excluded from every count, and the number excluded is printed (ADR-0021).

Level 4 is one class diagram per package β€” a package is something the source states, where a cluster is something an algorithm decided, and the level where a reader is closest to the code is the wrong place to introduce a grouping that is not in the code. Boxes carry stereotypes, fields and methods as declared; generalisation, realisation and association are drawn as UML draws them.

The data model is read out of the O/R mappings. The entity is the table, its columns are the mapped class's fields including the ones it inherits β€” JPA maps a mapped-superclass chain into the subclass's table, so without that owners would have no primary key β€” and cardinality comes from the JPA annotation rather than from a field being plural.

That last part cost an extractor change worth recording. List<Pet> erases to java.util.List for the fqn, and has to, or two overloads differing only by type argument become two nodes. It also destroys the only thing that says what the collection holds: on petclinic, three of four entity associations had a readable cardinality and an unreadable target. The extractor now records the type arguments beside the erased type β€” no fqn changes β€” and all four relationships come out (ADR-0022). An association whose target still cannot be read gets a row in its own table saying why, never a line to a plausible one.

Run against spring-petclinic β€” 49 Java sources, 1,040 commits β€” it produces 6 tables with 4 relationships, 6 class diagrams, 17 endpoints, and 44 findings, every one carrying either a file:line or a commit sha, and each one states its own limits inline:

mvnw and mvnw.cmd change together β€” 11 of the 11 commits touching either file touch both, 65.1Γ— what independent files would share. No extractor parses mvnw or mvnw.cmd, so no dependency could have been observed between them either way. This is co-change without a checkable explanation, not a demonstrated absence of coupling in the code.

Ask it questions from an agent

stratigraph mcp serves the fact store over MCP on stdio, so an agent working in the codebase can ask structural questions instead of grepping for them.

claude mcp add stratigraph -- stratigraph mcp --repo ../some-monolith

Thirteen tools, all read-only:

ToolAnswers
describe_runWhat this store contains, and β€” the point of it β€” what it does not
find_nodeResolve a name you have to the exact fqn the other tools take
query_dependenciesWhat a package or type depends on, and what depends on it
find_callersEvery observed call or injection into a method or type
describe_moduleOne package in full: types, endpoints, tables, churn, cluster
list_endpointsThe HTTP surface, with the method that serves each route
find_hotspotsChurn Γ— complexity, or files whose history is one person
trace_to_tableThe types mapped to a table, and what reaches them
check_cycleWhether two packages depend on each other, with the edges
what_breaks_ifWhat depends on a symbol, directly and transitively, with the edges
who_knowsWho has changed a file or package, and how concentrated that knowledge is
where_is_table_writtenThe code that writes and reads a table, cited at each access
explain_hotspotWhy a file ranks as a hotspot: its commits, authors and complexity

The server only reads. It opens the database read-only and never starts an extractor: a stale store is reported as stale, not silently rebuilt, because a tool call is a bad place to start a JVM over 4,000 files. Every answer ends by naming the files that changed on disk since the run read them. When a newer run completes, for example after stratigraph scan in another terminal, the server moves to it, and the first answer after the move says so, naming both runs. That way a transcript shows where answers stop agreeing. --run <id> pins one run for the session instead (ADR-0046).

Every result carries a file and line, an fqn or a sha. Anything a model wrote comes back labelled authoredBy: "model" and is never blended into the structural answer. And every tool that can return nothing says which kind of nothing it is β€” found for "that name is not in this run", covered for "this run could not have answered that" β€” because to an agent an empty array reads as "there is no such thing" either way (ADR-0015).

Pointed at dubbo, with the repository itself out of context, it answered five structural questions correctly β€” a cycle with both directions cited, the sole call site of a method, a bus-factor file, a package summary β€” and each answer was checked by hand against the lines it cited. One of its numbers disagreed with git log --follow, and the tool turned out to be right; the run is recorded in the ADR.

Keeping it current

Run stratigraph scan again after changing code. Extractors whose inputs did not change are skipped: their stored facts are replayed instead. An input is every file the extractor could read, tracked or not, plus the extractor itself. If HEAD has not moved, the history mined last time is reused too. Both are exact, not approximate. A replayed stream is byte-for-byte what the extractor would print, and the run records facts reused from run N. On nacos (5,600 files), a TypeScript-only edit refreshes in about 15 s instead of about 78 s.

The limit: the Java extractor parses the whole program at once, so any .java or .kt change re-parses all of it. --no-reuse on extract, history or scan turns reuse off.

The JVM extractor β€” Java and Kotlin

Needs a JDK 17+ to run in; it parses source of any vintage, including Java 8. It parses the source set and never runs or resolves your build (ADR-0006), so it works on a repository that does not compile, has no build file, or uses a layout nobody has used since Ant. Plain core Java with no framework at all gets the full structural output β€” package graph, cycles and all.

Kotlin is read by the same extractor, in the same run. That last part is the point: Kotlin turns up inside Java repositories β€” a Spring Boot service with a Kotlin test module, a team migrating package by package β€” and two separate analyses could not join a Kotlin service to the Java repository it calls, because nodes are scoped by run_id. One run, one graph.

Everything built on the fact graph therefore works on Kotlin without knowing about it. On a four-file Kotlin fixture:

node  class     com.example.shop.service.OrderService
edge  annotated_with  OrderService -> org.springframework.stereotype.Service
edge  injects         OrderService -> repo.OrderRepository
node  endpoint  GET /api/orders/{id}
edge  handles         OrderController#byId(long) -> GET /api/orders/{id}
edge  maps_to         domain.Order -> orders

β€” constructor injection, the HTTP surface, and an ER model with Long id PK read out of @Entity and @Id. --lang kotlin, kt and jvm all name this extractor; .kt files select it with no flag at all. A build.gradle.kts is read as a module's identity, never as a source file.

What the J model has no place for is read as its nearest Java shape or not at all: extension functions appear as methods, and coroutines and object declarations have no distinct representation yet. Nothing is guessed (ADR-0029).

One boundary is worth knowing before you rely on a mixed repository. The two languages are parsed by different parsers that cannot see each other's sources, so a Kotlin class injecting a Java one resolves β€” that comes from the import and the declared type β€” while a call from Kotlin into a Java method does not:

edge  injects  com.example.NewService -> com.example.LegacyRepository   βœ“
info: 1 call site(s) could not be resolved to a declaring type and were
      not recorded as edges

Closing that would mean compiling the Java half and handing the Kotlin parser a classpath, which is the one thing this extractor will not do. So it is an absence with a diagnostic attached, never a guessed edge.

The npm package does not ship the jar β€” it is 22 MB of JVM bytecode, and most installs never analyse a line of Java. One command downloads it:

stratigraph fetch-extractor

It is verified against a SHA-256 written into the npm package by the same release job that built and attached the jar, so the digest never arrives from the same place as the file it describes. A mismatch writes nothing and says so. There is no flag to skip the check. The jar is cached per version under ~/.cache/stratigraph/ β€” keyed by version, so installing an older stratigraph cannot silently run a newer extractor β€” and every command finds it there afterwards with no flag.

This is the only thing in the tool that touches the network, and it is a command rather than something extract does on your behalf. Extraction and history mining are entirely local; keeping that true is worth one extra line (ADR-0004).

--dry-run prints the URL, the target path and the pinned digest without downloading. For an air-gapped machine, fetch the jar somewhere else and drop it in β€” STRATIGRAPH_CACHE_HOME points the cache anywhere:

STRATIGRAPH_CACHE_HOME=/mnt/share/stratigraph stratigraph extract --repo ../monolith

Building it yourself still works and still wins over a downloaded jar, because a developer who has just run maven means the jar they just built:

git clone https://github.com/imashishsharma/stratigraph
cd stratigraph/extractors/java && ./mvnw package

You can also point at one with --extractor-jar <path>, java.jar in the config file, or STRATIGRAPH_JAVA_JAR. stratigraph doctor reports which of these it found, and when the jar was built.

Or skip the toolchain question entirely

The image carries its own JDK 17, git and the extractor, so nothing on the host matters:

docker run --rm -v "$PWD:/repo:ro" -v "$PWD/out:/work" \
  ghcr.io/imashishsharma/stratigraph extract --repo /repo

The repository is mounted read-only and the fact store is written to /work, never inside the repository being analysed β€” the same rule the CLI follows.

Documentation truncated β€” see the full README on GitHub.

Reviews

No reviews yet

Be the first to review this server!