Release notes

Changes per released version of the Architecture MCP server, newest first. Starts at 26.3.1; earlier versions shipped without release notes.


26.5.2 — 2026-08-26

Fixed

In practice agents were reaching for analyze_cycle on package cycles and never calling suggest_relocations at all, so the cheapest repair was routinely skipped.


26.5.1 — 2026-08-21

Upgrading

// before                          // now
{ "fqn": "...",                    { "fqn": "...",
  "filterName": "Mod/A/Foo",         "components": [
  "perFile": [ ... ] }                 { "filterName": "Mod/A/Foo", "perFile": [ ... ] } ] }

One name can legitimately belong to several components: a C# partial type is one type written across several files, and the halves can be assigned to different artifacts — one under Domain/ and another under Generated/ is normal. Answering with one of them was answering a different question. Expect a single entry for most types and do not assume exactly one.

Added

What you need on the machine is the .NET 10 SDK — the SDK, not just the runtime, because solutions are opened through MSBuild. The parser itself ships inside the jar, so nothing is downloaded and it works offline. If the solution's NuGet packages have never been restored, the first scan restores them for you rather than telling you to.

A solution file is required, and generate_config records which one it chose. Where a repository holds several — nhibernate-core has three — the choice is written to project.solution and reported, so pointing it at a different one is an edit rather than a mystery.

The one thing that surprises C# developers: .arc rules address directories, not namespaces. A component is a source file and its package is the folder it sits in, relative to the project directory — the same rule as Java. C# does not require namespaces to follow folders, and where a codebase's don't, the rules follow the folders. So an artifact is written include "MyApp/Services/**", not include "MyApp.Services.**".

An external component is External/<assembly>/<namespace>/<type> — for example External/System.Collections/System/Collections/Generic/List<T>. The assembly is part of the name because it is the unit of external identity in .NET and a namespace does not imply one: NHibernate's Antlr.Runtime.* types come from Antlr3.Runtime.dll. It is a single segment, so External/System.Collections/** names everything from one assembly. Generic parameters are part of the name — arity is part of a .NET type's identity, so Task and Task<TResult> are different components — and a nested type has none of its own: a dependency on HashSet<T>.Enumerator lands on HashSet<T>.

Six attribute retrievers, the same ones Sonargraph offers and with the same semantics: CSharpTypeOf, CSharpExtendsClass, CSharpImplementsInterface, CSharpIsClass, CSharpIsInterface and CSharpIsEnum. They assign components by what a type is rather than by where it sits — include "CSharpImplementsInterface: **.IRepository" gathers every repository however each one is named. They match dotted type names, so a single * stops at a dot.

Two more things worth knowing before you write rules. A project that targets several frameworks is analysed under exactly one of them — the newest, recorded in the configuration — and componentIds never mention it, so adding a target framework cannot invalidate a baseline. And a project that declares IsTestProject is not part of the model at all: its cycles and violations would be noise against what the production code owes, so no rule can govern the test-to-production direction.


26.4.1 — 2026-08-20

Added

There is no build system to ask, so the structure comes from the layout. A virtualenv tells you where packages are installed and never where the sources are, so poetry, uv, pdm and conda have nothing to contribute here. Every pyproject.toml is one module — named after the distribution it declares — with its source roots taken from setuptools, hatch or poetry metadata, or from src-vs-flat convention when the metadata says nothing. A project with no pyproject.toml at all is still configured from its directories.

Nothing has to be installed first. An import names its own target, so a freshly cloned repository with no environment set up yields a complete, governable model. What you do need is a Python 3 interpreter the server can find — it looks for python3, then python, and on Windows the py launcher first. Prefer 3.10 or newer: Python's own parser only understands the syntax of its own release, so an older interpreter reports a project's modern syntax as errors in your source. To pin a specific one, add "interpreter" to the project section.

Writing .arc rules for Python. A componentId is module/path/to/file — the source file's location under its source root, with no extension. locate_fqn works as it does for Java, and resolves three kinds of name to a component: a class, a module-level function, and the module itself — pkg.archive names the file, which is what an import writes.

Python has two attribute retrievers of its own. PythonTypeOf matches any direct or indirect base class, so include "PythonTypeOf: pydantic.BaseModel" gathers a DTO layer, and PythonTypeOf: airflow.sdk.bases.operator.BaseOperator an operator layer — roles no naming convention identifies. Where Java needs four hierarchy retrievers, Python needs one: it has no class/interface split, so an ABC or a Protocol is simply another base. PythonHasDecorator matches any decorator on a class or a function — function decorators being where the signal usually is — and matches either spelling: the written app.get, whose head is an instance and resolves to nothing, or the resolved airflow.decorators.task, which keeps matching after import task as t. The Java retrievers do not apply, and naming one in a Python project fails at rule-compile time rather than silently matching nothing.

One limitation: the hierarchy walk stops at the project's edge, since installed packages are not parsed. Deriving from BaseModel is seen; deriving from a third-party class that itself derives from BaseModel is not.

