You have an existing Graphitron subgraph and you want a tight edit-save-see-it loop. The short version: run two long-lived terminals, one for mvn graphitron:dev (regenerates Java from your .graphqls, serves an LSP to your editor, and serves an MCP server to your agent), one for mvn quarkus:dev (hot-reloads the running app). Edit the schema, save, and the change flows through both without restarting anything.

On macOS the filesystem-watch half of this loop is slow on its own. Attaching your editor to the LSP is what makes saves feel instant. See Why macOS needs the LSP.

TLDR

Step What to run

1. Regenerate + LSP

mvn graphitron:dev in a side terminal. Binds an LSP on localhost:8487, watches your .graphqls, regenerates only the changed files under target/generated-sources/graphitron.

2. Run the app

mvn quarkus:dev in another terminal. Quarkus watches target/generated-sources/ by mtime and hot-reloads only the classes that actually changed.

3. Connect your editor

Point your LSP client at localhost:8487 (TCP) for diagnostics, hover, completion, go-to-definition, and find-references on .graphqls. IntelliJ users: see IntelliJ users.

4. Connect your agent (optional)

Point an MCP-aware agent (Claude Code, Cursor) at http://127.0.0.1:8488/mcp. See Connect your agent to the MCP server.

5. Edit and save

Save a .graphqls. Graphitron regenerates, Quarkus reloads, your editor refreshes diagnostics. No restart of either terminal.

Both Maven goals are long-lived. Start them once at the beginning of a session and leave them running; stop with Ctrl+C.

The loop in detail

mvn graphitron:dev is one JVM, one terminal, one socket. It binds an LSP server on localhost:8487, watches your schema sources, and on every save re-runs the generator. Writes are idempotent: only files whose rendered content actually changed touch disk (SHA-256 comparison), and types you delete from the schema have their generated files removed. So git diff after a session shows only what the edit predicts, and downstream tools recompile only what changed.

That idempotence is exactly what lets quarkus:dev sit on top. Quarkus detects source-root changes by mtime; because unchanged generated files keep their mtimes, a one-type schema edit triggers a reload of just that type’s classes, not the whole schema. The same is true of IntelliJ’s incremental compiler and Spring Boot DevTools, so no Graphitron-specific build plugin is needed for the reload half.

If 8487 is already in use, pass -Dgraphitron.dev.port=N to mvn graphitron:dev and use the matching port in your editor config. The goal fails fast on a bind conflict with a message naming the property; it never silently rebinds to a port the editor would not know about.

You can run ordinary builds in the same checkout while a dev session is up. Graphitron keeps a warm-start cache for the workspace, one file held by one process at a time, so a mvn test or mvn package that starts while the dev session holds it pauses for at most a second or two and then prints:

[WARNING] graphitron: could not use the shared fact store for this workspace, so this run captured
its facts in memory instead of waiting for it. [...] This costs warm-start speed and nothing else:
the generated output is identical.

That is the expected message, not a failure. The cache only makes a run start faster; the build reads your schema and generates exactly the same code without it, so there is nothing to configure and nothing to clean up. If you would rather have the warm start back for a long build, stop the dev session for its duration.

A surface goes quiet after a build. If hovers or inlay hints stop appearing for a schema that was fine a moment ago, and the build log carries a warning that a read (the hover read, the workspace diagnostics drain) ran out of its budget, a relation the language server reads has become too expensive to answer. The editor keeps whatever it was already showing rather than telling you the schema changed, so nothing you see is wrong; it is just no longer being refreshed for that surface. The warning names the read, which is what to bring to a bug report. The statement that overran is logged at DEBUG on no.sikt.graphitron.lsp.state.StoreAccess, so enable that logger if a report needs it. Diagnostics you can already see stay on screen: a drain that ran out of budget publishes nothing rather than clearing them. The budgets are not configurable, and the next request is served normally, so there is nothing to do about it locally beyond reporting the read.

For the full picture of how the schema watcher, classpath watcher, and generator dispatch cooperate inside the dev goal, see the dev loop reference in the architecture docs.

Compiled generated classes

graphitron:dev compiles the generated code, so the dev tools can run it. In a dev session the goal writes the generated Java and compiles it into its own output directory, target/graphitron-classes/<outputPackage>/, keeping a runnable image of your whole schema as you edit. Only the classes an edit touches are recompiled, so a one-field change is fast even on a large schema. The immediate payoff: the dev loop’s MCP tools can execute a query against your resolvers in-process, with no app server and no quarkus:dev needed to see what a query returns.

