Setup in 5 Minutes
Zügel consists of a small launcher jar, which you download once from www.hello2morrow.com, and one entry in your agent’s MCP configuration that tells the agent how to start it. There are two ways to set it up, and you can combine them:
- Project-based: the entry and the launcher live in the repository. Recommended for teams: commit both, and everyone who clones the project gets Zügel without any setup of their own.
- User-based: the entry lives in your personal agent configuration. Recommended if you work across many repositories: set it up once, and Zügel is available in every session, whichever repository you are in.
Project-based setup
Put the launcher into a directory named .mcp at the root of your project, and create the agent’s configuration file next to it. For Claude Code that is .mcp.json:
{
"mcpServers": {
"zugel": {
"command": "java",
"args": [
"-jar",
".mcp/zugel-launcher.jar",
"<your-activation-code-or-license-file>",
"--project_root", "."
]
}
}
}
--project_root . binds Zügel to the project as soon as it starts. The path is relative to the directory you start your agent in, which here is the project root.
.mcp.json in the project root is Claude Code’s convention. Every other MCP-capable agent reads the same information from its own file:
| Agent | File | Wrapper key |
|---|---|---|
| Claude Code | .mcp.json (project) or ~/.claude.json | mcpServers |
| Cursor | .cursor/mcp.json or ~/.cursor/mcp.json | mcpServers |
| Windsurf | ~/.codeium/windsurf/mcp_config.json | mcpServers |
| Gemini CLI | .gemini/settings.json | mcpServers |
| Amazon Q Developer CLI | .amazonq/mcp.json or ~/.aws/amazonq/mcp.json | mcpServers |
| VS Code / GitHub Copilot | .vscode/mcp.json | servers |
| Zed | settings.json | contextServers |
| OpenAI Codex CLI | ~/.codex/config.toml | [mcp_servers], TOML |
For the first five the zugel entry above is copied across unchanged. VS Code renames the wrapper and wants a type:
{
"servers": {
"zugel": {
"type": "stdio",
"command": "java",
"args": ["-jar", ".mcp/zugel-launcher.jar", "<your-activation-code-or-license-file>",
"--project_root", "."]
}
}
}
Codex CLI is the same thing in TOML:
[mcp_servers.zugel]
command = "java"
args = ["-jar", ".mcp/zugel-launcher.jar", "<your-activation-code-or-license-file>",
"--project_root", "."]
And Zed nests the command:
{
"contextServers": {
"zugel": {
"source": "custom",
"command": {
"path": "java",
"args": ["-jar", ".mcp/zugel-launcher.jar", "<your-activation-code-or-license-file>",
"--project_root", "."]
}
}
}
}
Nothing else changes: it is the same jar, speaking the same protocol, to all of them. If your agent is not in the list please consult the documentation of your agent about how to setup an MCP server.
Commit .mcp/zugel-launcher.jar and the configuration file to your repository, and every developer of your project benefits from Zügel. The launcher is small (about 2.5 MB) and rarely changes. Claude Code asks each user once to approve a server defined in a project’s .mcp.json.
User-based setup
Put the launcher somewhere in your home directory, e.g. ~/.zugel/zugel-launcher.jar, and register Zügel once for your user, without --project_root. In Claude Code:
claude mcp add --scope user zugel -- java -jar ~/.zugel/zugel-launcher.jar <your-activation-code-or-license-file>
This writes the entry into ~/.claude.json, so Zügel starts in every Claude Code session, whatever directory you start it in. If you write a configuration file by hand instead, spell out the launcher path in full (/Users/you/.zugel/zugel-launcher.jar, or C:\\Users\\you\\.zugel\\zugel-launcher.jar on Windows). Agents start the server without a shell, so ~ is not expanded there.
The other agents work the same way through their user-level files from the table above: ~/.cursor/mcp.json, ~/.aws/amazonq/mcp.json, ~/.gemini/settings.json. Windsurf, Zed and Codex CLI only have a user-level file anyway, and VS Code has a user-level MCP configuration next to .vscode/mcp.json.
Started this way, Zügel is not bound to a project yet. Its first message asks the agent to bind it to the repository it is working in, which is the agent’s working directory, using the switch_project tool. So the first thing each session does is bind itself, and you never type a path. If you move to another repository in the same session, the agent calls switch_project again, or you simply tell it “switch Zügel to the other repository”. A repository Zügel has analyzed before loads from its cache within seconds; one without a zugel.json starts in bootstrap mode, as described below. Every reply names the project it answers for, so the agent cannot mix up two repositories.
Using both
A repository with its own project-based entry keeps using it for sessions you start inside it: Claude Code prefers a project entry over a user entry with the same name. Name the entry zugel in both places. Under different names you would get two Zügel servers in one session, and every tool twice.
The single positional argument is your license activation code (or a path to a license file) — always required. Everything else is an optional named flag:
--project_root <dir>(optional): the project to bind at startup, as in the project-based setup. If it is missing, Zügel starts in unbound mode: it is not bound to any project until the agent callsswitch_projectwith the repository it is working in, as in the user-based setup. Either way, switching to another project later is possible. Zügel never falls back to its working directory: a stdio MCP server inherits the agent’s working directory, which is not necessarily your project, and thecwdfield some clients accept is ignored.--license_server_url <url>: only for an on-prem license server; defaults tohttps://www.hello2morrow.com, so most users never set it.--config_dir <dir>: see below.--launcher.server_version=<version>,--launcher.repository_url=<url>and--launcher.no_update_check: these control the launcher; see What the launcher does. Launcher options are always written as a single argument, with=.
Please note that Zügel requires Java 21 or higher. It also must have the same or a higher version compared to the Java version used by the project. In other words, Zügel cannot analyze a Java 25 project when it runs on a Java 21 runtime.
What the launcher does
The jar you download is a launcher, not Zügel itself. Its job is to keep Zügel up to date. Every time it starts, it checks for a newer version and, if there is one, downloads it and starts it right away. That includes the very first start, so no restart is needed. Only on a slow connection, where the download takes more than a few seconds, does it start the version it already has and use the new one from the next start. On the very first start it then asks you to restart once the download has finished. While a session runs, it keeps checking in the background. Every download is verified against its checksum and code signature before it is used.
The launcher and Zügel have separate version numbers. The launcher is on 1.x and changes rarely; Zügel versions start with the year, e.g. 26.6.1.
If you do not want automatic updates, pin a version with --launcher.server_version=<version> in the launch arguments: 26.6.1 for exactly that build, or 26.6.x or 26.x to stay on that line. With 26.x the launcher never moves you to 27.x or higher. Without the option you always get the newest version. We recommend that, because Zügel improves at a high pace. Earlier versions read the pin from serverVersion in zugel.json; that key is now ignored, and Zügel tells you so. The pin option and the immediate updates need launcher 1.0.5 or newer.
The launcher supports mirroring with --launcher.repository_url=<your mirror url>. Without it, downloads come from https://eclipse.hello2morrow.com/jenkins/architectureMcp/. Sites that do not allow update checks can pass --launcher.no_update_check.
Now start your agent. The server also needs a project configuration — zugel.json, in the project root by default — but you usually don’t write that one: if it is missing, the server starts in bootstrap mode, looks at the project root for a build it recognizes, tells the agent what it found, and offers to generate the configuration itself.
If your environment does not allow new files at the project root (some security policies don’t), add --config_dir <dir> — e.g. "--config_dir", "config/mcp". It must be a relative path, and in a user-based setup it applies to every project the session binds. The configuration file and the .baselines/ directory then live in that subdirectory of each project instead, and every relative path inside the configuration resolves against it; generate_config writes the paths accordingly. Build-system detection is unaffected — your pom.xml / Gradle files are always read from the project root, never the config directory — so bootstrap and generate_config behave exactly the same. Everything else works unchanged.
Creating the Configuration File
There are basically two ways to generate zugel.json. If you have a Sonargraph-Architect license and your project is already configured for Sonargraph, you can generate the configuration from Sonargraph (via the File menu). But since Zügel can also work completely independently from Sonargraph it can bootstrap the configuration file itself. If it starts and does not find a configuration file it will start in bootstrap mode and the agent will usually offer you to create the configuration file for you. Once it is created it should also be added to your repository. Every time your project changes – e.g. modules are added or removed or dependencies change (Java), you should regenerate the configuration file. Zügel needs Java 21+ installed on your machine to run. The exact bootstrap procedure depends on the programming language and is described below for Java, C# and Python.
Setup for Java Projects
Case 1 — Maven. The agent calls the generate_config tool. The server walks your pom tree for the module structure (names, source roots, generated-source roots, inter-module dependencies) and asks Maven itself for each module’s resolved classpath — only the build tool can resolve versions, dependency management, and profiles correctly. Sibling modules become dependsOn entries so internal code resolves to source, and jars are written home-relative (~/.m2/...), so the generated file works on every developer’s machine and can be committed. The server then initializes in place; all tools work immediately, no restart.
Case 2 — Gradle. Same single tool call, different machinery: because a Gradle build is a program only Gradle can evaluate, the server injects a small init script via --init-script — your build files are never touched — and lets Gradle report every JVM project’s source roots, project dependencies, and resolved external classpath (without building a single subproject). Progress streams live while Gradle configures, and the result is the same portable, committable configuration. This is the path we validated on the gradle/gradle build itself: 214 modules, one tool call.
Case 3 — Bazel. Same one tool call again. Bazel is asked rather than read, exactly as Gradle is: BUILD files are data, but data containing glob(), macros and rule expansion, so reading them is not the same as knowing what they mean. So the server asks: a query for the target graph, a configuration query for each target’s source jars and compile classpath, and a build to materialize what does not exist until something produces it. It then shuts the Bazel daemon down again, so scanning a few workspaces does not leave several multi-gigabyte JVMs resident.
One decision shapes everything you will see in the output, so it is worth knowing up front: a Zügel module is one source root, not one Bazel target. Bazel has no unit corresponding to a Maven module — its unit is the target, at whatever granularity the build author found convenient. Google’s Copybara compiles 325 java targets out of three directories; modelling that as 325 modules would be faithful to the build and useless to a human, because nobody on that project thinks in terms of copybara_lib versus labels. They think in terms of java/, javatests/ and third_party/bazel/, and those are the three modules you get. Source roots are worked out from each file’s declared package rather than from directory names, because Bazel workspaces rarely follow the src/main/java convention — in grpc-java, roots like api/src/context/java and okhttp/third_party/okhttp/main/java are found correctly without being told anything. Generated java arrives as source jars rather than files, so those are unpacked into .zugel-bazel/generated-sources/ beside your configuration; add that directory to .gitignore (the tool reminds you).
One caveat is specific to Bazel and the tool warns about it: do not commit a Bazel-generated zugel.json. Every jar Bazel reports lives under bazel-out/<platform>-<mode>/…, so the file is tied to the machine that produced it, and bazel clean deletes the whole tree it points at. Regenerate rather than commit — it is quick once Bazel’s cache is warm. If you do scan against a stale one, Zügel now says so instead of quietly analyzing a smaller model than your code. On Windows, a workspace with Maven dependencies also needs BAZEL_SH pointing at a bash (Git Bash will do), or rules_jvm_external fails to fetch before Zügel ever sees the workspace.
In each case the invocation runs your project’s own wrapper, ./mvnw, ./gradlew or the bazel (usually Bazelisk) on your path. If it fails — the classic cause is that the server process lacks your shell environment, say a pinned JDK — the error hands the agent the exact command to run in a real terminal, and a second generate_config call picks up the result. And when your build structure changes later (modules added or removed, dependencies bumped), just run generate_config again: it regenerates only the machine-derived project section, preserves everything you wrote by hand, and reports which modules appeared or disappeared.
One thing is worth setting before you go far, because it decides what everything else measures: a build reactor is not the same thing as an architecture. Real projects carry modules that have code but no design intent worth checking — documentation builds, samples, tooling, vendored third-party sources — and a generator faithfully writes every one of them into your configuration. moduleFilter is where you record which modules actually are the system:
"moduleFilter": {
"includes": ["com.example.**"],
"excludes": ["com.example.docs"]
}
Both lists are optional, and an absent or empty includes means everything the build reports. Two lists rather than one, because naming what belongs is usually shorter than naming what does not: a large reactor buries test and sample modules deep in the tree under names with nothing in common, and a single include — com.example.** — states the rule your project already follows without naming any of them. Excludes are checked first, so an exclude always beats an include. Patterns use the same wildcard syntax as the .arc files (** for anything, * within one dot-separated segment), anchored to the whole module name.
It sits outside the project section deliberately, and that is what makes it survive: deleting a module from project.modules by hand does not, because the next generate_config rebuilds that section from your build files and puts it straight back. The filter shapes what a generator writes rather than how the file is read, so an edit takes effect on the next generate_config — which also accepts includeModules / excludeModules arguments if you would rather ask the agent to adjust it for you. Do treat it as a trade and not a free action: a module you leave out is not parsed, holds no components, and appears in no cycle or violation, so other modules’ references to it stop resolving as internal code.
Case 4 — anything else. No Maven, no Gradle, no Bazel: write zugel.json by hand. If your project is already imported into Sonargraph, you can use File / Export Zügel Configuration... to generate a skeleton configuration file where you only need to add the class path details for your modules. This is what the file looks like — and also what the generators produce, since it is the same file:
{
"language": "java",
"project": {
"javaRelease": "21",
"modules": [
{
"name": "MyApp",
"moduleRoot": ".",
"sourceRoots": [
"src/main/java"
],
"generatedSourceRoots": [
"target/generated-sources"
],
"classpath": [
"~/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.17.2/jackson-databind-2.17.2.jar",
"~/.m2/repository/org/apache/commons/commons-lang3/3.14.0/commons-lang3-3.14.0.jar"
]
}
]
},
"arcFiles": [
"architecture/MyApp.arc"
],
"cycles": {
"componentCycleTolerance": 3,
"packageCycleTolerance": 0
}
}
arcFiles and cycles are controlled by you in all cases — the generators never touch them. Without any .arc files the server runs in cycles-only mode: no rules yet, but the full dependency model, cycle detection, and metrics are live from the first minute — many teams start exactly there and add rules once they’ve seen the cycle report.
Once configured and downloaded, the server parses the sources (about 20 seconds for 10,000 files, cached for ~2-second warm starts after that), compiles any .arc rules, pins a default baseline, and announces its tools.
C# Setup
Set Zügel up as described above, project-based or user-based. Start the agent in the root directory of your solution (with a user-based setup it first binds Zügel there), and ask it to call generate_config followed by reload_all. After that the agent will analyze your project and create the initial baselines.
Two things are different for C#. You need the .NET 10 SDK — the SDK, not just the runtime, because Zügel opens your solution through MSBuild and only an SDK ships MSBuild. And Zügel needs a .sln or .slnx file: if your repository has several, one is picked, written into the configuration and the others are named, so pointing it somewhere else is an edit rather than a guess. Nothing is downloaded — the Roslyn based parser we also use in Sonargraph ships inside the Zügel jar — and if the solution’s NuGet packages have never been restored, the first scan restores them for you.
This is how the generated zugel.json configuration file looks like for typical C# project:
{
"language": "csharp",
"project": {
"solution": "src/MyApp.sln",
"modules": [
{ "name": "MyApp", "project": "MyApp(net10.0)",
"sourceRoots": ["src/MyApp"],
"generatedSourceRoots": ["src/MyApp/obj/Debug/net10.0"] }
],
"generatedPatterns": ["**/*.designer.cs", "**/*.generated.cs", "**/*.g.cs"]
},
"arcFiles": ["architecture/MyApp.arc"]
}
A project targeting several frameworks is one project per framework as far as Roslyn is concerned, so exactly one of them is analyzed. The choice is written to project and you can change it there — componentIds never mention a framework, so adding one to a .csproj cannot invalidate a baseline. Projects declaring <IsTestProject>true</IsTestProject> are left out of the model altogether. generatedSourceRoots points into obj, where MSBuild puts generated sources; declaring it is what keeps Debug out of your componentIds.
Python Setup
Set Zügel up as described above, project-based or user-based. Then start your agent in the root directory of your Python project and ask it to use the generate_config tool to generate the zugel.json file followed by a call to reload_all. After that the agent will analyze your project and create the initial baselines. It will inform you about existing cyclic dependencies on the file and directory/package level. If you add architecture definition files it will also check your architectural rules.
The Python version of zugel.json looks like that:
{
"language": "python",
"project": {
"modules": [
{ "name": "widget", "sourceRoots": ["src"] }
],
"generatedPatterns": ["**/*_pb2.py", "**/*_pb2_grpc.py", "**/generated.py"]
},
"arcFiles": ["architecture/Widget.arc"]
}
The configuration for tolerated cyclic dependencies is the same as in Java. To mark generated code you can provide a list of patterns that match generated Python files. This is relevant for cycle analysis. Cyclic dependencies only consisting of generated files are tolerated automatically.
First Steps
Now start your agent and ask it “Which version of Zügel is running?“. If you get a meaningful reply, you have set up Zügel successfully. With a user-based setup, the agent binds Zügel to your repository first; if it has not done so, ask it to “switch Zügel to this project“. Answer “yes” if it asks whether it should create the configuration file. After the configuration is up and running you can relax. From now on your agent will not add cyclic dependencies to your code. Once you have an architectural model defined, you can also be sure, that the agent will never add dependencies that violate your architectural rules. If you want to be on the safe side tell your agent that it must consult Zügel before adding new dependencies. While that is part of the agent instructions generated by Zügel, agents sometimes “forget” about that part.
It is highly recommended to work on a basic architecture definition which you can easily create via prompts, e.g. “Create a layered architecture split into domains“. Or define your layers via package names or annotations, e.g. “All classes in the controller package form the controller layer, all classes in the persistence package form the database layer and only the controller layer can access the database layer“. Having an architecture adds a lot of value to the other features of Zügel, e.g. breaking up a big ball of mud becomes a lot easier once you have defined some basic rules.
Then you can ask your agent about cyclic dependency clusters in your code base. Since Zügel maintains a complete dependency model of your code it can use clever graph algorithms to come up with a breakup plan. It also helps to save tokens because your agent can now ask Zügel about dependencies instead of having to use grep to find them. Once it lists the cyclic clusters you can ask the agent for ways to improve the structure. You will be surprised how quickly you will be able to improve a big ball of mud that has grown over years. This still requires somebody with a good understanding of the code base and a good test suite to confirm that the refactorings done by the agent don’t break anything, but it is as close to a magic fix-it button as we can get today.
Please refer to this article for a complete explanation of Zügel’s capabilities and features. Consider it the living manual for Zügel. It is updated whenever we add new features.