Generated code is declared by pattern rather than by root. Java points generatedSourceRoots at a directory; Python has no such directory, because a module's dotted name is its path from the source root — anything importable as pkg.api.models has to live at pkg/api/models.py, beside the hand-written code. So declare "generatedPatterns": ["**/*_pb2.py"] in the project section instead. Matching files get the same treatment generated Java gets: cycles among them, and violations originating in them, are excused. generate_config seeds the protobuf globs when your project actually has such files.

One configuration covers one language. A repository that is genuinely both needs a server per language, each with its own --config_dir.

Changed

Getting it needs the new launcher, which does not update itself — re-download zugel-launcher.jar if you want this.


26.3.6 — 2026-08-18

Added

A Bazel module is one SOURCE ROOT, not one target. Bazel has no unit corresponding to a Maven module — its unit is the target, at whatever granularity the build author found convenient — so a workspace compiling 325 targets out of three directories would otherwise be modelled as 325 modules nobody on that project would recognise. Source roots are derived from each file's declared package rather than from directory names, because Bazel workspaces do not follow the src/main/java convention and often have no src anywhere.

Generated java comes along: the source jars produced by genrule, java_proto_library, java_grpc_library and the like are unpacked under <config dir>/.zugel-bazel/generated-sources/, which wants a .gitignore entry — the tool says so. Those components are flagged generated, so they are already excused from cycles and violations.

One caveat, and generate_config warns about it: a Bazel-generated zugel.json is machine-specific. Every jar Bazel reports lives under bazel-out/<platform>-<mode>/…, so a configuration generated on macOS does not resolve on Linux, and bazel clean deletes the entire tree it points at. Regenerate it rather than committing it — that is cheap once Bazel's cache is warm. If you do scan against a stale one, the classpath warning below now tells you instead of letting the model quietly shrink.

Verified on Windows as well as macOS, on every route Bazel installs by there — Bazelisk via npm, winget, Chocolatey and Scoop. One Windows prerequisite is not ours to fix and worth knowing: a workspace with Maven dependencies needs BAZEL_SH pointing at a bash (Git Bash will do), or rules_jvm_external's fetch fails before we ever see it.

Fixed

These were false violations, so your next scan may show fewer. If you changed code, or loosened an .arc file, to satisfy one of them, that change can be reverted.

The same gap dropped dependency edges: new SomeProjectClass() { … } recorded no NEW edge, so that coupling was invisible to your architecture rules. It is visible now, so a violation this was hiding may appear on your next scan — the rule catching something real, not a new restriction.


26.3.5 — 2026-08-17

Added

Retriever Matches
JavaIsClass any type that is a class
JavaIsInterface any type that is an interface
JavaExtendsClass any direct or indirect base class of a non-interface type
JavaImplementsInterface any interface implemented by a non-interface type, transitively
JavaExtendsImplementsInterface as above, but interfaces extending interfaces count too
JavaTypeOf any direct or indirect base type, class or interface
JavaHasAnnotation any annotation on the type

Wildcards work on fully qualified names, so * stops at a dot and ** does not: **.Controller matches the interface in any package, *.Controller in one package level only. A type may carry several values (a class implements many interfaces) and matching any one of them matches.

As in Sonargraph, only the component's main type is considered — the one named like the file — and external components match nothing. The walk up a hierarchy stops at the first type outside the analysed sources: its name still counts, but nothing above it does. A retriever name the configured language does not provide is a rule-compile error rather than a pattern that silently matches nothing.

Two Sonargraph retrievers are deliberately absent: JavaHasAnnotationValue, which needs annotation property values, and JavaBelongsToAggregateRoot, which needs generic type arguments. JavaHasAnnotation differs from Sonargraph's in one way on purpose: it sees annotations on the type itself, not on its fields and methods, so a detail of one member cannot decide the whole component's artifact.

explain_architecture_dsl covers them, so an agent asked to express a grouping that no name pattern can capture will find them without being told they exist.

Fixed

A component's contains list consequently holds every declared type rather than only the top-level ones. Duplicate-FQN reporting is unaffected on purpose: nested types are excluded from it, because no dependency edge ever resolves to a nested name (edge targets are normalized to the enclosing top-level type), and a nested FQN can only collide when its enclosing type already has.

Upgrading


26.3.4 — 2026-08-11

Changed


26.3.3 — 2026-08-10

Added


26.3.2 — 2026-08-10

Improved


26.3.1 — 2026-08-05

Upgrading

Added

Changed

Fixed

Known issues

ecj does not close the classpath jars it opens. The open set does not accumulate across rescans (macOS and Windows). On Windows those jars stay locked while the server idles until a garbage collection releases them. Generated configurations put only immutable cache jars under ~/.m2 and ~/.gradle on the parse classpath, and class directories hold no handle, so mvn clean is unaffected. Restarting the server releases everything.