Do you need to configure anything? Usually not. Graphitron writes to its own directory and never touches a file your build owns, so there is nothing to guard against and no required setup. Two situations where a one-liner helps:

  • Spring Boot DevTools, plain java, IDE runs. These load .class off the classpath, so putting target/graphitron-classes ahead of target/classes on the run classpath lets your running app use Graphitron’s fresh classes directly. Ordering is what matters; Graphitron’s copy wins over any copy your own build produced.

  • Quarkus dev (quarkus:dev). Do not add Graphitron’s directory to the Quarkus classpath. Quarkus recompiles the generated sources itself and will not pick up an external .class, so front-loading our directory buys nothing and only risks shadowing Quarkus’s own reload. Run quarkus:dev exactly as today; Graphitron’s compile still powers the in-process MCP query tools running alongside it.

Optional: skip the redundant compile. Independently of the above, you can stop your own build from re-compiling the generated sources (they stay a source root for IDE indexing and go-to-definition either way). This only saves double-work; it is never required for correctness, because Graphitron writes to a separate directory:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <excludes>
      <exclude>**/<outputPackage-as-path>/**</exclude>
    </excludes>
  </configuration>
</plugin>

If you skip the classpath step, nothing breaks. Your app runs exactly as today: your build compiles the generated sources into target/classes and runs from there, while Graphitron’s separate output still powers the in-process MCP query tools; it just is not fed to your running app. There is no race and no corruption to guard against, because the two never write the same file.

To turn Graphitron’s compile off entirely (giving up the MCP query tools too), run with -Dgraphitron.dev.compile=false. Because the output directory is Graphitron’s alone, there is no cross-writer race and no fail-fast check: every misconfiguration degrades to today’s generate-only behaviour rather than corrupting bytecode.

Why macOS needs the LSP

Java’s WatchService has no native file-event backend on macOS; it falls back to a polling implementation that only notices a change on its next poll. In practice a save can take several seconds to register, which makes the filesystem-watch path of the dev loop feel laggy on a Mac in a way it does not on Linux.

The LSP path sidesteps this entirely. When your editor is attached to the LSP on localhost:8487, the editor’s save event drives the regeneration directly, instead of waiting for the polling watcher to notice the file changed on disk. So on macOS, attaching your editor to the LSP is not just for diagnostics and completion: it is what restores an instant save-to-regen loop. The schema watcher still runs as a headless fallback for saves made outside the attached editor; it is just slower on this platform.

Linux and Windows have native watch backends and do not suffer this latency, but connecting the editor is still worth it for the diagnostics, hover, completion, go-to-definition, and find-references the LSP provides.

Connect your editor to the LSP

The LSP speaks over TCP on localhost:8487. Any LSP client that can connect to a TCP socket works: point it at that address and register it for .graphql and .graphqls files. Neovim, Helix, and VS Code can target a TCP socket directly. IntelliJ needs one extra hop, covered next.

Find usages

Ask your editor for references (Find Usages) on a name in a .graphqls file and you get every other site in the schema that uses it.

On a type name that is every field and argument declared with that type, every implements, and every union member. Put the cursor on the declaration or on any use of it; both ask the same question.

On a name inside a directive it is every coordinate bound to the same thing: the other types mapped to a table, the other fields bound to a column, every coordinate naming a service class or one of its methods, every @reference path keyed on a foreign key. Two spellings of one table find each other, since the match is on the table the name resolves to rather than on the text. A field that binds a column by name alone, with no @field, is a use of that column too, because the generator reads it as one.

Two things to know about the answer.

It is the last capture’s view of your schema. A usage you typed a moment ago appears once graphitron:dev has picked the file up, and a result in a file you have been editing points at the line it was captured at. The whole list rides one cadence, so it is never half live.

A result lands on the site the capture recorded, which is not always the name you clicked. An implements or a union member is positioned on the type token itself; a field or an argument is positioned on its own declaration, so a use in films: [Film!]! lands on films rather than on Film.

IntelliJ users

IntelliJ’s LSP support (both the Ultimate built-in API and the LSP4IJ plugin) launches a language server as a subprocess and talks to it over stdin/stdout; neither connects to a TCP socket out of the box. The simplest bridge is netcat: a nc 127.0.0.1 8487 subprocess copies bytes between IntelliJ’s stdio and the dev server’s socket.

Set it up with LSP4IJ (Red Hat), which works in both IntelliJ Community and Ultimate:

  1. Install LSP4IJ from Settings → Plugins → Marketplace.

  2. Open Settings → Languages & Frameworks → Language Servers, add a new server, and import (or fill in) this configuration:

    {
      "name": "Graphitron",
      "programArgs": {
        "default": "sh -c \"nc 127.0.0.1 8487\""
      },
      "languageMappings": [
        {
          "language": "GraphQL",
          "languageId": "graphql"
        }
      ],
      "fileTypeMappings": [
        {
          "fileType": {
            "patterns": [
              "*.graphql"
            ]
          },
          "languageId": "graphql"
        },
        {
          "fileType": {
            "patterns": [
              "*.graphqls"
            ]
          },
          "languageId": "graphql"
        }
      ]
    }

    The programArgs command is what bridges stdio to the dev server’s TCP socket; the mappings register the server for .graphql and .graphqls files.

  3. Start mvn graphitron:dev in a terminal first (the bridge connects to it; if the dev goal is not running, nc exits immediately and IntelliJ reports the server as crashed).

  4. Open a .graphqls file. LSP4IJ spawns the bridge, attaches, and you get diagnostics, hover, completion, go-to-definition, and find-references.

Notes:

  • nc ships with macOS and most Linux distributions. On Windows, install a netcat (for example ncat from Nmap) and adjust the command, or use an editor that connects to TCP directly.

  • If you overrode the port with -Dgraphitron.dev.port=N, change 8487 in the command to match.

  • The dev goal serves all connected clients off the same warm catalog, so you can attach IntelliJ and another editor at once; a save in either is seen by both.

  • A native IntelliJ plugin that removes the manual LSP4IJ + netcat setup is on the roadmap; until it ships, this is the supported IntelliJ path.

Connect your agent to the MCP server

The same mvn graphitron:dev process that serves the LSP also runs an MCP server on http://127.0.0.1:8488/mcp (Streamable HTTP, loopback only). An MCP-aware agent (Claude Code, Cursor) connected to it gets read-only context about your project: it can search the bundled manual (docs.search), discover tables in your jOOQ catalog (catalog.search, catalog.describe), inspect the classified schema, @service / @condition / @record bindings, and the current validation diagnostics, plus a /mcp__graphitron__about slash command that orients it on the loop. Point the dev loop at a database and the agent can also run queries against your generated resolvers in-process (the execute tool; mutations always roll back), closing the loop from schema edit to real query result without an app server. See Run a query without an app server.

For Claude Code, run the line the dev server prints on startup:

claude mcp add --transport http graphitron http://127.0.0.1:8488/mcp

Unlike the LSP port, the MCP port is fixed at 8488 and not overridable, so a committed .mcp.json stays valid across machines. If 8488 is already taken, the dev goal fails fast naming the conflict rather than rebinding.

The full setup, the complete tool list, and the .mcp.json for zero-config sharing are in Agent context over MCP.

Query the fact store while the session runs

The dev session keeps one fact store open for its whole run, and every answer it gives comes out of that store: the classifier’s verdicts, the catalog census, the diagnostics, the @service bindings. When an answer looks wrong, the fastest way to find out why is usually to query the rows behind it. The console is that door: a read-only SQL prompt on the session’s own store, live as rounds land.

A session without a console tells you how to start one, and a session with a console tells you how to connect to it. You never have to compose either command yourself:

$ mvn graphitron:dev
graphitron:dev: no fact-store console (<storeConsole> or GRAPHITRON_DEV_STORE_CONSOLE). To query
graphitron:dev: this session's facts with psql, restart with:
graphitron:dev:   GRAPHITRON_DEV_STORE_CONSOLE=true mvn graphitron:dev

$ GRAPHITRON_DEV_STORE_CONSOLE=true mvn graphitron:dev
graphitron:dev: fact-store console up, read-only, 219 relations linked, 127.0.0.1 only.
graphitron:dev:   PGPASSWORD=graphitron psql -X -h 127.0.0.1 -p 32973 -U graphitron -d store

Paste the second line into another terminal and you are in. The port is a fresh free one each start, so two dev sessions never collide; copy it from the log. Pin <port> in the POM if you would rather have a stable port, and note that a pinned port collides when you run two sessions.

The listener is on 127.0.0.1 and the console refuses to start if it cannot confirm that, so it is not reachable from anywhere else.

What you get is the session’s live rows, read-only: a query after a save sees that round’s facts. "Read-only" means writes to the store’s relations are refused, which stops a mistyped UPDATE while you are poking around. It is not a sandbox, so nothing you type at this prompt should be load-bearing.

Three things about the prompt itself. psql’s backslash commands (\dt`, \d <table>) do not work, because the server implements only part of PostgreSQL’s system catalog; find your way around with information_schema.tables and information_schema.columns instead, and note that its identifiers are upper-case (WHERE table_schema = 'PUBLIC'). psql is the client: the server speaks the PostgreSQL wire protocol, but JDBC-based tools (DBeaver, IntelliJ’s PostgreSQL driver) cannot connect to it. And the printed command passes -X, so your ~/.psqlrc is skipped: a startup file written for a real PostgreSQL server sets things this one does not have (application_name, bytea_output), and without -X those become syntax errors printed ahead of every answer. Drop the -X if you would rather have your own settings and can live with that.

An agent on the session’s MCP server reads the same coordinates as structured fields, so it connects without scraping them out of this log. See Query the fact store from an agent.

See also

← How-to index </content> </invoke>